Skip to main content

aion_context/
dsse.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! DSSE envelope support — RFC-0023.
3//!
4//! DSSE (Dead Simple Signing Envelope) is the universal envelope format
5//! used across Sigstore, in-toto, SLSA, Kyverno, and every major
6//! supply-chain verifier in 2026. This module emits and verifies DSSE
7//! envelopes wrapping aion signatures, giving `aion-context` interop
8//! with those ecosystems.
9//!
10//! Signatures are still produced by [`crate::crypto::SigningKey`] —
11//! only the wire format changes. DSSE's native multi-signature support
12//! maps onto RFC-0021 multi-party attestations.
13//!
14//! # Example
15//!
16//! ```
17//! use aion_context::dsse::{sign_envelope, verify_envelope, AION_ATTESTATION_TYPE};
18//! use aion_context::crypto::SigningKey;
19//! use aion_context::key_registry::KeyRegistry;
20//! use aion_context::types::AuthorId;
21//!
22//! let payload = br#"{"_type":"https://aion-context.dev/attestation/v1"}"#;
23//! let signer = AuthorId::new(50001);
24//! let master = SigningKey::generate();
25//! let key = SigningKey::generate();
26//! let mut registry = KeyRegistry::new();
27//! registry
28//!     .register_author(signer, master.verifying_key(), key.verifying_key(), 0)
29//!     .unwrap();
30//!
31//! let envelope = sign_envelope(payload, AION_ATTESTATION_TYPE, signer, &key);
32//! let verified = verify_envelope(&envelope, &registry, 1).unwrap();
33//! assert_eq!(verified, vec!["aion:author:50001".to_string()]);
34//! ```
35
36use base64::engine::general_purpose::STANDARD_NO_PAD;
37use serde::{Deserialize, Deserializer, Serialize, Serializer};
38
39use crate::crypto::{SigningKey, VerifyingKey};
40use crate::manifest::ArtifactManifest;
41use crate::serializer::VersionEntry;
42use crate::types::AuthorId;
43use crate::{AionError, Result};
44
45/// DSSE protocol preamble — signed into every PAE.
46pub const DSSE_PREAMBLE: &str = "DSSEv1";
47
48/// `payloadType` for aion version attestations (RFC-0021 carried via DSSE).
49pub const AION_ATTESTATION_TYPE: &str = "application/vnd.aion.attestation.v1+json";
50
51/// `payloadType` for aion external-artifact manifests (RFC-0022).
52pub const AION_MANIFEST_TYPE: &str = "application/vnd.aion.manifest.v1+json";
53
54/// Keyid prefix for aion signatures. Full form: `aion:author:<decimal_id>`.
55pub const AION_KEYID_PREFIX: &str = "aion:author:";
56
57/// Build the canonical keyid string for an [`AuthorId`].
58#[must_use]
59pub fn keyid_for(author: AuthorId) -> String {
60    format!("{AION_KEYID_PREFIX}{}", author.as_u64())
61}
62
63/// Parse a keyid back to an [`AuthorId`].
64///
65/// # Errors
66///
67/// Returns `Err` for keyids that do not start with [`AION_KEYID_PREFIX`]
68/// or whose suffix is not a valid `u64`.
69pub fn author_from_keyid(keyid: &str) -> Result<AuthorId> {
70    let suffix = keyid
71        .strip_prefix(AION_KEYID_PREFIX)
72        .ok_or_else(|| AionError::InvalidFormat {
73            reason: format!("keyid does not start with '{AION_KEYID_PREFIX}': {keyid}"),
74        })?;
75    let id = suffix
76        .parse::<u64>()
77        .map_err(|_| AionError::InvalidFormat {
78            reason: format!("keyid suffix is not a u64: {suffix}"),
79        })?;
80    Ok(AuthorId::new(id))
81}
82
83/// Pre-Authentication Encoding — the exact bytes signed/verified.
84///
85/// ```text
86/// PAE(type, body) = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body
87/// ```
88#[must_use]
89pub fn pae(payload_type: &str, payload: &[u8]) -> Vec<u8> {
90    let type_len = payload_type.len().to_string();
91    let body_len = payload.len().to_string();
92    let mut out = Vec::with_capacity(
93        DSSE_PREAMBLE
94            .len()
95            .saturating_add(3)
96            .saturating_add(type_len.len())
97            .saturating_add(payload_type.len())
98            .saturating_add(body_len.len())
99            .saturating_add(payload.len()),
100    );
101    out.extend_from_slice(DSSE_PREAMBLE.as_bytes());
102    out.push(b' ');
103    out.extend_from_slice(type_len.as_bytes());
104    out.push(b' ');
105    out.extend_from_slice(payload_type.as_bytes());
106    out.push(b' ');
107    out.extend_from_slice(body_len.as_bytes());
108    out.push(b' ');
109    out.extend_from_slice(payload);
110    out
111}
112
113/// A DSSE envelope. Serialises to the canonical DSSE JSON form on the wire:
114/// `payload` and `sig` fields are base64-standard-no-padding when encoded,
115/// raw bytes when in memory.
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct DsseEnvelope {
118    /// Media type URI describing `payload`.
119    #[serde(rename = "payloadType")]
120    pub payload_type: String,
121    /// Raw payload bytes; JSON encodes/decodes as base64.
122    #[serde(with = "base64_bytes")]
123    pub payload: Vec<u8>,
124    /// All signatures bound to the (`payload_type`, payload) tuple.
125    pub signatures: Vec<DsseSignature>,
126}
127
128/// One signature entry inside a [`DsseEnvelope`].
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct DsseSignature {
131    /// Opaque key identifier. aion uses `aion:author:<decimal>`.
132    pub keyid: String,
133    /// Raw 64-byte Ed25519 signature; JSON encodes/decodes as base64.
134    #[serde(with = "base64_bytes")]
135    pub sig: Vec<u8>,
136}
137
138/// Serde adapter: `Vec<u8>` ⇄ base64-standard-no-padding string.
139mod base64_bytes {
140    use super::{Deserializer, Serializer, STANDARD_NO_PAD};
141    use base64::Engine;
142    use serde::{Deserialize, Serialize};
143
144    pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
145        let encoded = STANDARD_NO_PAD.encode(bytes);
146        encoded.serialize(serializer)
147    }
148
149    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
150        let raw = String::deserialize(deserializer)?;
151        STANDARD_NO_PAD
152            .decode(raw.as_bytes())
153            .map_err(serde::de::Error::custom)
154    }
155}
156
157/// Produce a single-signature envelope for `payload` under `payload_type`.
158#[must_use]
159pub fn sign_envelope(
160    payload: &[u8],
161    payload_type: &str,
162    signer: AuthorId,
163    key: &SigningKey,
164) -> DsseEnvelope {
165    let message = pae(payload_type, payload);
166    let signature_bytes = key.sign(&message);
167    DsseEnvelope {
168        payload_type: payload_type.to_string(),
169        payload: payload.to_vec(),
170        signatures: vec![DsseSignature {
171            keyid: keyid_for(signer),
172            sig: signature_bytes.to_vec(),
173        }],
174    }
175}
176
177/// Append an additional signature to an existing envelope.
178///
179/// Used to build up a multi-signature envelope for RFC-0021
180/// multi-party attestations. Infallible — Ed25519 signing over
181/// fixed-size inputs cannot fail.
182pub fn add_signature(envelope: &mut DsseEnvelope, signer: AuthorId, key: &SigningKey) {
183    let message = pae(&envelope.payload_type, &envelope.payload);
184    let signature_bytes = key.sign(&message);
185    envelope.signatures.push(DsseSignature {
186        keyid: keyid_for(signer),
187        sig: signature_bytes.to_vec(),
188    });
189}
190
191/// Verify every envelope signature against the pinned registry — RFC-0023 / RFC-0034.
192///
193/// Each signature is checked against its signer's active epoch in
194/// [`KeyRegistry`](crate::key_registry::KeyRegistry) at
195/// `at_version`. Returns the distinct keyids of verified signatures
196/// in envelope order.
197///
198/// For each distinct keyid the signer is resolved via
199/// [`author_from_keyid`] and cross-checked against the active
200/// epoch in `registry`. A signer whose keyid does not parse as a
201/// well-formed aion keyid, or who has no active epoch at
202/// `at_version`, causes the whole envelope to fail.
203///
204/// A given `keyid` contributes to the returned vector **at most
205/// once** even if the envelope carries multiple signature entries
206/// under the same keyid (RFC-0033 C6).
207///
208/// # Errors
209///
210/// Returns `Err` if the envelope has zero signatures, if any keyid
211/// parses as a non-aion form, if any signer has no active epoch
212/// at `at_version`, or if any signature fails Ed25519 verification
213/// under the pinned key.
214pub fn verify_envelope(
215    envelope: &DsseEnvelope,
216    registry: &crate::key_registry::KeyRegistry,
217    at_version: u64,
218) -> Result<Vec<String>> {
219    if envelope.signatures.is_empty() {
220        return Err(AionError::InvalidFormat {
221            reason: "DSSE envelope has zero signatures".to_string(),
222        });
223    }
224    let message = pae(&envelope.payload_type, &envelope.payload);
225    let mut verified = Vec::with_capacity(envelope.signatures.len());
226    let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
227    for sig_entry in &envelope.signatures {
228        if !seen.insert(sig_entry.keyid.as_str()) {
229            continue;
230        }
231        let author = author_from_keyid(&sig_entry.keyid).map_err(|_| AionError::InvalidFormat {
232            reason: format!("non-aion keyid cannot be resolved: {}", sig_entry.keyid),
233        })?;
234        let epoch = registry
235            .active_epoch_at(author, at_version)
236            .ok_or_else(|| AionError::InvalidFormat {
237                reason: format!(
238                    "no active epoch at version {at_version} for keyid: {}",
239                    sig_entry.keyid
240                ),
241            })?;
242        let verifying_key = VerifyingKey::from_bytes(&epoch.public_key)?;
243        let sig_bytes =
244            sig_entry
245                .sig
246                .as_slice()
247                .try_into()
248                .map_err(|_| AionError::InvalidFormat {
249                    reason: format!(
250                        "DSSE signature for {} has length {} (expected 64)",
251                        sig_entry.keyid,
252                        sig_entry.sig.len()
253                    ),
254                })?;
255        verifying_key.verify(&message, sig_bytes)?;
256        verified.push(sig_entry.keyid.clone());
257    }
258    Ok(verified)
259}
260
261impl DsseEnvelope {
262    /// Serialise to canonical DSSE JSON.
263    ///
264    /// # Errors
265    ///
266    /// Propagates `serde_json` errors (should not occur for
267    /// well-constructed envelopes).
268    pub fn to_json(&self) -> Result<String> {
269        serde_json::to_string(self).map_err(|e| AionError::InvalidFormat {
270            reason: format!("DSSE JSON serialization failed: {e}"),
271        })
272    }
273
274    /// Parse a DSSE envelope from JSON.
275    ///
276    /// # Errors
277    ///
278    /// Returns `Err` for malformed JSON or invalid base64 in the
279    /// `payload` / `sig` fields.
280    pub fn from_json(s: &str) -> Result<Self> {
281        serde_json::from_str(s).map_err(|e| AionError::InvalidFormat {
282            reason: format!("DSSE JSON deserialization failed: {e}"),
283        })
284    }
285}
286
287// ---------------------------------------------------------------------------
288// Aion-native payload builders.
289// ---------------------------------------------------------------------------
290
291/// Hex-encode a fixed-size hash to lowercase.
292fn hex32(bytes: &[u8; 32]) -> String {
293    hex::encode(bytes)
294}
295
296/// Build the canonical JSON body for an aion version attestation.
297///
298/// Shape matches the RFC-0023 §"Aion payload types" table.
299#[must_use]
300pub fn version_attestation_payload(version: &VersionEntry, signer: AuthorId) -> Vec<u8> {
301    let json = serde_json::json!({
302        "_type": "https://aion-context.dev/attestation/v1",
303        "version": {
304            "version_number": version.version_number,
305            "parent_hash": hex32(&version.parent_hash),
306            "rules_hash": hex32(&version.rules_hash),
307            "author_id": version.author_id,
308            "timestamp": version.timestamp,
309            "message_offset": version.message_offset,
310            "message_length": version.message_length,
311        },
312        "signer": signer.as_u64(),
313    });
314    // Safety: serde_json::to_vec on a Value cannot fail for finite data.
315    serde_json::to_vec(&json).unwrap_or_else(|_| std::process::abort())
316}
317
318/// Build the canonical JSON body for an aion artifact manifest.
319#[must_use]
320pub fn manifest_payload(manifest: &ArtifactManifest) -> Vec<u8> {
321    let entries: Vec<serde_json::Value> = manifest
322        .entries()
323        .iter()
324        .map(|entry| {
325            let name = manifest
326                .name_of(entry)
327                .unwrap_or("<invalid-utf8>")
328                .to_string();
329            serde_json::json!({
330                "name": name,
331                "size": entry.size,
332                "hash_algorithm": "BLAKE3-256",
333                "hash": hex32(&entry.hash),
334            })
335        })
336        .collect();
337    let json = serde_json::json!({
338        "_type": "https://aion-context.dev/manifest/v1",
339        "manifest_id": hex32(manifest.manifest_id()),
340        "entries": entries,
341    });
342    serde_json::to_vec(&json).unwrap_or_else(|_| std::process::abort())
343}
344
345/// Wrap a version attestation into a DSSE envelope signed by `signer`.
346#[must_use]
347pub fn wrap_version_attestation(
348    version: &VersionEntry,
349    signer: AuthorId,
350    key: &SigningKey,
351) -> DsseEnvelope {
352    let payload = version_attestation_payload(version, signer);
353    sign_envelope(&payload, AION_ATTESTATION_TYPE, signer, key)
354}
355
356/// Wrap an artifact manifest into a DSSE envelope signed by `signer`.
357#[must_use]
358pub fn wrap_manifest(
359    manifest: &ArtifactManifest,
360    signer: AuthorId,
361    key: &SigningKey,
362) -> DsseEnvelope {
363    let payload = manifest_payload(manifest);
364    sign_envelope(&payload, AION_MANIFEST_TYPE, signer, key)
365}
366
367#[cfg(test)]
368#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
369mod tests {
370    use super::*;
371    use crate::key_registry::KeyRegistry;
372
373    /// Build a registry pinning one signer at epoch 0 with `key` as its
374    /// operational pubkey. Master key is throwaway.
375    fn reg_pinning(signer: AuthorId, key: &SigningKey) -> KeyRegistry {
376        let mut reg = KeyRegistry::new();
377        let master = SigningKey::generate();
378        reg.register_author(signer, master.verifying_key(), key.verifying_key(), 0)
379            .unwrap_or_else(|_| std::process::abort());
380        reg
381    }
382
383    /// Build a registry pinning every `(signer, key)` pair at epoch 0.
384    fn reg_pinning_multi(pairs: &[(AuthorId, SigningKey)]) -> KeyRegistry {
385        let mut reg = KeyRegistry::new();
386        for (signer, key) in pairs {
387            let master = SigningKey::generate();
388            reg.register_author(*signer, master.verifying_key(), key.verifying_key(), 0)
389                .unwrap_or_else(|_| std::process::abort());
390        }
391        reg
392    }
393
394    /// RFC-0023 Appendix vector: PAE("test", "hello").
395    #[test]
396    fn pae_matches_spec_vector() {
397        let out = pae("test", b"hello");
398        assert_eq!(out.as_slice(), b"DSSEv1 4 test 5 hello");
399    }
400
401    #[test]
402    fn pae_empty_body_is_well_formed() {
403        let out = pae("x", b"");
404        assert_eq!(out.as_slice(), b"DSSEv1 1 x 0 ");
405    }
406
407    #[test]
408    fn keyid_round_trip() {
409        let a = AuthorId::new(12345);
410        let k = keyid_for(a);
411        assert_eq!(k, "aion:author:12345");
412        let parsed = author_from_keyid(&k).unwrap();
413        assert_eq!(parsed, a);
414    }
415
416    #[test]
417    fn keyid_rejects_wrong_prefix() {
418        assert!(author_from_keyid("not-aion:42").is_err());
419        assert!(author_from_keyid("aion:author:xyz").is_err());
420    }
421
422    #[test]
423    fn sign_verify_roundtrip() {
424        let key = SigningKey::generate();
425        let signer = AuthorId::new(7);
426        let envelope = sign_envelope(b"hello world", "text/plain", signer, &key);
427        let reg = reg_pinning(signer, &key);
428        let verified = verify_envelope(&envelope, &reg, 1).unwrap();
429        assert_eq!(verified, vec![keyid_for(signer)]);
430    }
431
432    #[test]
433    fn tampered_payload_fails_verification() {
434        let key = SigningKey::generate();
435        let signer = AuthorId::new(7);
436        let mut envelope = sign_envelope(b"hello", "text/plain", signer, &key);
437        envelope.payload[0] ^= 0x01;
438        let reg = reg_pinning(signer, &key);
439        assert!(verify_envelope(&envelope, &reg, 1).is_err());
440    }
441
442    #[test]
443    fn multi_signature_all_verify() {
444        let k1 = SigningKey::generate();
445        let k2 = SigningKey::generate();
446        let s1 = AuthorId::new(1);
447        let s2 = AuthorId::new(2);
448        let mut env = sign_envelope(b"payload", "text/plain", s1, &k1);
449        add_signature(&mut env, s2, &k2);
450        let reg = reg_pinning_multi(&[(s1, k1), (s2, k2)]);
451        let verified = verify_envelope(&env, &reg, 1).unwrap();
452        assert_eq!(verified.len(), 2);
453    }
454
455    #[test]
456    fn verify_rejects_empty_signatures() {
457        let env = DsseEnvelope {
458            payload_type: "text/plain".to_string(),
459            payload: b"x".to_vec(),
460            signatures: Vec::new(),
461        };
462        let reg = KeyRegistry::new();
463        assert!(verify_envelope(&env, &reg, 1).is_err());
464    }
465
466    #[test]
467    fn json_roundtrip_preserves_envelope() {
468        let key = SigningKey::generate();
469        let signer = AuthorId::new(3);
470        let env = sign_envelope(b"abc", "text/plain", signer, &key);
471        let json = env.to_json().unwrap();
472        let parsed = DsseEnvelope::from_json(&json).unwrap();
473        assert_eq!(parsed, env);
474    }
475
476    #[test]
477    fn json_payload_field_uses_base64() {
478        let key = SigningKey::generate();
479        let signer = AuthorId::new(3);
480        let env = sign_envelope(b"\xff\x00\x7f", "text/plain", signer, &key);
481        let json = env.to_json().unwrap();
482        // Expect base64-standard-no-padding of [0xff, 0x00, 0x7f] = "/wB/"
483        assert!(json.contains("\"payload\":\"/wB/\""));
484    }
485
486    mod properties {
487        use super::*;
488        use hegel::generators as gs;
489
490        #[hegel::test]
491        fn prop_dsse_sign_verify_roundtrip(tc: hegel::TestCase) {
492            let payload = tc.draw(gs::binary().max_size(1024));
493            let ptype = tc.draw(gs::text().min_size(1).max_size(64));
494            let signer = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
495            let key = SigningKey::generate();
496            let env = sign_envelope(&payload, &ptype, signer, &key);
497            let reg = reg_pinning(signer, &key);
498            let verified = verify_envelope(&env, &reg, 1).unwrap_or_else(|_| std::process::abort());
499            assert_eq!(verified.len(), 1);
500        }
501
502        #[hegel::test]
503        fn prop_dsse_tampered_payload_rejects(tc: hegel::TestCase) {
504            let payload = tc.draw(gs::binary().min_size(1).max_size(1024));
505            let ptype = tc.draw(gs::text().min_size(1).max_size(64));
506            let signer = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
507            let key = SigningKey::generate();
508            let mut env = sign_envelope(&payload, &ptype, signer, &key);
509            let max_idx = env.payload.len().saturating_sub(1);
510            let idx = tc.draw(gs::integers::<usize>().max_value(max_idx));
511            if let Some(byte) = env.payload.get_mut(idx) {
512                *byte ^= 0x01;
513            }
514            let reg = reg_pinning(signer, &key);
515            assert!(verify_envelope(&env, &reg, 1).is_err());
516        }
517
518        #[hegel::test]
519        fn prop_dsse_tampered_payload_type_rejects(tc: hegel::TestCase) {
520            let payload = tc.draw(gs::binary().max_size(1024));
521            let ptype = tc.draw(gs::text().min_size(1).max_size(64));
522            let signer = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
523            let key = SigningKey::generate();
524            let mut env = sign_envelope(&payload, &ptype, signer, &key);
525            env.payload_type.push('!');
526            let reg = reg_pinning(signer, &key);
527            assert!(verify_envelope(&env, &reg, 1).is_err());
528        }
529
530        #[hegel::test]
531        fn prop_dsse_wrong_key_rejects(tc: hegel::TestCase) {
532            let payload = tc.draw(gs::binary().max_size(1024));
533            let ptype = tc.draw(gs::text().min_size(1).max_size(64));
534            let signer = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
535            let real_key = SigningKey::generate();
536            let fake_key = SigningKey::generate();
537            let env = sign_envelope(&payload, &ptype, signer, &real_key);
538            // Pin the WRONG key for the signer — registry check rejects.
539            let reg = reg_pinning(signer, &fake_key);
540            assert!(verify_envelope(&env, &reg, 1).is_err());
541        }
542
543        #[hegel::test]
544        fn prop_dsse_json_roundtrip(tc: hegel::TestCase) {
545            let payload = tc.draw(gs::binary().max_size(1024));
546            let ptype = tc.draw(gs::text().min_size(1).max_size(64));
547            let signer = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
548            let key = SigningKey::generate();
549            let env = sign_envelope(&payload, &ptype, signer, &key);
550            let json = env.to_json().unwrap_or_else(|_| std::process::abort());
551            let parsed = DsseEnvelope::from_json(&json).unwrap_or_else(|_| std::process::abort());
552            assert_eq!(parsed, env);
553        }
554
555        #[hegel::test]
556        fn prop_dsse_multi_signature_all_verify(tc: hegel::TestCase) {
557            let n = tc.draw(gs::integers::<u32>().min_value(2).max_value(6));
558            let payload = tc.draw(gs::binary().max_size(512));
559            let ptype = tc.draw(gs::text().min_size(1).max_size(32));
560            // Build N distinct (author, key) pairs.
561            let signers: Vec<(AuthorId, SigningKey)> = (0..n)
562                .map(|i| (AuthorId::new(1_000 + u64::from(i)), SigningKey::generate()))
563                .collect();
564            // Start an envelope with signer 0, then add 1..n.
565            let first = signers.first().unwrap_or_else(|| std::process::abort());
566            let mut env = sign_envelope(&payload, &ptype, first.0, &first.1);
567            for (who, key) in signers.iter().skip(1) {
568                add_signature(&mut env, *who, key);
569            }
570            let reg = reg_pinning_multi(&signers);
571            let verified = verify_envelope(&env, &reg, 1).unwrap_or_else(|_| std::process::abort());
572            assert_eq!(verified.len(), n as usize);
573        }
574
575        #[hegel::test]
576        fn prop_dsse_verify_dedups_repeated_keyid(tc: hegel::TestCase) {
577            // RFC-0033 C6: an envelope with N entries under the same
578            // keyid must yield exactly one element in `verified`.
579            let payload = tc.draw(gs::binary().max_size(256));
580            let ptype = tc.draw(gs::text().min_size(1).max_size(32));
581            let signer = AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1)));
582            let extra = tc.draw(gs::integers::<usize>().min_value(1).max_value(4));
583            let key = SigningKey::generate();
584            let mut env = sign_envelope(&payload, &ptype, signer, &key);
585            for _ in 0..extra {
586                add_signature(&mut env, signer, &key);
587            }
588            let reg = reg_pinning(signer, &key);
589            let verified = verify_envelope(&env, &reg, 1).unwrap_or_else(|_| std::process::abort());
590            assert_eq!(verified.len(), 1);
591        }
592
593        #[hegel::test]
594        fn prop_dsse_pae_injective_on_body(tc: hegel::TestCase) {
595            // Two payloads that differ in any byte must produce
596            // different PAE output for the same payload_type.
597            let ptype = tc.draw(gs::text().min_size(1).max_size(32));
598            let mut body_a = tc.draw(gs::binary().min_size(1).max_size(512));
599            let idx = tc.draw(gs::integers::<usize>().max_value(body_a.len().saturating_sub(1)));
600            let mut body_b = body_a.clone();
601            if let Some(b) = body_b.get_mut(idx) {
602                *b ^= 0x01;
603            }
604            // make them definitely differ even if idx was out-of-range
605            body_a.push(0);
606            body_b.push(1);
607            assert_ne!(pae(&ptype, &body_a), pae(&ptype, &body_b));
608        }
609
610        #[hegel::test]
611        fn prop_dsse_registry_verify_accepts_pinned_signer(tc: hegel::TestCase) {
612            use crate::key_registry::KeyRegistry;
613            let payload = tc.draw(gs::binary().max_size(512));
614            let ptype = tc.draw(gs::text().min_size(1).max_size(32));
615            let signer =
616                AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 32)));
617            let master = SigningKey::generate();
618            let op = SigningKey::generate();
619            let mut reg = KeyRegistry::new();
620            reg.register_author(signer, master.verifying_key(), op.verifying_key(), 0)
621                .unwrap_or_else(|_| std::process::abort());
622            let env = sign_envelope(&payload, &ptype, signer, &op);
623            let at = tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 20));
624            let verified =
625                verify_envelope(&env, &reg, at).unwrap_or_else(|_| std::process::abort());
626            assert_eq!(verified.len(), 1);
627            assert_eq!(verified[0], keyid_for(signer));
628        }
629
630        #[hegel::test]
631        fn prop_dsse_registry_verify_rejects_unknown_signer(tc: hegel::TestCase) {
632            use crate::key_registry::KeyRegistry;
633            let payload = tc.draw(gs::binary().max_size(256));
634            let ptype = tc.draw(gs::text().min_size(1).max_size(32));
635            let signer =
636                AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 32)));
637            let op = SigningKey::generate();
638            // Registry is empty — `signer` is not registered.
639            let reg = KeyRegistry::new();
640            let env = sign_envelope(&payload, &ptype, signer, &op);
641            let at = tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 20));
642            assert!(verify_envelope(&env, &reg, at).is_err());
643        }
644
645        #[hegel::test]
646        fn prop_dsse_registry_verify_rejects_revoked_signer(tc: hegel::TestCase) {
647            use crate::key_registry::{sign_revocation_record, KeyRegistry, RevocationReason};
648            let payload = tc.draw(gs::binary().max_size(256));
649            let ptype = tc.draw(gs::text().min_size(1).max_size(32));
650            let signer =
651                AuthorId::new(tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 32)));
652            let master = SigningKey::generate();
653            let op = SigningKey::generate();
654            let mut reg = KeyRegistry::new();
655            reg.register_author(signer, master.verifying_key(), op.verifying_key(), 0)
656                .unwrap_or_else(|_| std::process::abort());
657            let effective = tc.draw(gs::integers::<u64>().min_value(1).max_value(1 << 20));
658            let revocation = sign_revocation_record(
659                signer,
660                0,
661                RevocationReason::Compromised,
662                effective,
663                &master,
664            );
665            reg.apply_revocation(&revocation)
666                .unwrap_or_else(|_| std::process::abort());
667            let env = sign_envelope(&payload, &ptype, signer, &op);
668            let v_after = effective.saturating_add(1);
669            assert!(verify_envelope(&env, &reg, v_after).is_err());
670        }
671    }
672}