use crate::catalog_authority_payload::{CatalogAuthorityPayload, CatalogAuthorityPayloadKind};
use base64::{
Engine as _, alphabet,
engine::{DecodePaddingMode, GeneralPurpose, general_purpose::GeneralPurposeConfig},
};
use ed25519_dalek::{Signature, VerifyingKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::fmt;
use thiserror::Error;
pub const SIGNED_ENVELOPE_SCHEMA_VERSION: &str = "1.0.0";
pub const SIGNED_ENVELOPE_ALGORITHM: &str = "Ed25519";
pub const MAX_SIGNED_TRANSPORT_BYTES: usize = 16 * 1024;
const SIGNATURE_BYTES: usize = 64;
const SIGNATURE_B64U_BYTES: usize = 86;
const KEY_ID_PREFIX: &str = "sha256:";
const STRICT_BASE64URL: GeneralPurpose = GeneralPurpose::new(
&alphabet::URL_SAFE,
GeneralPurposeConfig::new()
.with_encode_padding(false)
.with_decode_allow_trailing_bits(false)
.with_decode_padding_mode(DecodePaddingMode::RequireNone),
);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CatalogAuthorityDomain {
ConformanceEvidence,
PromotionApply,
PromotionRollback,
TrustPolicyUpdate,
TrustPolicyBootstrap,
}
impl CatalogAuthorityDomain {
pub const fn payload_kind(self) -> CatalogAuthorityPayloadKind {
match self {
Self::ConformanceEvidence => CatalogAuthorityPayloadKind::ConformanceEvidence,
Self::PromotionApply => CatalogAuthorityPayloadKind::PromotionApply,
Self::PromotionRollback => CatalogAuthorityPayloadKind::PromotionRollback,
Self::TrustPolicyUpdate => CatalogAuthorityPayloadKind::TrustPolicyUpdate,
Self::TrustPolicyBootstrap => CatalogAuthorityPayloadKind::TrustPolicyBootstrap,
}
}
pub const fn prefix(self) -> &'static [u8] {
self.payload_kind().domain()
}
pub const fn maximum_payload_bytes(self) -> usize {
self.payload_kind().maximum_payload_bytes()
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct SignedEnvelopeTransport {
schema_version: String,
algorithm: String,
key_id: String,
payload_b64u: String,
signature_b64u: String,
}
#[must_use]
#[derive(PartialEq, Eq)]
pub struct VerifiedSignedEnvelope {
domain: CatalogAuthorityDomain,
key_id: String,
payload: Vec<u8>,
typed_payload: CatalogAuthorityPayload,
transport_sha256: String,
}
impl VerifiedSignedEnvelope {
pub fn domain(&self) -> CatalogAuthorityDomain {
self.domain
}
pub fn key_id(&self) -> &str {
&self.key_id
}
pub fn payload(&self) -> &[u8] {
&self.payload
}
pub fn typed_payload(&self) -> &CatalogAuthorityPayload {
&self.typed_payload
}
pub fn transport_sha256(&self) -> &str {
&self.transport_sha256
}
}
impl fmt::Debug for VerifiedSignedEnvelope {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("VerifiedSignedEnvelope")
.field("domain", &self.domain)
.field("key_id", &self.key_id)
.field("payload_bytes", &self.payload.len())
.field("transport_sha256", &self.transport_sha256)
.finish()
}
}
pub fn verify_signed_envelope(
transport_json: &[u8],
public_key_bytes: &[u8],
expected_domain: CatalogAuthorityDomain,
) -> Result<VerifiedSignedEnvelope, SignedEnvelopeError> {
if transport_json.is_empty() || transport_json.len() > MAX_SIGNED_TRANSPORT_BYTES {
return Err(SignedEnvelopeError::InvalidTransportSize);
}
let transport: SignedEnvelopeTransport = serde_json::from_slice(transport_json)
.map_err(|_| SignedEnvelopeError::MalformedTransport)?;
let canonical =
serde_json::to_vec(&transport).map_err(|_| SignedEnvelopeError::MalformedTransport)?;
if canonical != transport_json {
return Err(SignedEnvelopeError::NonCanonicalTransport);
}
if transport.schema_version != SIGNED_ENVELOPE_SCHEMA_VERSION {
return Err(SignedEnvelopeError::UnsupportedSchemaVersion);
}
if transport.algorithm != SIGNED_ENVELOPE_ALGORITHM {
return Err(SignedEnvelopeError::UnsupportedAlgorithm);
}
if !valid_key_id(&transport.key_id) {
return Err(SignedEnvelopeError::InvalidKeyId);
}
let public_key_bytes: &[u8; 32] = public_key_bytes
.try_into()
.map_err(|_| SignedEnvelopeError::InvalidPublicKey)?;
let verifying_key = VerifyingKey::from_bytes(public_key_bytes)
.map_err(|_| SignedEnvelopeError::InvalidPublicKey)?;
if verifying_key.is_weak() {
return Err(SignedEnvelopeError::WeakPublicKey);
}
if transport.key_id != sha256_id(public_key_bytes) {
return Err(SignedEnvelopeError::KeyIdMismatch);
}
let maximum_payload_b64u_bytes = unpadded_base64_len(expected_domain.maximum_payload_bytes());
if transport.payload_b64u.is_empty()
|| transport.payload_b64u.len() > maximum_payload_b64u_bytes
{
return Err(SignedEnvelopeError::InvalidPayloadSize);
}
let payload = strict_decode(&transport.payload_b64u)
.map_err(|_| SignedEnvelopeError::InvalidPayloadEncoding)?;
if payload.len() > expected_domain.maximum_payload_bytes()
|| payload.len() <= expected_domain.prefix().len()
{
return Err(SignedEnvelopeError::InvalidPayloadSize);
}
if !payload.starts_with(expected_domain.prefix()) {
return Err(SignedEnvelopeError::DomainMismatch);
}
let typed_payload = CatalogAuthorityPayload::decode(expected_domain.payload_kind(), &payload)
.map_err(|_| SignedEnvelopeError::InvalidTypedPayload)?;
let transport_key_id: [u8; 32] = Sha256::digest(public_key_bytes).into();
if typed_payload.signing_key_id() != &transport_key_id {
return Err(SignedEnvelopeError::PayloadKeyIdMismatch);
}
let reencoded_payload = typed_payload
.encode()
.map_err(|_| SignedEnvelopeError::InvalidTypedPayload)?;
if reencoded_payload != payload {
return Err(SignedEnvelopeError::InvalidTypedPayload);
}
if transport.signature_b64u.len() != SIGNATURE_B64U_BYTES {
return Err(SignedEnvelopeError::InvalidSignatureEncoding);
}
let signature_bytes = strict_decode(&transport.signature_b64u)
.map_err(|_| SignedEnvelopeError::InvalidSignatureEncoding)?;
if signature_bytes.len() != SIGNATURE_BYTES {
return Err(SignedEnvelopeError::InvalidSignatureEncoding);
}
let signature = Signature::from_slice(&signature_bytes)
.map_err(|_| SignedEnvelopeError::InvalidSignatureEncoding)?;
verifying_key
.verify_strict(&payload, &signature)
.map_err(|_| SignedEnvelopeError::InvalidSignature)?;
Ok(VerifiedSignedEnvelope {
domain: expected_domain,
key_id: transport.key_id,
payload,
typed_payload,
transport_sha256: lower_hex(&Sha256::digest(transport_json)),
})
}
pub fn inspect_signed_envelope_key_id(
transport_json: &[u8],
) -> Result<String, SignedEnvelopeError> {
let transport = parse_canonical_transport(transport_json)?;
Ok(transport.key_id)
}
pub fn validate_ed25519_public_key(public_key_bytes: &[u8]) -> Result<(), SignedEnvelopeError> {
let public_key_bytes: &[u8; 32] = public_key_bytes
.try_into()
.map_err(|_| SignedEnvelopeError::InvalidPublicKey)?;
let verifying_key = VerifyingKey::from_bytes(public_key_bytes)
.map_err(|_| SignedEnvelopeError::InvalidPublicKey)?;
if verifying_key.is_weak() {
return Err(SignedEnvelopeError::WeakPublicKey);
}
Ok(())
}
pub fn verify_bootstrap_self_signed_envelope(
transport_json: &[u8],
) -> Result<VerifiedSignedEnvelope, SignedEnvelopeError> {
let transport = parse_canonical_transport(transport_json)?;
let payload = strict_decode(&transport.payload_b64u)
.map_err(|_| SignedEnvelopeError::InvalidPayloadEncoding)?;
let typed = CatalogAuthorityPayload::decode(
CatalogAuthorityPayloadKind::TrustPolicyBootstrap,
&payload,
)
.map_err(|_| SignedEnvelopeError::InvalidTypedPayload)?;
let CatalogAuthorityPayload::TrustPolicyBootstrap(bootstrap) = typed else {
return Err(SignedEnvelopeError::InvalidTypedPayload);
};
verify_signed_envelope(
transport_json,
&bootstrap.bootstrap_public_key,
CatalogAuthorityDomain::TrustPolicyBootstrap,
)
}
fn parse_canonical_transport(
transport_json: &[u8],
) -> Result<SignedEnvelopeTransport, SignedEnvelopeError> {
if transport_json.is_empty() || transport_json.len() > MAX_SIGNED_TRANSPORT_BYTES {
return Err(SignedEnvelopeError::InvalidTransportSize);
}
let transport: SignedEnvelopeTransport = serde_json::from_slice(transport_json)
.map_err(|_| SignedEnvelopeError::MalformedTransport)?;
let canonical =
serde_json::to_vec(&transport).map_err(|_| SignedEnvelopeError::MalformedTransport)?;
if canonical != transport_json {
return Err(SignedEnvelopeError::NonCanonicalTransport);
}
if transport.schema_version != SIGNED_ENVELOPE_SCHEMA_VERSION {
return Err(SignedEnvelopeError::UnsupportedSchemaVersion);
}
if transport.algorithm != SIGNED_ENVELOPE_ALGORITHM {
return Err(SignedEnvelopeError::UnsupportedAlgorithm);
}
if !valid_key_id(&transport.key_id) {
return Err(SignedEnvelopeError::InvalidKeyId);
}
Ok(transport)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Error)]
pub enum SignedEnvelopeError {
#[error("signed envelope transport size is invalid")]
InvalidTransportSize,
#[error("signed envelope transport is malformed")]
MalformedTransport,
#[error("signed envelope transport is not canonical")]
NonCanonicalTransport,
#[error("signed envelope schema version is unsupported")]
UnsupportedSchemaVersion,
#[error("signed envelope algorithm is unsupported")]
UnsupportedAlgorithm,
#[error("signed envelope key id is invalid")]
InvalidKeyId,
#[error("signed envelope payload encoding is invalid")]
InvalidPayloadEncoding,
#[error("signed envelope payload size is invalid")]
InvalidPayloadSize,
#[error("signed envelope signature encoding is invalid")]
InvalidSignatureEncoding,
#[error("signed envelope public key is invalid")]
InvalidPublicKey,
#[error("signed envelope public key is weak")]
WeakPublicKey,
#[error("signed envelope key id does not match the public key")]
KeyIdMismatch,
#[error("signed envelope domain does not match the expected operation")]
DomainMismatch,
#[error("signed envelope typed payload is invalid")]
InvalidTypedPayload,
#[error("signed envelope payload key id does not match the transport public key")]
PayloadKeyIdMismatch,
#[error("signed envelope signature is invalid")]
InvalidSignature,
}
fn sha256_id(bytes: &[u8]) -> String {
format!("{KEY_ID_PREFIX}{}", lower_hex(&Sha256::digest(bytes)))
}
fn valid_key_id(value: &str) -> bool {
let Some(digest) = value.strip_prefix(KEY_ID_PREFIX) else {
return false;
};
digest.len() == 64
&& digest
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
fn unpadded_base64_len(decoded_bytes: usize) -> usize {
let complete_groups = decoded_bytes / 3;
let remainder = decoded_bytes % 3;
complete_groups * 4
+ match remainder {
0 => 0,
1 => 2,
_ => 3,
}
}
fn strict_decode(encoded: &str) -> Result<Vec<u8>, ()> {
let decoded = STRICT_BASE64URL.decode(encoded).map_err(|_| ())?;
if STRICT_BASE64URL.encode(&decoded) != encoded {
return Err(());
}
Ok(decoded)
}
fn lower_hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut encoded = String::with_capacity(bytes.len() * 2);
for byte in bytes {
encoded.push(char::from(HEX[usize::from(byte >> 4)]));
encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
encoded
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
const TEST_ONLY_SIGNING_SEED: [u8; 32] = [7; 32];
const POSITIVE_KEY_ID: &str =
"sha256:fe812c12f3ab4ce6ac5db69ac352f906cb1b11ef43fb33e252ef7ff552263889";
fn positive_typed_payload(domain: CatalogAuthorityDomain) -> Vec<u8> {
let fixture: serde_json::Value = serde_json::from_str(include_str!(
"../tests/fixtures/adr002_typed_payload_vectors.json"
))
.expect("checked vector fixture parses");
let hex = fixture["positives"]
.as_array()
.expect("positive vector array")
.iter()
.find(|vector| {
vector["kind"]
== match domain {
CatalogAuthorityDomain::ConformanceEvidence => "conformance_evidence",
CatalogAuthorityDomain::PromotionApply => "promotion_apply",
CatalogAuthorityDomain::PromotionRollback => "promotion_rollback",
CatalogAuthorityDomain::TrustPolicyUpdate => "trust_policy_update",
CatalogAuthorityDomain::TrustPolicyBootstrap => "trust_policy_bootstrap",
}
})
.and_then(|vector| vector["hex"].as_str())
.expect("promotion apply positive hex");
let mut payload: Vec<u8> = hex
.as_bytes()
.chunks_exact(2)
.map(|pair| {
let text = std::str::from_utf8(pair).expect("ASCII fixture hex");
u8::from_str_radix(text, 16).expect("lowercase fixture hex")
})
.collect();
let signing_key = SigningKey::from_bytes(&TEST_ONLY_SIGNING_SEED);
let signer_start = domain.prefix().len();
let signer_end = signer_start + 32;
payload[signer_start..signer_end]
.copy_from_slice(&Sha256::digest(signing_key.verifying_key().as_bytes()));
if domain == CatalogAuthorityDomain::TrustPolicyBootstrap {
payload[signer_end..signer_end + 32]
.copy_from_slice(signing_key.verifying_key().as_bytes());
}
payload
}
fn replace_ascii_field(payload: &mut Vec<u8>, old: &str, replacement: &str) {
let mut encoded_old = u16::try_from(old.len())
.expect("test string length")
.to_be_bytes()
.to_vec();
encoded_old.extend_from_slice(old.as_bytes());
let offset = payload
.windows(encoded_old.len())
.position(|window| window == encoded_old)
.expect("test field exists");
let mut encoded_new = u16::try_from(replacement.len())
.expect("replacement length")
.to_be_bytes()
.to_vec();
encoded_new.extend_from_slice(replacement.as_bytes());
payload.splice(offset..offset + encoded_old.len(), encoded_new);
}
fn domain_payload(domain: CatalogAuthorityDomain, body: &[u8]) -> Vec<u8> {
let mut payload = domain.prefix().to_vec();
payload.extend_from_slice(body);
payload
}
fn signed_transport(signing_key: &SigningKey, payload: &[u8]) -> SignedEnvelopeTransport {
let signature = signing_key.sign(payload);
SignedEnvelopeTransport {
schema_version: SIGNED_ENVELOPE_SCHEMA_VERSION.to_owned(),
algorithm: SIGNED_ENVELOPE_ALGORITHM.to_owned(),
key_id: sha256_id(signing_key.verifying_key().as_bytes()),
payload_b64u: STRICT_BASE64URL.encode(payload),
signature_b64u: STRICT_BASE64URL.encode(signature.to_bytes()),
}
}
fn canonical_json(transport: &SignedEnvelopeTransport) -> Vec<u8> {
serde_json::to_vec(transport).expect("test transport serializes")
}
#[test]
fn deterministic_positive_vector_verifies_without_apply_authority() {
let signing_key = SigningKey::from_bytes(&TEST_ONLY_SIGNING_SEED);
let expected_domain = CatalogAuthorityDomain::PromotionApply;
let payload = positive_typed_payload(expected_domain);
let transport = signed_transport(&signing_key, &payload);
let canonical = canonical_json(&transport);
assert_eq!(transport.key_id, POSITIVE_KEY_ID);
assert_eq!(
canonical,
canonical_json(&signed_transport(&signing_key, &payload))
);
let verified = verify_signed_envelope(
&canonical,
signing_key.verifying_key().as_bytes(),
expected_domain,
)
.expect("fixed signed checkpoint verifies");
assert_eq!(verified.domain(), expected_domain);
assert_eq!(verified.key_id(), transport.key_id);
assert_eq!(verified.payload(), payload);
assert!(matches!(
verified.typed_payload(),
CatalogAuthorityPayload::PromotionApply(_)
));
assert_eq!(verified.transport_sha256().len(), 64);
assert!(!canonical.contains(&b' '));
assert_eq!(canonical, canonical_json(&transport));
}
#[test]
fn domain_constants_and_payload_bounds_match_r1() {
let cases = [
(
CatalogAuthorityDomain::ConformanceEvidence,
b"steadymux.catalog-authority/conformance-evidence/v1\0".as_slice(),
4 * 1024,
),
(
CatalogAuthorityDomain::PromotionApply,
b"steadymux.catalog-authority/promotion-apply/v1\0".as_slice(),
4 * 1024,
),
(
CatalogAuthorityDomain::PromotionRollback,
b"steadymux.catalog-authority/promotion-rollback/v1\0".as_slice(),
2 * 1024,
),
(
CatalogAuthorityDomain::TrustPolicyUpdate,
b"steadymux.catalog-authority/trust-policy-update/v1\0".as_slice(),
8 * 1024,
),
(
CatalogAuthorityDomain::TrustPolicyBootstrap,
b"steadymux.catalog-authority/trust-policy-bootstrap/v1\0".as_slice(),
4 * 1024,
),
];
for (domain, prefix, maximum_payload_bytes) in cases {
assert_eq!(domain.prefix(), prefix);
assert_eq!(domain.prefix().last(), Some(&0));
assert_eq!(domain.maximum_payload_bytes(), maximum_payload_bytes);
}
}
#[test]
fn transport_is_exact_canonical_strict_json() {
let signing_key = SigningKey::from_bytes(&TEST_ONLY_SIGNING_SEED);
let payload = positive_typed_payload(CatalogAuthorityDomain::PromotionApply);
let transport = signed_transport(&signing_key, &payload);
let canonical = canonical_json(&transport);
let mut whitespace = b" ".to_vec();
whitespace.extend_from_slice(&canonical);
assert_eq!(
verify_signed_envelope(
&whitespace,
signing_key.verifying_key().as_bytes(),
CatalogAuthorityDomain::PromotionApply,
),
Err(SignedEnvelopeError::NonCanonicalTransport)
);
let reordered = format!(
"{{\"algorithm\":\"{}\",\"schema_version\":\"{}\",\"key_id\":\"{}\",\"payload_b64u\":\"{}\",\"signature_b64u\":\"{}\"}}",
transport.algorithm,
transport.schema_version,
transport.key_id,
transport.payload_b64u,
transport.signature_b64u,
);
assert_eq!(
verify_signed_envelope(
reordered.as_bytes(),
signing_key.verifying_key().as_bytes(),
CatalogAuthorityDomain::PromotionApply,
),
Err(SignedEnvelopeError::NonCanonicalTransport)
);
let escaped = String::from_utf8(canonical.clone())
.expect("ASCII transport")
.replacen("1.0.0", "\\u0031.0.0", 1);
assert_eq!(
verify_signed_envelope(
escaped.as_bytes(),
signing_key.verifying_key().as_bytes(),
CatalogAuthorityDomain::PromotionApply,
),
Err(SignedEnvelopeError::NonCanonicalTransport)
);
let unknown = String::from_utf8(canonical.clone())
.expect("ASCII transport")
.replace("}", ",\"unexpected\":\"not-allowed\"}");
assert_eq!(
verify_signed_envelope(
unknown.as_bytes(),
signing_key.verifying_key().as_bytes(),
CatalogAuthorityDomain::PromotionApply,
),
Err(SignedEnvelopeError::MalformedTransport)
);
let duplicate = String::from_utf8(canonical)
.expect("ASCII transport")
.replace("}", ",\"algorithm\":\"Ed25519\"}");
assert_eq!(
verify_signed_envelope(
duplicate.as_bytes(),
signing_key.verifying_key().as_bytes(),
CatalogAuthorityDomain::PromotionApply,
),
Err(SignedEnvelopeError::MalformedTransport)
);
}
#[test]
fn schema_algorithm_key_and_public_key_fail_closed() {
let signing_key = SigningKey::from_bytes(&TEST_ONLY_SIGNING_SEED);
let payload = positive_typed_payload(CatalogAuthorityDomain::PromotionApply);
let mut transport = signed_transport(&signing_key, &payload);
transport.schema_version = "2.0.0".to_owned();
assert_eq!(
verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
CatalogAuthorityDomain::PromotionApply,
),
Err(SignedEnvelopeError::UnsupportedSchemaVersion)
);
transport.schema_version = SIGNED_ENVELOPE_SCHEMA_VERSION.to_owned();
transport.algorithm = "ed25519".to_owned();
assert_eq!(
verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
CatalogAuthorityDomain::PromotionApply,
),
Err(SignedEnvelopeError::UnsupportedAlgorithm)
);
transport.algorithm = SIGNED_ENVELOPE_ALGORITHM.to_owned();
transport.key_id = "sha256:ABCDEF".to_owned();
assert_eq!(
verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
CatalogAuthorityDomain::PromotionApply,
),
Err(SignedEnvelopeError::InvalidKeyId)
);
transport.key_id = format!("sha256:{}", "0".repeat(64));
assert_eq!(
verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
CatalogAuthorityDomain::PromotionApply,
),
Err(SignedEnvelopeError::KeyIdMismatch)
);
assert_eq!(
verify_signed_envelope(
&canonical_json(&signed_transport(&signing_key, &payload)),
&[1; 31],
CatalogAuthorityDomain::PromotionApply,
),
Err(SignedEnvelopeError::InvalidPublicKey)
);
let weak_key = [0; 32];
assert_eq!(
verify_signed_envelope(
&canonical_json(&signed_transport(&signing_key, &payload)),
&weak_key,
CatalogAuthorityDomain::PromotionApply,
),
Err(SignedEnvelopeError::WeakPublicKey)
);
}
#[test]
fn base64_size_domain_and_signature_substitution_fail_closed() {
let signing_key = SigningKey::from_bytes(&TEST_ONLY_SIGNING_SEED);
let domain = CatalogAuthorityDomain::PromotionApply;
let payload = positive_typed_payload(CatalogAuthorityDomain::PromotionApply);
let mut transport = signed_transport(&signing_key, &payload);
transport.payload_b64u.push('=');
assert_eq!(
verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
domain,
),
Err(SignedEnvelopeError::InvalidPayloadEncoding)
);
transport = signed_transport(&signing_key, &payload);
transport.payload_b64u = "AB".to_owned();
assert_eq!(
verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
domain,
),
Err(SignedEnvelopeError::InvalidPayloadEncoding)
);
transport = signed_transport(&signing_key, &payload);
transport.payload_b64u = "+".to_owned();
assert_eq!(
verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
domain,
),
Err(SignedEnvelopeError::InvalidPayloadEncoding)
);
transport = signed_transport(&signing_key, &payload);
transport.signature_b64u.push('=');
assert_eq!(
verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
domain,
),
Err(SignedEnvelopeError::InvalidSignatureEncoding)
);
let exact_bound = domain_payload(
domain,
&vec![0; domain.maximum_payload_bytes() - domain.prefix().len()],
);
transport = signed_transport(&signing_key, &exact_bound);
assert_eq!(
verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
domain,
),
Err(SignedEnvelopeError::InvalidTypedPayload)
);
let oversized = domain_payload(domain, &vec![0; domain.maximum_payload_bytes()]);
transport = signed_transport(&signing_key, &oversized);
assert_eq!(
verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
domain,
),
Err(SignedEnvelopeError::InvalidPayloadSize)
);
let substituted_domain =
domain_payload(CatalogAuthorityDomain::PromotionRollback, b"strict");
transport = signed_transport(&signing_key, &substituted_domain);
assert_eq!(
verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
domain,
),
Err(SignedEnvelopeError::DomainMismatch)
);
transport = signed_transport(&signing_key, &payload);
let mut signature = STRICT_BASE64URL
.decode(&transport.signature_b64u)
.expect("test signature decodes");
signature[0] ^= 1;
transport.signature_b64u = STRICT_BASE64URL.encode(signature);
assert_eq!(
verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
domain,
),
Err(SignedEnvelopeError::InvalidSignature)
);
assert_eq!(
verify_signed_envelope(
&vec![b' '; MAX_SIGNED_TRANSPORT_BYTES + 1],
signing_key.verifying_key().as_bytes(),
domain,
),
Err(SignedEnvelopeError::InvalidTransportSize)
);
}
#[test]
fn payload_signer_id_must_match_transport_key_for_every_payload_kind() {
let signing_key = SigningKey::from_bytes(&TEST_ONLY_SIGNING_SEED);
let domains = [
CatalogAuthorityDomain::ConformanceEvidence,
CatalogAuthorityDomain::PromotionApply,
CatalogAuthorityDomain::PromotionRollback,
CatalogAuthorityDomain::TrustPolicyUpdate,
CatalogAuthorityDomain::TrustPolicyBootstrap,
];
for domain in domains {
let payload = positive_typed_payload(domain);
let transport = signed_transport(&signing_key, &payload);
let _verified = verify_signed_envelope(
&canonical_json(&transport),
signing_key.verifying_key().as_bytes(),
domain,
)
.expect("matching payload and transport signer verifies");
let mut substituted = payload;
let signer_start = domain.prefix().len();
if domain == CatalogAuthorityDomain::TrustPolicyBootstrap {
let substituted_key = SigningKey::from_bytes(&[8; 32]).verifying_key();
substituted[signer_start..signer_start + 32]
.copy_from_slice(&Sha256::digest(substituted_key.as_bytes()));
substituted[signer_start + 32..signer_start + 64]
.copy_from_slice(substituted_key.as_bytes());
} else {
substituted[signer_start] ^= 0x01;
}
let substituted_transport = signed_transport(&signing_key, &substituted);
assert_eq!(
verify_signed_envelope(
&canonical_json(&substituted_transport),
signing_key.verifying_key().as_bytes(),
domain,
),
Err(SignedEnvelopeError::PayloadKeyIdMismatch),
"{domain:?} payload signer substitution must fail closed"
);
}
}
#[test]
fn errors_and_debug_output_do_not_expose_signed_bytes_or_signatures() {
let signing_key = SigningKey::from_bytes(&TEST_ONLY_SIGNING_SEED);
let marker = b"sensitive-marker-never-in-error";
let mut payload = positive_typed_payload(CatalogAuthorityDomain::PromotionApply);
replace_ascii_field(
&mut payload,
"promotion-001",
std::str::from_utf8(marker).expect("ASCII marker"),
);
let transport = signed_transport(&signing_key, &payload);
let mut signature = STRICT_BASE64URL
.decode(&transport.signature_b64u)
.expect("test signature decodes");
signature[0] ^= 1;
let mut invalid = transport;
invalid.signature_b64u = STRICT_BASE64URL.encode(signature);
let error = verify_signed_envelope(
&canonical_json(&invalid),
signing_key.verifying_key().as_bytes(),
CatalogAuthorityDomain::PromotionApply,
)
.expect_err("substituted signature fails");
let rendered = format!("{error:?} {error}");
assert!(!rendered.contains("sensitive-marker"));
assert!(!rendered.contains(&invalid.payload_b64u));
assert!(!rendered.contains(&invalid.signature_b64u));
}
}