server/lib/src/evidence_key.rs
//! **The evidence public-key door** (lane HOL-OCI-EVIDENCE, owner 2026-10-31).
//!
//! Holger signs its evidence (the scan verdict, the image SBOM, the approvals) with the
//! key of `attestation.signer_cert` ([`crate::attest::AttestSigner`]), and serves that
//! evidence to registry clients as OCI referrers and `.att` layers ([`crate::referrers`]).
//! A customer verifies it with `cosign verify-attestation --key <url>`; this module is
//! the `<url>`: a public, read-only door with the public half of the SAME certificate,
//! in the shapes verifiers read.
//!
//! | Path | Body | Content type |
//! |---|---|---|
//! | `GET /-/evidence-key`, `/-/evidence-key.pem` | the SubjectPublicKeyInfo as PEM (`PUBLIC KEY`), what `cosign --key` takes | `application/x-pem-file` |
//! | `GET /-/evidence-key.jwks` | a JWK Set (RFC 7517) with the one key: `kid` the DSSE keyid Holger writes (hex SHA-256 of the certificate), `x5c` the certificate, `x5t#S256` | `application/jwk-set+json` |
//! | `GET /-/evidence-key.crt` | the certificate (PEM) | `application/x-pem-file` |
//!
//! Open on every box, a read-gated one too: a public key is public, and a verifier that
//! must sign in to fetch the key it verifies with is a verifier that never runs. A Holger
//! that signs no evidence answers 404 and names `attestation.signer_cert`.
use base64::Engine as _;
use serde_json::{json, Value};
use sha2::Digest as _;
/// The door's paths.
pub const PEM_PATH: &str = "/-/evidence-key";
pub const PEM_PATH_EXT: &str = "/-/evidence-key.pem";
pub const JWKS_PATH: &str = "/-/evidence-key.jwks";
pub const CERT_PATH: &str = "/-/evidence-key.crt";
/// Is `path` one of this door's?
pub fn is_door(path: &str) -> bool {
matches!(path, PEM_PATH | PEM_PATH_EXT | JWKS_PATH | CERT_PATH)
}
/// What a Holger with no evidence signer answers.
pub const NO_SIGNER: &str =
"this Holger signs no evidence: its config names no `attestation.signer_cert` \
(with `signer_key`), so there is no evidence key to serve";
/// Holger's evidence key, in every shape the door serves. Built once, at boot, from the
/// certificate the signer was loaded with.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EvidenceKey {
/// `-----BEGIN PUBLIC KEY-----` (SubjectPublicKeyInfo).
pub public_pem: String,
/// `-----BEGIN CERTIFICATE-----`.
pub cert_pem: String,
/// The JWK Set, serialized.
pub jwks: String,
/// The DSSE keyid Holger writes into every envelope: hex SHA-256 of the certificate.
pub keyid: String,
}
fn pem(label: &str, der: &[u8]) -> String {
let body = base64::engine::general_purpose::STANDARD.encode(der);
let mut out = format!("-----BEGIN {label}-----\n");
for line in body.as_bytes().chunks(64) {
out.push_str(std::str::from_utf8(line).unwrap_or_default());
out.push('\n');
}
out.push_str(&format!("-----END {label}-----\n"));
out
}
fn b64url(b: &[u8]) -> String {
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b)
}
const OID_EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1";
const OID_P256: &str = "1.2.840.10045.3.1.7";
const OID_ED25519: &str = "1.3.101.112";
impl EvidenceKey {
/// The key of a DER certificate (an evidence signer is P-256 or Ed25519).
pub fn from_cert_der(der: &[u8]) -> Result<Self, String> {
let (_, cert) = x509_parser::parse_x509_certificate(der)
.map_err(|e| format!("the evidence signer's certificate does not parse: {e}"))?;
let spki = cert.public_key();
let alg = spki.algorithm.algorithm.to_id_string();
let key = spki.subject_public_key.data.as_ref();
let thumb = sha2::Sha256::digest(der);
let keyid = hex::encode(thumb);
let mut jwk = match alg.as_str() {
OID_EC_PUBLIC_KEY => {
let curve = spki
.algorithm
.parameters
.as_ref()
.and_then(|p| p.as_oid().ok())
.map(|o| o.to_id_string());
if curve.as_deref() != Some(OID_P256) {
return Err(format!(
"the evidence key is an EC key on {curve:?}, not P-256"
));
}
if key.len() != 65 || key[0] != 4 {
return Err("the evidence key's P-256 point is not uncompressed".into());
}
json!({ "kty": "EC", "crv": "P-256", "alg": "ES256", "x": b64url(&key[1..33]), "y": b64url(&key[33..]) })
}
OID_ED25519 => {
if key.len() != 32 {
return Err("the evidence key's Ed25519 key is not 32 bytes".into());
}
json!({ "kty": "OKP", "crv": "Ed25519", "alg": "EdDSA", "x": b64url(key) })
}
other => {
return Err(format!(
"the evidence key's algorithm {other} is neither P-256 nor Ed25519"
))
}
};
jwk["kid"] = Value::String(keyid.clone());
jwk["use"] = Value::String("sig".into());
jwk["x5c"] = json!([base64::engine::general_purpose::STANDARD.encode(der)]);
jwk["x5t#S256"] = Value::String(b64url(&thumb));
Ok(Self {
public_pem: pem("PUBLIC KEY", spki.raw),
cert_pem: pem("CERTIFICATE", der),
jwks: json!({ "keys": [jwk] }).to_string(),
keyid,
})
}
/// The body and content type of one door path, `None` for a path that is not the door's.
pub fn answer(&self, path: &str) -> Option<(&'static str, String)> {
match path {
PEM_PATH | PEM_PATH_EXT => Some(("application/x-pem-file", self.public_pem.clone())),
JWKS_PATH => Some(("application/jwk-set+json", self.jwks.clone())),
CERT_PATH => Some(("application/x-pem-file", self.cert_pem.clone())),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_p256_certificate_gives_its_key_in_every_shape() {
let k = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
let cert = rcgen::CertificateParams::new(vec![])
.unwrap()
.self_signed(&k)
.unwrap();
let e = EvidenceKey::from_cert_der(cert.der()).unwrap();
assert_eq!(e.public_pem.trim(), k.public_key_pem().trim());
assert_eq!(e.keyid, hex::encode(sha2::Sha256::digest(cert.der())));
let v: Value = serde_json::from_str(&e.jwks).unwrap();
assert_eq!(v["keys"][0]["crv"], "P-256");
assert!(is_door("/-/evidence-key.jwks") && !is_door("/-/evidence-keys"));
}
#[test]
fn a_certificate_that_is_not_one_says_so() {
assert!(EvidenceKey::from_cert_der(b"not a certificate")
.unwrap_err()
.contains("does not parse"));
}
}