1use 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
45pub const DSSE_PREAMBLE: &str = "DSSEv1";
47
48pub const AION_ATTESTATION_TYPE: &str = "application/vnd.aion.attestation.v1+json";
50
51pub const AION_MANIFEST_TYPE: &str = "application/vnd.aion.manifest.v1+json";
53
54pub const AION_KEYID_PREFIX: &str = "aion:author:";
56
57#[must_use]
59pub fn keyid_for(author: AuthorId) -> String {
60 format!("{AION_KEYID_PREFIX}{}", author.as_u64())
61}
62
63pub 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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct DsseEnvelope {
118 #[serde(rename = "payloadType")]
120 pub payload_type: String,
121 #[serde(with = "base64_bytes")]
123 pub payload: Vec<u8>,
124 pub signatures: Vec<DsseSignature>,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct DsseSignature {
131 pub keyid: String,
133 #[serde(with = "base64_bytes")]
135 pub sig: Vec<u8>,
136}
137
138mod 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#[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
177pub 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
191pub 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 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 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
287fn hex32(bytes: &[u8; 32]) -> String {
293 hex::encode(bytes)
294}
295
296#[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 serde_json::to_vec(&json).unwrap_or_else(|_| std::process::abort())
316}
317
318#[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#[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#[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 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 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 #[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, ®, 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, ®, 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, ®, 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, ®, 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 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, ®, 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, ®, 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, ®, 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 let reg = reg_pinning(signer, &fake_key);
540 assert!(verify_envelope(&env, ®, 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 let signers: Vec<(AuthorId, SigningKey)> = (0..n)
562 .map(|i| (AuthorId::new(1_000 + u64::from(i)), SigningKey::generate()))
563 .collect();
564 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, ®, 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 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, ®, 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 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 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, ®, 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 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, ®, 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, ®, v_after).is_err());
670 }
671 }
672}