use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use sha2::{Digest, Sha256};
use crate::canonicalize::{
compute_canonical_digest, parse_yaml_strict, to_canonical_jcs_bytes, CanonicalizeError,
};
use crate::error::{RegistryError, RegistryResult};
use crate::trust::TrustStore;
use crate::types::{DsseEnvelope, FetchResult};
pub const PAYLOAD_TYPE_PACK_V1: &str = "application/vnd.assay.pack+yaml;v=1";
#[derive(Debug, Clone)]
pub struct VerifyResult {
pub signed: bool,
pub key_id: Option<String>,
pub digest: String,
}
#[derive(Debug, Clone, Default)]
pub struct VerifyOptions {
pub allow_unsigned: bool,
pub skip_signature: bool,
}
impl VerifyOptions {
pub fn allow_unsigned(mut self) -> Self {
self.allow_unsigned = true;
self
}
pub fn skip_signature(mut self) -> Self {
self.skip_signature = true;
self
}
}
pub fn verify_pack(
result: &FetchResult,
trust_store: &TrustStore,
options: &VerifyOptions,
) -> RegistryResult<VerifyResult> {
if let Some(claimed_digest) = &result.headers.digest {
if claimed_digest != &result.computed_digest {
return Err(RegistryError::DigestMismatch {
name: "pack".to_string(),
version: "unknown".to_string(),
expected: claimed_digest.clone(),
actual: result.computed_digest.clone(),
});
}
}
let signature = &result.headers.signature;
if signature.is_none() {
if options.allow_unsigned {
return Ok(VerifyResult {
signed: false,
key_id: None,
digest: result.computed_digest.clone(),
});
} else {
return Err(RegistryError::Unsigned {
name: "pack".to_string(),
version: "unknown".to_string(),
});
}
}
if options.skip_signature {
return Ok(VerifyResult {
signed: true,
key_id: result.headers.key_id.clone(),
digest: result.computed_digest.clone(),
});
}
let canonical_bytes = canonicalize_for_dsse(&result.content)?;
let sig_b64 = signature.as_ref().unwrap();
let envelope = parse_dsse_envelope(sig_b64)?;
verify_dsse_signature_bytes(&canonical_bytes, &envelope, trust_store)?;
Ok(VerifyResult {
signed: true,
key_id: envelope.signatures.first().map(|s| s.key_id.clone()),
digest: result.computed_digest.clone(),
})
}
fn canonicalize_for_dsse(content: &str) -> RegistryResult<Vec<u8>> {
let json_value = parse_yaml_strict(content).map_err(|e| RegistryError::InvalidResponse {
message: format!("failed to parse YAML for signature verification: {}", e),
})?;
to_canonical_jcs_bytes(&json_value).map_err(|e| RegistryError::InvalidResponse {
message: format!("failed to canonicalize for signature verification: {}", e),
})
}
pub fn verify_digest(content: &str, expected: &str) -> RegistryResult<()> {
let computed = compute_digest(content);
if computed != expected {
return Err(RegistryError::DigestMismatch {
name: "pack".to_string(),
version: "unknown".to_string(),
expected: expected.to_string(),
actual: computed,
});
}
Ok(())
}
pub fn compute_digest(content: &str) -> String {
match compute_canonical_digest(content) {
Ok(digest) => digest,
Err(e) => {
tracing::warn!(
error = %e,
"canonical digest failed, falling back to raw digest"
);
let hash = Sha256::digest(content.as_bytes());
format!("sha256:{:x}", hash)
}
}
}
pub fn compute_digest_strict(content: &str) -> Result<String, CanonicalizeError> {
compute_canonical_digest(content)
}
#[deprecated(since = "2.11.0", note = "use compute_digest for canonical JCS digest")]
#[allow(dead_code)]
pub fn compute_digest_raw(content: &str) -> String {
let hash = Sha256::digest(content.as_bytes());
format!("sha256:{:x}", hash)
}
fn parse_dsse_envelope(b64: &str) -> RegistryResult<DsseEnvelope> {
let bytes = BASE64
.decode(b64)
.map_err(|e| RegistryError::SignatureInvalid {
reason: format!("invalid base64 envelope: {}", e),
})?;
serde_json::from_slice(&bytes).map_err(|e| RegistryError::SignatureInvalid {
reason: format!("invalid DSSE envelope: {}", e),
})
}
fn build_pae(payload_type: &str, payload: &[u8]) -> Vec<u8> {
let type_len = payload_type.len().to_string();
let payload_len = payload.len().to_string();
let mut pae = Vec::new();
pae.extend_from_slice(b"DSSEv1 ");
pae.extend_from_slice(type_len.as_bytes());
pae.push(b' ');
pae.extend_from_slice(payload_type.as_bytes());
pae.push(b' ');
pae.extend_from_slice(payload_len.as_bytes());
pae.push(b' ');
pae.extend_from_slice(payload);
pae
}
fn verify_dsse_signature_bytes(
canonical_bytes: &[u8],
envelope: &DsseEnvelope,
trust_store: &TrustStore,
) -> RegistryResult<()> {
if envelope.payload_type != PAYLOAD_TYPE_PACK_V1 {
return Err(RegistryError::SignatureInvalid {
reason: format!(
"payload type mismatch: expected {}, got {}",
PAYLOAD_TYPE_PACK_V1, envelope.payload_type
),
});
}
let payload_bytes =
BASE64
.decode(&envelope.payload)
.map_err(|e| RegistryError::SignatureInvalid {
reason: format!("invalid base64 payload: {}", e),
})?;
if payload_bytes != canonical_bytes {
return Err(RegistryError::DigestMismatch {
name: "pack".to_string(),
version: "unknown".to_string(),
expected: format!("canonical payload ({} bytes)", payload_bytes.len()),
actual: format!("canonical content ({} bytes)", canonical_bytes.len()),
});
}
if envelope.signatures.is_empty() {
return Err(RegistryError::SignatureInvalid {
reason: "no signatures in envelope".to_string(),
});
}
let pae = build_pae(&envelope.payload_type, &payload_bytes);
let mut last_error = None;
for sig in &envelope.signatures {
match verify_single_signature(&pae, &sig.key_id, &sig.signature, trust_store) {
Ok(()) => return Ok(()),
Err(e) => last_error = Some(e),
}
}
Err(
last_error.unwrap_or_else(|| RegistryError::SignatureInvalid {
reason: "no valid signatures".to_string(),
}),
)
}
#[allow(dead_code)]
fn verify_dsse_signature(
content: &str,
envelope: &DsseEnvelope,
trust_store: &TrustStore,
) -> RegistryResult<()> {
let canonical_bytes = canonicalize_for_dsse(content)?;
verify_dsse_signature_bytes(&canonical_bytes, envelope, trust_store)
}
fn verify_single_signature(
pae: &[u8],
key_id: &str,
signature_b64: &str,
trust_store: &TrustStore,
) -> RegistryResult<()> {
let key = trust_store.get_key(key_id)?;
let signature_bytes =
BASE64
.decode(signature_b64)
.map_err(|e| RegistryError::SignatureInvalid {
reason: format!("invalid base64 signature: {}", e),
})?;
let signature =
Signature::from_slice(&signature_bytes).map_err(|e| RegistryError::SignatureInvalid {
reason: format!("invalid signature bytes: {}", e),
})?;
key.verify(pae, &signature)
.map_err(|_| RegistryError::SignatureInvalid {
reason: "ed25519 verification failed".to_string(),
})
}
pub fn compute_key_id(spki_bytes: &[u8]) -> String {
let hash = Sha256::digest(spki_bytes);
format!("sha256:{:x}", hash)
}
pub fn compute_key_id_from_key(key: &VerifyingKey) -> RegistryResult<String> {
use pkcs8::EncodePublicKey;
let doc = key.to_public_key_der().map_err(|e| RegistryError::Config {
message: format!("failed to encode public key: {}", e),
})?;
Ok(compute_key_id(doc.as_bytes()))
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
fn generate_keypair() -> SigningKey {
SigningKey::generate(&mut rand::thread_rng())
}
#[test]
fn test_compute_digest_canonical() {
let content = "name: test\nversion: \"1.0.0\"";
let digest = compute_digest(content);
assert!(digest.starts_with("sha256:"));
assert_eq!(digest.len(), 7 + 64);
let strict = compute_digest_strict(content).unwrap();
assert_eq!(digest, strict);
}
#[test]
fn test_compute_digest_golden_vector() {
let content = "name: eu-ai-act-baseline\nversion: \"1.0.0\"\nkind: compliance";
let digest = compute_digest(content);
assert_eq!(
digest,
"sha256:f47d932cdad4bde369ed0a7cf26fdcf4077777296346c4102d9017edbc62a070"
);
}
#[test]
fn test_compute_digest_key_ordering() {
let yaml1 = "z: 1\na: 2";
let yaml2 = "a: 2\nz: 1";
let digest1 = compute_digest(yaml1);
let digest2 = compute_digest(yaml2);
assert_eq!(digest1, digest2);
}
#[test]
#[allow(deprecated)]
fn test_compute_digest_raw_differs() {
let content = "name: eu-ai-act-baseline\nversion: \"1.0.0\"\nkind: compliance";
let canonical = compute_digest(content);
let raw = compute_digest_raw(content);
assert_ne!(canonical, raw);
assert_eq!(
raw,
"sha256:5a9a6b1e95e8c1d36779b87212835c9bfa9cae5d98cb9c75fb8c478750e5e200"
);
}
#[test]
fn test_verify_digest_success() {
let content = "name: test\nversion: \"1.0.0\"";
let expected = compute_digest(content);
assert!(verify_digest(content, &expected).is_ok());
}
#[test]
fn test_verify_digest_mismatch() {
let content = "name: test\nversion: \"1.0.0\"";
let wrong = "sha256:0000000000000000000000000000000000000000000000000000000000000000";
let result = verify_digest(content, wrong);
assert!(matches!(result, Err(RegistryError::DigestMismatch { .. })));
}
#[test]
fn test_build_pae() {
let pae = build_pae("application/json", b"test");
let expected = b"DSSEv1 16 application/json 4 test";
assert_eq!(pae, expected);
}
#[test]
fn test_payload_type_length() {
assert_eq!(
PAYLOAD_TYPE_PACK_V1.len(),
35,
"PAYLOAD_TYPE_PACK_V1 must be 35 bytes"
);
assert!(PAYLOAD_TYPE_PACK_V1.is_ascii());
let pae = build_pae(PAYLOAD_TYPE_PACK_V1, b"{}");
let pae_str = String::from_utf8_lossy(&pae);
assert!(
pae_str.starts_with("DSSEv1 35 application/vnd.assay.pack+yaml;v=1 2 {}"),
"PAE must start with 'DSSEv1 35 ...' for pack signing"
);
}
#[test]
fn test_key_id_computation() {
let key = generate_keypair();
let key_id = compute_key_id_from_key(&key.verifying_key()).unwrap();
assert!(key_id.starts_with("sha256:"));
assert_eq!(key_id.len(), 7 + 64);
let hex_part = &key_id[7..];
assert!(
hex_part
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
"key_id hex must be lowercase"
);
}
#[test]
fn test_parse_dsse_envelope_invalid_base64() {
let result = parse_dsse_envelope("not valid base64!!!");
assert!(matches!(
result,
Err(RegistryError::SignatureInvalid { .. })
));
}
#[test]
fn test_parse_dsse_envelope_invalid_json() {
let b64 = BASE64.encode(b"not json");
let result = parse_dsse_envelope(&b64);
assert!(matches!(
result,
Err(RegistryError::SignatureInvalid { .. })
));
}
#[test]
fn test_parse_dsse_envelope_valid() {
let envelope = DsseEnvelope {
payload_type: PAYLOAD_TYPE_PACK_V1.to_string(),
payload: BASE64.encode(b"test payload"),
signatures: vec![],
};
let json = serde_json::to_vec(&envelope).unwrap();
let b64 = BASE64.encode(&json);
let parsed = parse_dsse_envelope(&b64).unwrap();
assert_eq!(parsed.payload_type, PAYLOAD_TYPE_PACK_V1);
}
#[test]
fn test_dsse_envelope_size_small_pack() {
let content = "name: small-pack\nversion: \"1.0.0\"\nrules: []";
let canonical = crate::canonicalize::to_canonical_jcs_bytes(
&crate::canonicalize::parse_yaml_strict(content).unwrap(),
)
.unwrap();
let envelope = DsseEnvelope {
payload_type: PAYLOAD_TYPE_PACK_V1.to_string(),
payload: BASE64.encode(&canonical),
signatures: vec![crate::types::DsseSignature {
key_id: "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd"
.to_string(),
signature: BASE64.encode([0u8; 64]), }],
};
let json = serde_json::to_vec(&envelope).unwrap();
let header_value = BASE64.encode(&json);
assert!(
header_value.len() < 1024,
"Small pack DSSE envelope should be < 1KB, got {} bytes",
header_value.len()
);
}
#[test]
fn test_dsse_envelope_size_medium_pack() {
let mut content = String::from("name: medium-pack\nversion: \"1.0.0\"\nrules:\n");
for i in 0..100 {
content.push_str(&format!(
" - name: rule_{}\n pattern: \"test_pattern_{}\"\n",
i, i
));
}
let canonical = crate::canonicalize::to_canonical_jcs_bytes(
&crate::canonicalize::parse_yaml_strict(&content).unwrap(),
)
.unwrap();
let envelope = DsseEnvelope {
payload_type: PAYLOAD_TYPE_PACK_V1.to_string(),
payload: BASE64.encode(&canonical),
signatures: vec![crate::types::DsseSignature {
key_id: "sha256:abc123def456abc123def456abc123def456abc123def456abc123def456abcd"
.to_string(),
signature: BASE64.encode([0u8; 64]),
}],
};
let json = serde_json::to_vec(&envelope).unwrap();
let header_value = BASE64.encode(&json);
println!(
"Medium pack: canonical={} bytes, envelope={} bytes, header={} bytes",
canonical.len(),
json.len(),
header_value.len()
);
if header_value.len() > 8192 {
println!("WARNING: Pack exceeds 8KB header limit - use sidecar endpoint");
}
}
#[test]
fn test_header_size_limit_constant() {
const RECOMMENDED_HEADER_LIMIT: usize = 8192;
assert_eq!(RECOMMENDED_HEADER_LIMIT, 8192);
}
fn keypair_from_seed(seed: [u8; 32]) -> SigningKey {
SigningKey::from_bytes(&seed)
}
fn create_signed_envelope(signing_key: &SigningKey, content: &str) -> (DsseEnvelope, String) {
use ed25519_dalek::Signer;
use pkcs8::EncodePublicKey;
let canonical = crate::canonicalize::to_canonical_jcs_bytes(
&crate::canonicalize::parse_yaml_strict(content).unwrap(),
)
.unwrap();
let verifying_key = signing_key.verifying_key();
let spki_der = verifying_key.to_public_key_der().unwrap();
let key_id = compute_key_id(spki_der.as_bytes());
let payload_b64 = BASE64.encode(&canonical);
let pae = build_pae(PAYLOAD_TYPE_PACK_V1, &canonical);
let signature = signing_key.sign(&pae);
let envelope = DsseEnvelope {
payload_type: PAYLOAD_TYPE_PACK_V1.to_string(),
payload: payload_b64,
signatures: vec![crate::types::DsseSignature {
key_id: key_id.clone(),
signature: BASE64.encode(signature.to_bytes()),
}],
};
(envelope, key_id)
}
#[test]
fn test_dsse_valid_signature_real_ed25519() {
let seed: [u8; 32] = [
0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec,
0x2c, 0xc4, 0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03,
0x1c, 0xae, 0x7f, 0x60,
];
let signing_key = keypair_from_seed(seed);
let content = "name: test-pack\nversion: \"1.0.0\"\nrules: []";
let (envelope, key_id) = create_signed_envelope(&signing_key, content);
use pkcs8::EncodePublicKey;
let verifying_key = signing_key.verifying_key();
let spki_der = verifying_key.to_public_key_der().unwrap();
let trusted_key = crate::types::TrustedKey {
key_id: key_id.clone(),
algorithm: "Ed25519".to_string(),
public_key: BASE64.encode(spki_der.as_bytes()),
description: Some("Test key".to_string()),
added_at: None,
expires_at: None,
revoked: false,
};
let trust_store = TrustStore::new();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(trust_store.add_pinned_key(&trusted_key))
.unwrap();
let canonical = crate::canonicalize::to_canonical_jcs_bytes(
&crate::canonicalize::parse_yaml_strict(content).unwrap(),
)
.unwrap();
let content_str = String::from_utf8(canonical.clone()).unwrap();
let result = verify_dsse_signature(&content_str, &envelope, &trust_store);
assert!(result.is_ok(), "DSSE signature should verify: {:?}", result);
}
#[test]
fn test_dsse_payload_mismatch() {
let seed: [u8; 32] = [0x42; 32];
let signing_key = keypair_from_seed(seed);
let original_content = "name: original\nversion: \"1.0.0\"";
let (envelope, key_id) = create_signed_envelope(&signing_key, original_content);
use pkcs8::EncodePublicKey;
let verifying_key = signing_key.verifying_key();
let spki_der = verifying_key.to_public_key_der().unwrap();
let trusted_key = crate::types::TrustedKey {
key_id,
algorithm: "Ed25519".to_string(),
public_key: BASE64.encode(spki_der.as_bytes()),
description: None,
added_at: None,
expires_at: None,
revoked: false,
};
let trust_store = TrustStore::new();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(trust_store.add_pinned_key(&trusted_key))
.unwrap();
let tampered_content = "name: tampered\nversion: \"1.0.0\"";
let tampered_canonical = crate::canonicalize::to_canonical_jcs_bytes(
&crate::canonicalize::parse_yaml_strict(tampered_content).unwrap(),
)
.unwrap();
let tampered_str = String::from_utf8(tampered_canonical).unwrap();
let result = verify_dsse_signature(&tampered_str, &envelope, &trust_store);
assert!(
matches!(result, Err(RegistryError::DigestMismatch { .. })),
"Should return DigestMismatch for payload != content: {:?}",
result
);
}
#[test]
fn test_dsse_untrusted_key_rejected() {
let seed: [u8; 32] = [0x55; 32];
let signing_key = keypair_from_seed(seed);
let content = "name: commercial-pack\nversion: \"1.0.0\"";
let (envelope, _key_id) = create_signed_envelope(&signing_key, content);
let trust_store = TrustStore::new();
let canonical = crate::canonicalize::to_canonical_jcs_bytes(
&crate::canonicalize::parse_yaml_strict(content).unwrap(),
)
.unwrap();
let content_str = String::from_utf8(canonical).unwrap();
let result = verify_dsse_signature(&content_str, &envelope, &trust_store);
assert!(
matches!(result, Err(RegistryError::KeyNotTrusted { .. })),
"Should return KeyNotTrusted for unknown key: {:?}",
result
);
}
#[test]
fn test_dsse_wrong_payload_type_rejected() {
let envelope = DsseEnvelope {
payload_type: "application/json".to_string(), payload: BASE64.encode(b"test"),
signatures: vec![],
};
let trust_store = TrustStore::new();
let result = verify_dsse_signature("test", &envelope, &trust_store);
assert!(
matches!(result, Err(RegistryError::SignatureInvalid { .. })),
"Should reject wrong payload type: {:?}",
result
);
}
#[test]
fn test_dsse_empty_signatures_rejected() {
let content = "name: test\nversion: \"1.0.0\"";
let canonical = crate::canonicalize::to_canonical_jcs_bytes(
&crate::canonicalize::parse_yaml_strict(content).unwrap(),
)
.unwrap();
let envelope = DsseEnvelope {
payload_type: PAYLOAD_TYPE_PACK_V1.to_string(),
payload: BASE64.encode(&canonical),
signatures: vec![], };
let trust_store = TrustStore::new();
let content_str = String::from_utf8(canonical).unwrap();
let result = verify_dsse_signature(&content_str, &envelope, &trust_store);
assert!(
matches!(result, Err(RegistryError::SignatureInvalid { .. })),
"Should reject empty signatures: {:?}",
result
);
}
#[test]
fn test_dsse_invalid_signature_rejected() {
let seed: [u8; 32] = [0x77; 32];
let signing_key = keypair_from_seed(seed);
let content = "name: test\nversion: \"1.0.0\"";
let canonical = crate::canonicalize::to_canonical_jcs_bytes(
&crate::canonicalize::parse_yaml_strict(content).unwrap(),
)
.unwrap();
use pkcs8::EncodePublicKey;
let verifying_key = signing_key.verifying_key();
let spki_der = verifying_key.to_public_key_der().unwrap();
let key_id = compute_key_id(spki_der.as_bytes());
let envelope = DsseEnvelope {
payload_type: PAYLOAD_TYPE_PACK_V1.to_string(),
payload: BASE64.encode(&canonical),
signatures: vec![crate::types::DsseSignature {
key_id: key_id.clone(),
signature: BASE64.encode([0u8; 64]), }],
};
let trusted_key = crate::types::TrustedKey {
key_id,
algorithm: "Ed25519".to_string(),
public_key: BASE64.encode(spki_der.as_bytes()),
description: None,
added_at: None,
expires_at: None,
revoked: false,
};
let trust_store = TrustStore::new();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(trust_store.add_pinned_key(&trusted_key))
.unwrap();
let content_str = String::from_utf8(canonical).unwrap();
let result = verify_dsse_signature(&content_str, &envelope, &trust_store);
assert!(
matches!(result, Err(RegistryError::SignatureInvalid { .. })),
"Should reject invalid signature: {:?}",
result
);
}
#[test]
fn test_verify_pack_uses_canonical_bytes() {
let seed: [u8; 32] = [0x88; 32];
let signing_key = keypair_from_seed(seed);
let content = "z: 3\na: 1\nm: 2";
let (envelope, key_id) = create_signed_envelope(&signing_key, content);
use pkcs8::EncodePublicKey;
let verifying_key = signing_key.verifying_key();
let spki_der = verifying_key.to_public_key_der().unwrap();
let trusted_key = crate::types::TrustedKey {
key_id,
algorithm: "Ed25519".to_string(),
public_key: BASE64.encode(spki_der.as_bytes()),
description: None,
added_at: None,
expires_at: None,
revoked: false,
};
let trust_store = TrustStore::new();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(trust_store.add_pinned_key(&trusted_key))
.unwrap();
let fetch_result = FetchResult {
content: content.to_string(), headers: crate::types::PackHeaders {
digest: Some(compute_digest(content)),
signature: Some(BASE64.encode(serde_json::to_vec(&envelope).unwrap())),
key_id: envelope.signatures.first().map(|s| s.key_id.clone()),
etag: None,
cache_control: None,
content_length: None,
},
computed_digest: compute_digest(content),
};
let result = verify_pack(&fetch_result, &trust_store, &VerifyOptions::default());
assert!(
result.is_ok(),
"verify_pack should canonicalize content before DSSE verification: {:?}",
result
);
}
#[test]
fn test_canonical_bytes_differ_from_raw() {
let yaml = "z: 1\na: 2\nm: 3";
let raw_bytes = yaml.as_bytes();
let canonical_bytes = crate::canonicalize::to_canonical_jcs_bytes(
&crate::canonicalize::parse_yaml_strict(yaml).unwrap(),
)
.unwrap();
assert_ne!(
raw_bytes,
&canonical_bytes[..],
"Raw YAML and canonical JCS MUST differ for non-sorted keys"
);
let canonical_str = String::from_utf8(canonical_bytes).unwrap();
assert!(
canonical_str.starts_with(r#"{"a":"#),
"JCS must sort keys alphabetically, got: {}",
canonical_str
);
}
#[test]
fn test_verify_dsse_signature_bytes_directly() {
let seed: [u8; 32] = [0x99; 32];
let signing_key = keypair_from_seed(seed);
let content = "name: test\nversion: \"1.0.0\"";
let (envelope, key_id) = create_signed_envelope(&signing_key, content);
use pkcs8::EncodePublicKey;
let verifying_key = signing_key.verifying_key();
let spki_der = verifying_key.to_public_key_der().unwrap();
let trusted_key = crate::types::TrustedKey {
key_id,
algorithm: "Ed25519".to_string(),
public_key: BASE64.encode(spki_der.as_bytes()),
description: None,
added_at: None,
expires_at: None,
revoked: false,
};
let trust_store = TrustStore::new();
tokio::runtime::Runtime::new()
.unwrap()
.block_on(trust_store.add_pinned_key(&trusted_key))
.unwrap();
let canonical_bytes = crate::canonicalize::to_canonical_jcs_bytes(
&crate::canonicalize::parse_yaml_strict(content).unwrap(),
)
.unwrap();
let result = verify_dsse_signature_bytes(&canonical_bytes, &envelope, &trust_store);
assert!(
result.is_ok(),
"Canonical bytes verification should succeed: {:?}",
result
);
let raw_bytes = content.as_bytes();
let result = verify_dsse_signature_bytes(raw_bytes, &envelope, &trust_store);
assert!(
matches!(result, Err(RegistryError::DigestMismatch { .. })),
"Raw bytes should not match canonical payload: {:?}",
result
);
}
}