use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use async_trait::async_trait;
use ecdsa::signature::Verifier;
use p256::ecdsa::{Signature, VerifyingKey};
use p256::pkcs8::DecodePublicKey;
use reqwest::{Client as ReqwestClient, Url};
use tokio::sync::RwLock;
use crate::errors::{CleanLibraryError, TransportError};
pub const DEFAULT_PUBKEY_CACHE_TTL: Duration = Duration::from_secs(3600);
#[async_trait]
pub trait AttestationKeyLookup: Send + Sync {
async fn lookup_pem(&self, key_id: &str) -> Result<String, CleanLibraryError>;
}
const STAGING_KEY_ID: &str = "projects/cleanlibrary-prod/locations/us-central1/keyRings/cleanlibrary-signing/cryptoKeys/cleanlib-cosign-staging/cryptoKeyVersions/1";
const STAGING_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1yxckriMfZCZHgbnedOO6RHxg+Hq\nV6gRTA5/wIZtTbBLmVSg5//9L6xLvD7CaLEUMg8aH3+8vuytxl4K2wB7uA==\n-----END PUBLIC KEY-----\n";
const PROD_KEY_ID: &str = "projects/cleanlibrary-prod/locations/us-central1/keyRings/cleanlibrary-signing/cryptoKeys/cleanlib-cosign-prod/cryptoKeyVersions/1";
const PROD_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoHzMRp8uj6k8tv4YXpI8r/5eC3sC\nwoKhnouHz83WkAWGMd2U30R4Wpr6JR1WfI+MH6/Q6o4T8cAnYee91oa98g==\n-----END PUBLIC KEY-----\n";
#[derive(Debug, Clone, Default)]
pub struct PinnedKeyMap {
extra: HashMap<String, String>,
}
impl PinnedKeyMap {
pub fn new() -> Self {
Self::default()
}
pub fn with_extra_key(mut self, key_id: impl Into<String>, pem: impl Into<String>) -> Self {
self.extra.insert(key_id.into(), pem.into());
self
}
fn builtin() -> [(&'static str, &'static str); 2] {
[(STAGING_KEY_ID, STAGING_PEM), (PROD_KEY_ID, PROD_PEM)]
}
}
#[async_trait]
impl AttestationKeyLookup for PinnedKeyMap {
async fn lookup_pem(&self, key_id: &str) -> Result<String, CleanLibraryError> {
if let Some(pem) = self.extra.get(key_id) {
return Ok(pem.clone());
}
for (id, pem) in Self::builtin() {
if id == key_id {
return Ok(pem.to_string());
}
}
Err(CleanLibraryError::AttestationInvalid {
reason_code: "ATTESTATION_KEY_ID_UNKNOWN".to_string(),
message: format!(
"key_id {key_id:?} is not in this SDK's pinned key set (staging + \
prod, plus any caller-supplied `with_extra_key` pins). This is a \
PERMANENT fail-closed result, not a transient fetch failure — this \
type never fetches. If you believe this key_id is legitimate (e.g. \
a new key version), verify its fingerprint out of band and either \
pin it via `PinnedKeyMap::with_extra_key` or wait for an SDK release \
that pins it by default. `PubkeysEndpointLookup::describe_unknown_key` \
can surface a human-readable hint, but must never be auto-trusted."
),
})
}
}
#[derive(Debug, Clone, serde::Deserialize)]
struct PubkeyEntry {
key_id: String,
pubkey_pem: String,
algorithm: Option<String>,
}
#[derive(Debug, Clone, serde::Deserialize)]
struct PubkeysResponse {
keys: Vec<PubkeyEntry>,
}
#[derive(Debug, Clone)]
pub struct PubkeysEndpointLookup {
http: ReqwestClient,
pubkeys_url: Url,
ttl: Duration,
cache: Arc<RwLock<PubkeyCache>>,
}
type PubkeyCache = HashMap<String, (String, Option<String>, Instant)>;
impl PubkeysEndpointLookup {
pub fn new(base_url: &Url) -> Result<Self, CleanLibraryError> {
let pubkeys_url = base_url
.join("/v1/pubkeys")
.map_err(|e| TransportError::InvalidUrl(format!("/v1/pubkeys: {e}")))?;
let http = ReqwestClient::builder()
.timeout(Duration::from_secs(30))
.user_agent(concat!("cleanlib-client/", env!("CARGO_PKG_VERSION")))
.build()
.map_err(TransportError::Network)?;
Ok(Self {
http,
pubkeys_url,
ttl: DEFAULT_PUBKEY_CACHE_TTL,
cache: Arc::new(RwLock::new(HashMap::new())),
})
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.ttl = ttl;
self
}
async fn fetch_and_cache_all(&self) -> Result<(), CleanLibraryError> {
let response = self
.http
.get(self.pubkeys_url.clone())
.send()
.await
.map_err(TransportError::Network)?;
let status = response.status();
if !status.is_success() {
return Err(TransportError::Network(
response.error_for_status().unwrap_err(),
)
.into());
}
let text = response.text().await.map_err(TransportError::Network)?;
let body: PubkeysResponse = serde_json::from_str(&text)
.map_err(|e| CleanLibraryError::Parse(format!("/v1/pubkeys response: {e}")))?;
let now = Instant::now();
let mut cache = self.cache.write().await;
for entry in body.keys {
cache.insert(entry.key_id, (entry.pubkey_pem, entry.algorithm, now));
}
Ok(())
}
pub async fn describe_unknown_key(&self, key_id: &str) -> Option<String> {
if self.fetch_and_cache_all().await.is_err() {
return None;
}
let cache = self.cache.read().await;
let (_, algorithm, _) = cache.get(key_id)?;
Some(format!(
"key_id {key_id:?} IS present in this App's /v1/pubkeys (algorithm: {}). \
This is advisory only — NOT auto-trusted. If you want to rely on it, \
independently verify its fingerprint out of band (never solely against \
this same service), then pin it via `PinnedKeyMap::with_extra_key`.",
algorithm.as_deref().unwrap_or("unknown")
))
}
}
#[async_trait]
impl AttestationKeyLookup for PubkeysEndpointLookup {
async fn lookup_pem(&self, key_id: &str) -> Result<String, CleanLibraryError> {
{
let cache = self.cache.read().await;
if let Some((pem, _, fetched_at)) = cache.get(key_id) {
if fetched_at.elapsed() < self.ttl {
return Ok(pem.clone());
}
}
}
self.fetch_and_cache_all().await?;
let cache = self.cache.read().await;
cache
.get(key_id)
.map(|(pem, _, _)| pem.clone())
.ok_or_else(|| CleanLibraryError::AttestationInvalid {
reason_code: "ATTESTATION_KEY_ID_UNKNOWN".to_string(),
message: format!(
"key_id {key_id:?} not present in /v1/pubkeys — cannot verify \
(this is a permanent mismatch for this catalog snapshot, not a \
transient fetch failure; a genuinely new key version requires a \
BD-coordinated rotation event per [EphKeyDesync]). NOTE: this \
impl is convenience-only post-redesign (see type doc) — \
`verify_attestation`'s default path uses `PinnedKeyMap`, not this."
),
})
}
}
pub async fn verify_attestation(
attestation_envelope: &serde_json::Value,
key_lookup: &dyn AttestationKeyLookup,
) -> Result<(), CleanLibraryError> {
let malformed = |field: &str| CleanLibraryError::AttestationInvalid {
reason_code: "ATTESTATION_ENVELOPE_MALFORMED".to_string(),
message: format!("attestation envelope missing or wrong-typed field: {field}"),
};
let predicate = attestation_envelope
.get("attestation")
.ok_or_else(|| malformed("attestation"))?;
let signature_b64 = attestation_envelope
.get("signature_b64")
.and_then(|v| v.as_str())
.ok_or_else(|| malformed("signature_b64"))?;
let key_id = attestation_envelope
.get("key_id")
.and_then(|v| v.as_str())
.ok_or_else(|| malformed("key_id"))?;
let canonical = serde_jcs::to_vec(predicate).map_err(|e| CleanLibraryError::AttestationInvalid {
reason_code: "ATTESTATION_CANONICALIZATION_FAILED".to_string(),
message: format!("JCS canonicalization of attestation predicate failed: {e}"),
})?;
let sig_bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, signature_b64)
.map_err(|e| CleanLibraryError::AttestationInvalid {
reason_code: "ATTESTATION_SIGNATURE_ENCODING_INVALID".to_string(),
message: format!("signature_b64 is not valid base64: {e}"),
})?;
let signature = Signature::from_der(&sig_bytes).map_err(|e| CleanLibraryError::AttestationInvalid {
reason_code: "ATTESTATION_SIGNATURE_ENCODING_INVALID".to_string(),
message: format!("signature is not valid DER-encoded ECDSA: {e}"),
})?;
let pem = key_lookup.lookup_pem(key_id).await?;
let verifying_key = VerifyingKey::from_public_key_pem(&pem).map_err(|e| {
CleanLibraryError::AttestationInvalid {
reason_code: "ATTESTATION_KEY_ENCODING_INVALID".to_string(),
message: format!("pubkey PEM for key_id {key_id:?} is not a valid P-256 SPKI: {e}"),
}
})?;
verifying_key
.verify(&canonical, &signature)
.map_err(|e| CleanLibraryError::AttestationInvalid {
reason_code: "ATTESTATION_SIGNATURE_MISMATCH".to_string(),
message: format!(
"signature does not verify against key_id {key_id:?}'s canonical predicate: {e}"
),
})
}
#[cfg(test)]
mod tests {
use super::*;
use p256::ecdsa::signature::Signer as _;
use p256::ecdsa::SigningKey;
use p256::pkcs8::EncodePublicKey;
use rand_core_compat::OsRng;
use std::sync::Mutex;
mod rand_core_compat {
pub use p256::ecdsa::signature::rand_core::OsRng;
}
struct StaticKeyLookup {
keys: HashMap<String, String>,
lookups: Mutex<u32>,
}
#[async_trait]
impl AttestationKeyLookup for StaticKeyLookup {
async fn lookup_pem(&self, key_id: &str) -> Result<String, CleanLibraryError> {
*self.lookups.lock().unwrap() += 1;
self.keys.get(key_id).cloned().ok_or_else(|| {
CleanLibraryError::AttestationInvalid {
reason_code: "ATTESTATION_KEY_ID_UNKNOWN".to_string(),
message: format!("test lookup has no key_id {key_id:?}"),
}
})
}
}
fn sample_predicate() -> serde_json::Value {
serde_json::json!({
"artifact_hash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
"verdict_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"verdict_source": "allowed_no_findings",
"verdict_evaluated_at": "1970-01-01T00:00:00Z",
"policy_decision": "allow",
"policy_rule_id_matched": "rule_acme_001",
"risk_acceptance_status": "none",
"organization_id": "org_test_acme",
"app_version": "0.1.0",
"served_at": "1970-01-01T00:00:00Z",
})
}
fn sign_predicate(predicate: &serde_json::Value, signing_key: &SigningKey) -> (String, String) {
let canonical = serde_jcs::to_vec(predicate).unwrap();
let signature: Signature = signing_key.sign(&canonical);
let sig_b64 =
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, signature.to_der());
let pem = VerifyingKey::from(signing_key)
.to_public_key_pem(p256::pkcs8::LineEnding::LF)
.unwrap();
(sig_b64, pem)
}
#[tokio::test]
async fn positive_real_shaped_attestation_verifies() {
let signing_key = SigningKey::random(&mut OsRng);
let predicate = sample_predicate();
let (sig_b64, pem) = sign_predicate(&predicate, &signing_key);
let key_id = "projects/cleanlibrary-prod/locations/us-central1/keyRings/\
cleanlibrary-signing/cryptoKeys/cleanlib-cosign-staging/cryptoKeyVersions/1";
let envelope = serde_json::json!({
"attestation": predicate,
"signature_b64": sig_b64,
"key_id": key_id,
});
let lookup = StaticKeyLookup {
keys: HashMap::from([(key_id.to_string(), pem)]),
lookups: Mutex::new(0),
};
verify_attestation(&envelope, &lookup)
.await
.expect("genuine signature over the exact predicate must verify");
assert_eq!(*lookup.lookups.lock().unwrap(), 1);
}
#[tokio::test]
async fn negative_wrong_key_fails_loud_not_silent() {
let signing_key = SigningKey::random(&mut OsRng);
let wrong_key = SigningKey::random(&mut OsRng);
let predicate = sample_predicate();
let (sig_b64, _real_pem) = sign_predicate(&predicate, &signing_key);
let wrong_pem = VerifyingKey::from(&wrong_key)
.to_public_key_pem(p256::pkcs8::LineEnding::LF)
.unwrap();
let key_id = "local-ecdsa-p256-test";
let envelope = serde_json::json!({
"attestation": predicate,
"signature_b64": sig_b64,
"key_id": key_id,
});
let lookup = StaticKeyLookup {
keys: HashMap::from([(key_id.to_string(), wrong_pem)]),
lookups: Mutex::new(0),
};
let err = verify_attestation(&envelope, &lookup)
.await
.expect_err("signature signed by a different key must NOT verify");
match err {
CleanLibraryError::AttestationInvalid { reason_code, .. } => {
assert_eq!(reason_code, "ATTESTATION_SIGNATURE_MISMATCH");
}
other => panic!("expected AttestationInvalid, got {other:?}"),
}
}
#[tokio::test]
async fn tampered_predicate_after_signing_fails_verification() {
let signing_key = SigningKey::random(&mut OsRng);
let predicate = sample_predicate();
let (sig_b64, pem) = sign_predicate(&predicate, &signing_key);
let key_id = "local-ecdsa-p256-test";
let mut tampered = predicate.clone();
tampered["policy_decision"] = serde_json::json!("deny");
let envelope = serde_json::json!({
"attestation": tampered,
"signature_b64": sig_b64,
"key_id": key_id,
});
let lookup = StaticKeyLookup {
keys: HashMap::from([(key_id.to_string(), pem)]),
lookups: Mutex::new(0),
};
let err = verify_attestation(&envelope, &lookup).await.unwrap_err();
assert!(matches!(
err,
CleanLibraryError::AttestationInvalid { reason_code, .. }
if reason_code == "ATTESTATION_SIGNATURE_MISMATCH"
));
}
#[tokio::test]
async fn unknown_key_id_is_attestation_invalid_not_transport() {
let signing_key = SigningKey::random(&mut OsRng);
let predicate = sample_predicate();
let (sig_b64, _pem) = sign_predicate(&predicate, &signing_key);
let envelope = serde_json::json!({
"attestation": predicate,
"signature_b64": sig_b64,
"key_id": "some-key-id-not-in-the-catalog",
});
let lookup = StaticKeyLookup { keys: HashMap::new(), lookups: Mutex::new(0) };
let err = verify_attestation(&envelope, &lookup).await.unwrap_err();
assert!(!err.is_retryable());
assert!(matches!(err, CleanLibraryError::AttestationInvalid { .. }));
}
#[tokio::test]
async fn malformed_envelope_missing_signature_field_is_rejected() {
let envelope = serde_json::json!({
"attestation": sample_predicate(),
"key_id": "x",
});
let lookup = StaticKeyLookup { keys: HashMap::new(), lookups: Mutex::new(0) };
let err = verify_attestation(&envelope, &lookup).await.unwrap_err();
match err {
CleanLibraryError::AttestationInvalid { reason_code, .. } => {
assert_eq!(reason_code, "ATTESTATION_ENVELOPE_MALFORMED");
}
other => panic!("expected AttestationInvalid, got {other:?}"),
}
}
#[tokio::test]
async fn key_id_lookup_is_cached_across_repeated_verifies() {
let signing_key = SigningKey::random(&mut OsRng);
let predicate = sample_predicate();
let (sig_b64, pem) = sign_predicate(&predicate, &signing_key);
let key_id = "local-ecdsa-p256-test";
let envelope = serde_json::json!({
"attestation": predicate,
"signature_b64": sig_b64,
"key_id": key_id,
});
let lookup = StaticKeyLookup {
keys: HashMap::from([(key_id.to_string(), pem)]),
lookups: Mutex::new(0),
};
verify_attestation(&envelope, &lookup).await.unwrap();
verify_attestation(&envelope, &lookup).await.unwrap();
assert_eq!(*lookup.lookups.lock().unwrap(), 2);
}
#[tokio::test]
#[ignore = "requires a live cleanlib-app endpoint; run with `cargo test -- --ignored`"]
async fn pubkeys_endpoint_lookup_caches_within_ttl() {
let base = Url::parse("https://cleanapp.clnstrt.dev").unwrap();
let lookup = PubkeysEndpointLookup::new(&base).unwrap();
let staging_key_id = "projects/cleanlibrary-prod/locations/us-central1/keyRings/\
cleanlibrary-signing/cryptoKeys/cleanlib-cosign-staging/cryptoKeyVersions/1";
let pem1 = lookup.lookup_pem(staging_key_id).await.unwrap();
let pem2 = lookup.lookup_pem(staging_key_id).await.unwrap();
assert_eq!(pem1, pem2);
assert!(pem1.contains("BEGIN PUBLIC KEY"));
}
#[test]
fn pinned_key_map_builtin_entries_are_well_formed() {
for (key_id, pem) in PinnedKeyMap::builtin() {
assert!(key_id.contains("cryptoKeyVersions"), "key_id shape: {key_id}");
VerifyingKey::from_public_key_pem(pem)
.unwrap_or_else(|e| panic!("built-in PEM for {key_id:?} does not parse: {e}"));
}
}
#[tokio::test]
async fn pinned_key_map_verifies_a_pinned_extra_key() {
let signing_key = SigningKey::random(&mut OsRng);
let pem = VerifyingKey::from(&signing_key)
.to_public_key_pem(p256::pkcs8::LineEnding::LF)
.unwrap();
let predicate = sample_predicate();
let (sig_b64, _) = sign_predicate(&predicate, &signing_key);
let key_id = "test-pinned-extra-key";
let envelope = serde_json::json!({
"attestation": predicate,
"signature_b64": sig_b64,
"key_id": key_id,
});
let map = PinnedKeyMap::default().with_extra_key(key_id, pem);
verify_attestation(&envelope, &map)
.await
.expect("signature over a genuinely pinned key must verify");
}
#[test]
fn pinned_key_map_unknown_key_fails_closed_with_no_io_driver() {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.expect("bare runtime with no I/O/time driver");
let map = PinnedKeyMap::default();
let result = rt.block_on(map.lookup_pem("key-id-not-in-any-pinned-set"));
let err = result.expect_err("unrecognized key_id must fail, not silently succeed");
assert!(!err.is_retryable(), "unknown key_id is permanent, not transient");
assert!(matches!(
err,
CleanLibraryError::AttestationInvalid { reason_code, .. }
if reason_code == "ATTESTATION_KEY_ID_UNKNOWN"
));
}
#[test]
fn pinned_key_map_known_key_resolves_with_no_io_driver() {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.expect("bare runtime with no I/O/time driver");
let map = PinnedKeyMap::default();
let (known_key_id, _) = PinnedKeyMap::builtin()[0];
let pem = rt
.block_on(map.lookup_pem(known_key_id))
.expect("built-in key_id must resolve without any network driver");
assert!(pem.contains("BEGIN PUBLIC KEY"));
}
#[tokio::test]
async fn end_to_end_verify_fails_closed_for_unpinned_key_id() {
let signing_key = SigningKey::random(&mut OsRng);
let predicate = sample_predicate();
let (sig_b64, _pem_unused) = sign_predicate(&predicate, &signing_key);
let envelope = serde_json::json!({
"attestation": predicate,
"signature_b64": sig_b64,
"key_id": "totally-unpinned-key-id",
});
let map = PinnedKeyMap::default();
let err = verify_attestation(&envelope, &map).await.unwrap_err();
assert!(!err.is_retryable());
assert!(matches!(
err,
CleanLibraryError::AttestationInvalid { reason_code, .. }
if reason_code == "ATTESTATION_KEY_ID_UNKNOWN"
));
}
#[tokio::test]
async fn describe_unknown_key_returns_none_not_a_trust_signal_when_unreachable() {
let base = Url::parse("https://127.0.0.1.invalid.example").unwrap();
let lookup = PubkeysEndpointLookup::new(&base).unwrap();
let advisory = lookup.describe_unknown_key("any-key-id").await;
assert!(
advisory.is_none(),
"unreachable server must yield None (no advisory), never fabricate one"
);
}
}