1use crate::{CryptoRng, Error};
28use ed25519_dalek::Verifier as _;
29use serde_json::{Map, Value};
30use sha2::{Digest as _, Sha256};
31
32const ED25519_PUB_MULTICODEC: [u8; 2] = [0xed, 0x01];
34
35const MLDSA44_PUB_MULTICODEC: [u8; 2] = [0x90, 0x24];
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum Cryptosuite {
42 EddsaJcs2022,
44 Mldsa44Jcs2024,
46}
47
48impl Cryptosuite {
49 #[must_use]
51 pub fn as_str(self) -> &'static str {
52 match self {
53 Self::EddsaJcs2022 => "eddsa-jcs-2022",
54 Self::Mldsa44Jcs2024 => "mldsa44-jcs-2024",
55 }
56 }
57
58 fn from_str(value: &str) -> Option<Self> {
59 match value {
60 "eddsa-jcs-2022" => Some(Self::EddsaJcs2022),
61 "mldsa44-jcs-2024" => Some(Self::Mldsa44Jcs2024),
62 _ => None,
63 }
64 }
65}
66
67#[derive(Clone, Debug, PartialEq, Eq)]
69pub enum PublicKey {
70 Ed25519(Box<[u8; 32]>),
71 MlDsa44(Box<[u8]>),
73}
74
75const ED25519_SPKI_PREFIX: [u8; 12] = [
79 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
80];
81
82const MLDSA44_SPKI_PREFIX: [u8; 22] = [
86 0x30, 0x82, 0x05, 0x32, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03,
87 0x11, 0x03, 0x82, 0x05, 0x21, 0x00,
88];
89
90const MLDSA44_PUBLIC_KEY_BYTES: usize = 1312;
92
93impl PublicKey {
94 pub fn to_spki_der(&self) -> Result<Vec<u8>, Error> {
99 match self {
100 Self::Ed25519(key) => Ok([ED25519_SPKI_PREFIX.as_slice(), key.as_slice()].concat()),
101 Self::MlDsa44(key) if key.len() == MLDSA44_PUBLIC_KEY_BYTES => {
102 Ok([MLDSA44_SPKI_PREFIX.as_slice(), key].concat())
103 }
104 Self::MlDsa44(key) => Err(Error::Key(format!(
105 "an ML-DSA-44 key of {} bytes",
106 key.len()
107 ))),
108 }
109 }
110
111 pub fn to_spki_pem(&self) -> Result<String, Error> {
118 use base64::Engine as _;
119
120 let encoded = base64::engine::general_purpose::STANDARD.encode(self.to_spki_der()?);
121 let mut pem = String::from("-----BEGIN PUBLIC KEY-----\n");
122 for line in encoded.as_bytes().chunks(64) {
123 pem.push_str(core::str::from_utf8(line).unwrap_or_default());
124 pem.push('\n');
125 }
126 pem.push_str("-----END PUBLIC KEY-----\n");
127 Ok(pem)
128 }
129
130 pub fn from_spki_der(der: &[u8]) -> Result<Self, Error> {
135 if let Some(key) = der.strip_prefix(&ED25519_SPKI_PREFIX) {
136 return Ok(Self::Ed25519(Box::new(ed25519_bytes(key)?)));
137 }
138 if let Some(key) = der.strip_prefix(&MLDSA44_SPKI_PREFIX)
139 && key.len() == MLDSA44_PUBLIC_KEY_BYTES
140 {
141 return Ok(Self::MlDsa44(key.to_vec().into_boxed_slice()));
142 }
143 Err(Error::Key(
144 "not an Ed25519 or ML-DSA-44 public key in SPKI".into(),
145 ))
146 }
147
148 pub fn from_spki_pem(pem: &str) -> Result<Self, Error> {
155 use base64::Engine as _;
156
157 let body: String = pem
158 .lines()
159 .map(str::trim)
160 .filter(|line| !line.starts_with("-----"))
161 .collect();
162 let der = base64::engine::general_purpose::STANDARD
163 .decode(body.as_bytes())
164 .map_err(|_| Error::Key("the public key is not base64".into()))?;
165 Self::from_spki_der(&der)
166 }
167}
168
169#[must_use]
178pub fn extract_integrity_proof(
179 document: &Value,
180 now: i64,
181) -> Option<(Map<String, Value>, Cryptosuite, String)> {
182 integrity_proofs(document, now).into_iter().next()
183}
184
185#[must_use]
190pub fn integrity_proofs(
191 document: &Value,
192 now: i64,
193) -> Vec<(Map<String, Value>, Cryptosuite, String)> {
194 let candidates = match document.get("proof") {
195 Some(Value::Array(arr)) => arr.clone(),
196 Some(obj @ Value::Object(_)) => alloc_one(obj.clone()),
197 _ => return Vec::new(),
198 };
199 let mut proofs = Vec::new();
200 for candidate in candidates {
201 let Value::Object(obj) = candidate else {
202 continue;
203 };
204 let Some(suite) = obj
205 .get("cryptosuite")
206 .and_then(Value::as_str)
207 .and_then(Cryptosuite::from_str)
208 else {
209 continue;
210 };
211 let usable = obj.get("proofPurpose").and_then(Value::as_str) == Some("assertionMethod")
212 && obj.get("proofValue").and_then(Value::as_str).is_some()
213 && obj.get("created").and_then(Value::as_str).is_some();
214 if !usable {
215 continue;
216 }
217 if let Some(expires) = obj.get("expires").and_then(Value::as_str) {
220 match chrono::DateTime::parse_from_rfc3339(expires) {
221 Ok(deadline) if deadline.timestamp() < now => continue,
222 Err(_) => continue,
223 Ok(_) => {}
224 }
225 }
226 if let Some(vm) = obj.get("verificationMethod").and_then(Value::as_str) {
227 let vm = vm.to_string();
228 proofs.push((obj, suite, vm));
229 }
230 }
231 proofs
232}
233
234fn alloc_one(value: Value) -> Vec<Value> {
235 vec![value]
236}
237
238pub fn verify_object_integrity_proof(
245 document: &Value,
246 proof: &Map<String, Value>,
247 public_key: &PublicKey,
248) -> Result<(), Error> {
249 let suite = proof
250 .get("cryptosuite")
251 .and_then(Value::as_str)
252 .and_then(Cryptosuite::from_str)
253 .ok_or_else(|| Error::Unsupported("or missing cryptosuite".into()))?;
254 let proof_value = proof
255 .get("proofValue")
256 .and_then(Value::as_str)
257 .ok_or_else(|| Error::Malformed("proof has no proofValue".into()))?;
258
259 let mut proof_config = proof.clone();
264 proof_config.remove("proofValue");
265 let document_context = document.get("@context").cloned();
266
267 match suite {
268 Cryptosuite::EddsaJcs2022 => {
269 if let Some(Value::Array(proof_context)) = proof_config.get("@context").cloned() {
273 let Some(Value::Array(doc_context)) = document_context.clone() else {
274 return Err(Error::Malformed(
275 "proof declares @context but the document does not".into(),
276 ));
277 };
278 if doc_context.len() < proof_context.len()
279 || doc_context[..proof_context.len()] != proof_context[..]
280 {
281 return Err(Error::Malformed(
282 "proof @context is not a prefix of the document's".into(),
283 ));
284 }
285 } else {
286 proof_config.insert(
287 "@context".to_string(),
288 document_context.clone().unwrap_or(Value::Null),
289 );
290 }
291 }
292 Cryptosuite::Mldsa44Jcs2024 => {
293 proof_config.insert(
297 "@context".to_string(),
298 document_context.clone().unwrap_or(Value::Null),
299 );
300 }
301 }
302
303 let proof_canon = canonical(&Value::Object(proof_config))?;
304 let proof_hash = Sha256::digest(proof_canon.as_bytes());
305
306 let signature_bytes = decode_multibase(proof_value)?;
307
308 let mut unsecured = document
310 .as_object()
311 .cloned()
312 .ok_or_else(|| Error::Malformed("document is not a JSON object".into()))?;
313 unsecured.remove("proof");
314 unsecured.remove("https://w3id.org/security#proof");
315
316 if let (Cryptosuite::EddsaJcs2022, Some(proof_context)) = (suite, proof.get("@context")) {
320 unsecured.insert("@context".to_string(), proof_context.clone());
321 }
322
323 let mut candidates = vec![unsecured.clone()];
329 if unsecured.contains_key("signature") {
330 let mut without_sig = unsecured;
331 without_sig.remove("signature");
332 candidates.push(without_sig);
333 }
334
335 for candidate in candidates {
336 let doc_canon = match serde_json_canonicalizer::to_string(&Value::Object(candidate)) {
337 Ok(c) => c,
338 Err(_) => continue,
339 };
340 let mut hash_data = [0u8; 64];
341 hash_data[..32].copy_from_slice(&proof_hash);
342 hash_data[32..].copy_from_slice(&Sha256::digest(doc_canon.as_bytes()));
343
344 if verify_signature(suite, public_key, &hash_data, &signature_bytes)? {
345 return Ok(());
346 }
347 }
348 Err(Error::Invalid)
349}
350
351fn canonical(value: &Value) -> Result<String, Error> {
353 serde_json_canonicalizer::to_string(value)
354 .map_err(|error| Error::Malformed(format!("cannot canonicalize: {error}")))
355}
356
357pub fn generate_ed25519_key(rng: &mut impl CryptoRng) -> Result<String, Error> {
367 use ed25519_dalek::pkcs8::EncodePrivateKey as _;
368
369 let key = ed25519_dalek::SigningKey::generate(rng);
370 Ok(key
371 .to_pkcs8_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
372 .map_err(|error| Error::Key(format!("Ed25519 key as PKCS#8: {error}")))?
373 .to_string())
374}
375
376pub fn parse_ed25519_key(pem: &str) -> Result<([u8; 32], [u8; 32]), Error> {
381 use ed25519_dalek::pkcs8::DecodePrivateKey as _;
382
383 let key = ed25519_dalek::SigningKey::from_pkcs8_pem(pem)
384 .map_err(|_| Error::Key("not an Ed25519 private key in PKCS#8 PEM".into()))?;
385 Ok((key.to_bytes(), key.verifying_key().to_bytes()))
386}
387
388#[must_use]
390pub fn ed25519_public_key(seed: &[u8; 32]) -> [u8; 32] {
391 ed25519_dalek::SigningKey::from_bytes(seed)
392 .verifying_key()
393 .to_bytes()
394}
395
396#[must_use]
398pub fn generate_ed25519_seed(rng: &mut impl CryptoRng) -> [u8; 32] {
399 ed25519_dalek::SigningKey::generate(rng).to_bytes()
400}
401
402pub const DATA_INTEGRITY_CONTEXT: &str = "https://w3id.org/security/data-integrity/v1";
406
407#[must_use]
415pub fn with_data_integrity_context(mut document: Value) -> Option<Value> {
416 let object = document.as_object_mut()?;
417 match object.get_mut("@context") {
418 Some(Value::Array(entries)) => {
419 if !entries
420 .iter()
421 .any(|entry| entry.as_str() == Some(DATA_INTEGRITY_CONTEXT))
422 {
423 entries.push(Value::from(DATA_INTEGRITY_CONTEXT));
424 }
425 }
426 Some(Value::String(context)) if context == DATA_INTEGRITY_CONTEXT => {}
427 Some(existing) => {
428 let existing = existing.take();
429 object.insert(
430 "@context".to_string(),
431 Value::Array(vec![existing, Value::from(DATA_INTEGRITY_CONTEXT)]),
432 );
433 }
434 None => {
435 object.insert("@context".to_string(), Value::from(DATA_INTEGRITY_CONTEXT));
436 }
437 }
438 Some(document)
439}
440
441pub fn sign_object_integrity_proof(
463 document: &Value,
464 verification_method: &str,
465 signing_key: &[u8; 32],
466 now: i64,
467) -> Result<Value, Error> {
468 use ed25519_dalek::Signer as _;
469
470 let mut unsecured = document
471 .as_object()
472 .cloned()
473 .ok_or_else(|| Error::Malformed("document is not a JSON object".into()))?;
474 unsecured.remove("proof");
475 unsecured.remove("https://w3id.org/security#proof");
476
477 let created = chrono::DateTime::from_timestamp(now, 0)
478 .ok_or_else(|| Error::Malformed(format!("{now} is not a time")))?
479 .format("%Y-%m-%dT%H:%M:%SZ")
480 .to_string();
481
482 let mut proof_config = Map::new();
485 proof_config.insert(
486 "@context".to_string(),
487 unsecured.get("@context").cloned().unwrap_or(Value::Null),
488 );
489 proof_config.insert("type".to_string(), Value::from("DataIntegrityProof"));
490 proof_config.insert(
491 "cryptosuite".to_string(),
492 Value::from(Cryptosuite::EddsaJcs2022.as_str()),
493 );
494 proof_config.insert(
495 "verificationMethod".to_string(),
496 Value::from(verification_method),
497 );
498 proof_config.insert("proofPurpose".to_string(), Value::from("assertionMethod"));
499 proof_config.insert("created".to_string(), Value::from(created));
500
501 let proof_canon = canonical(&Value::Object(proof_config.clone()))?;
502 let doc_canon = canonical(&Value::Object(unsecured.clone()))?;
503
504 let mut hash_data = [0u8; 64];
505 hash_data[..32].copy_from_slice(&Sha256::digest(proof_canon.as_bytes()));
506 hash_data[32..].copy_from_slice(&Sha256::digest(doc_canon.as_bytes()));
507
508 let signature = ed25519_dalek::SigningKey::from_bytes(signing_key).sign(&hash_data);
509
510 let mut proof = proof_config;
511 proof.insert(
512 "proofValue".to_string(),
513 Value::from(encode_multibase_base58btc(&signature.to_bytes())),
514 );
515
516 let mut signed = unsecured;
517 signed.insert("proof".to_string(), Value::Object(proof));
518 Ok(Value::Object(signed))
519}
520
521#[must_use]
524pub fn encode_ed25519_multikey(public_key: &[u8; 32]) -> String {
525 let mut bytes = ED25519_PUB_MULTICODEC.to_vec();
526 bytes.extend_from_slice(public_key);
527 encode_multibase_base58btc(&bytes)
528}
529
530fn encode_multibase_base58btc(bytes: &[u8]) -> String {
531 format!("z{}", bs58::encode(bytes).into_string())
532}
533
534fn verify_signature(
536 suite: Cryptosuite,
537 public_key: &PublicKey,
538 message: &[u8],
539 signature: &[u8],
540) -> Result<bool, Error> {
541 match (suite, public_key) {
542 (Cryptosuite::EddsaJcs2022, PublicKey::Ed25519(key)) => {
543 let signature = ed25519_dalek::Signature::from_slice(signature)
544 .map_err(|e| Error::Malformed(format!("Ed25519 signature: {e}")))?;
545 let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(key)
546 .map_err(|e| Error::Key(format!("Ed25519 public key: {e}")))?;
547 Ok(verifying_key.verify(message, &signature).is_ok())
548 }
549 (Cryptosuite::Mldsa44Jcs2024, PublicKey::MlDsa44(key)) => {
550 let encoded = ml_dsa::EncodedVerifyingKey::<ml_dsa::MlDsa44>::try_from(&key[..])
551 .map_err(|_| Error::Key(format!("an ML-DSA-44 key of {} bytes", key.len())))?;
552 let verifying_key = ml_dsa::VerifyingKey::<ml_dsa::MlDsa44>::decode(&encoded);
553 let Ok(encoded_signature) =
554 ml_dsa::EncodedSignature::<ml_dsa::MlDsa44>::try_from(signature)
555 else {
556 return Ok(false);
557 };
558 let Some(signature) = ml_dsa::Signature::<ml_dsa::MlDsa44>::decode(&encoded_signature)
559 else {
560 return Ok(false);
561 };
562 Ok(verifying_key.verify_with_context(message, b"", &signature))
564 }
565 (suite, _) => Err(Error::WrongKey(format!(
566 "key type does not match cryptosuite {}",
567 suite.as_str()
568 ))),
569 }
570}
571
572pub fn decode_ed25519_multikey(multibase: &str) -> Result<[u8; 32], Error> {
578 let bytes = decode_multibase(multibase)?;
579 let key = bytes
580 .strip_prefix(&ED25519_PUB_MULTICODEC)
581 .ok_or_else(|| Error::Key("multikey is not ed25519-pub".into()))?;
582 ed25519_bytes(key)
583}
584
585fn ed25519_bytes(key: &[u8]) -> Result<[u8; 32], Error> {
586 key.try_into()
587 .map_err(|_| Error::Key(format!("an Ed25519 key of {} bytes", key.len())))
588}
589
590pub fn decode_mldsa44_multikey(multibase: &str) -> Result<Vec<u8>, Error> {
596 let bytes = decode_multibase(multibase)?;
597 let key = bytes
598 .strip_prefix(&MLDSA44_PUB_MULTICODEC)
599 .ok_or_else(|| Error::Key("multikey is not mldsa-44-pub".into()))?;
600 Ok(key.to_vec())
601}
602
603pub fn decode_multikey(multibase: &str) -> Result<PublicKey, Error> {
609 let bytes = decode_multibase(multibase)?;
610 if let Some(key) = bytes.strip_prefix(&ED25519_PUB_MULTICODEC) {
611 return Ok(PublicKey::Ed25519(Box::new(ed25519_bytes(key)?)));
612 }
613 if let Some(key) = bytes.strip_prefix(&MLDSA44_PUB_MULTICODEC) {
614 return Ok(PublicKey::MlDsa44(key.to_vec().into_boxed_slice()));
615 }
616 Err(Error::Unsupported("multikey type".into()))
617}
618
619fn decode_multibase(value: &str) -> Result<Vec<u8>, Error> {
626 let mut chars = value.chars();
627 match chars.next() {
628 Some('z') => bs58::decode(chars.as_str())
629 .into_vec()
630 .map_err(|e| Error::Malformed(format!("base58btc: {e}"))),
631 Some('u') => base64::Engine::decode(
632 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
633 chars.as_str(),
634 )
635 .map_err(|e| Error::Malformed(format!("base64url: {e}"))),
636 Some(other) => Err(Error::Unsupported(format!("multibase encoding {other:?}"))),
637 None => Err(Error::Malformed("empty multibase value".into())),
638 }
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644
645 #[test]
646 fn the_data_integrity_context_is_added_however_context_is_shaped() {
647 use serde_json::json;
648 let as_ = "https://www.w3.org/ns/activitystreams";
649 assert_eq!(
650 with_data_integrity_context(json!({"@context": as_})).unwrap()["@context"],
651 json!([as_, DATA_INTEGRITY_CONTEXT])
652 );
653 assert_eq!(
654 with_data_integrity_context(json!({"@context": [as_, {"a": "b"}]})).unwrap()["@context"],
655 json!([as_, {"a": "b"}, DATA_INTEGRITY_CONTEXT])
656 );
657 let already = json!({"@context": [as_, DATA_INTEGRITY_CONTEXT]});
658 assert_eq!(
659 with_data_integrity_context(already.clone()).unwrap(),
660 already
661 );
662 assert_eq!(
663 with_data_integrity_context(json!({})).unwrap()["@context"],
664 json!(DATA_INTEGRITY_CONTEXT)
665 );
666 assert_eq!(with_data_integrity_context(json!("x")), None);
667 }
668
669 #[test]
671 fn reads_and_writes_the_rfc_8410_public_key() {
672 let pem = "-----BEGIN PUBLIC KEY-----\n\
673 MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE=\n\
674 -----END PUBLIC KEY-----\n";
675 let key = PublicKey::from_spki_pem(pem).unwrap();
676 let PublicKey::Ed25519(raw) = &key else {
677 panic!("{key:?}");
678 };
679 assert_eq!(raw[..4], [0x19, 0xbf, 0x44, 0x09]);
680 assert_eq!(key.to_spki_pem().unwrap(), pem);
681 }
682
683 #[test]
686 fn an_ml_dsa_44_key_round_trips_through_pem() {
687 let key = PublicKey::MlDsa44(vec![7; 1312].into_boxed_slice());
688 let pem = key.to_spki_pem().unwrap();
689 assert!(pem.starts_with("-----BEGIN PUBLIC KEY-----\nMIIFMjALBglghkgBZQMEAxEDggUhAA"));
690 assert!(pem.lines().all(|line| line.len() <= 64));
691 assert_eq!(PublicKey::from_spki_pem(&pem).unwrap(), key);
692 let unwrapped: String = pem.lines().collect::<Vec<_>>().join("");
693 let unwrapped = unwrapped
694 .replace("-----BEGIN PUBLIC KEY-----", "-----BEGIN PUBLIC KEY-----\n")
695 .replace("-----END", "\n-----END");
696 assert_eq!(PublicKey::from_spki_pem(&unwrapped).unwrap(), key);
697 assert!(
698 PublicKey::MlDsa44(vec![7; 10].into_boxed_slice())
699 .to_spki_pem()
700 .is_err()
701 );
702 }
703
704 #[test]
705 fn what_is_not_an_ed25519_or_ml_dsa_44_key_is_refused() {
706 let rsa = include_str!("../../ojak/tests/fixtures/rfc9421_test_key_rsa_public.pem");
707 assert!(PublicKey::from_spki_pem(rsa).is_err());
708 assert!(PublicKey::from_spki_pem("not base64!").is_err());
709 }
710
711 const NOW: i64 = 1_759_000_000;
714
715 #[test]
719 fn verifies_the_fep_8b32_test_vector() {
720 let document = serde_json::json!({
721 "@context": [
722 "https://www.w3.org/ns/activitystreams",
723 "https://w3id.org/security/data-integrity/v2"
724 ],
725 "id": "https://server.example/activities/1",
726 "type": "Create",
727 "actor": "https://server.example/users/alice",
728 "object": {
729 "id": "https://server.example/objects/1",
730 "type": "Note",
731 "attributedTo": "https://server.example/users/alice",
732 "content": "Hello world",
733 "location": {
734 "type": "Place",
735 "longitude": -71.184902,
736 "latitude": 25.273962
737 }
738 },
739 "proof": {
740 "@context": [
741 "https://www.w3.org/ns/activitystreams",
742 "https://w3id.org/security/data-integrity/v2"
743 ],
744 "type": "DataIntegrityProof",
745 "cryptosuite": "eddsa-jcs-2022",
746 "verificationMethod": "https://server.example/users/alice#ed25519-key",
747 "proofPurpose": "assertionMethod",
748 "proofValue": "z42ffGu6AUKPCFcFPiabmUvnGLPJzC7e4DGWC52NUasSSH37UMa9c58tdgVszUcZfytxa4fQ5TYHaJENCxUDe9SdL",
749 "created": "2023-02-24T23:36:38Z"
750 }
751 });
752 let key = decode_multikey("z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2").unwrap();
753 let (proof, suite, method) = extract_integrity_proof(&document, NOW).unwrap();
754 assert_eq!(suite, Cryptosuite::EddsaJcs2022);
755 assert_eq!(method, "https://server.example/users/alice#ed25519-key");
756 verify_object_integrity_proof(&document, &proof, &key).unwrap();
757
758 let mut moved = document.clone();
759 moved["object"]["location"]["latitude"] = serde_json::json!(25.273963);
760 assert!(verify_object_integrity_proof(&moved, &proof, &key).is_err());
761 }
762
763 #[test]
768 fn canonical_json_sorts_keys_as_rfc_8785_does() {
769 let object = serde_json::json!({
770 "\u{20ac}": "Euro Sign",
771 "\r": "Carriage Return",
772 "\u{fb33}": "Hebrew Letter Dalet With Dagesh",
773 "1": "One",
774 "\u{1f600}": "Emoji: Grinning Face",
775 "\u{80}": "Control",
776 "\u{f6}": "Latin Small Letter O With Diaeresis"
777 });
778 let canonical = serde_json_canonicalizer::to_string(&object).unwrap();
779 let order: Vec<&str> = [
780 "Carriage Return",
781 "One",
782 "Control",
783 "Latin Small Letter O With Diaeresis",
784 "Euro Sign",
785 "Emoji: Grinning Face",
786 "Hebrew Letter Dalet With Dagesh",
787 ]
788 .into_iter()
789 .collect();
790 let mut positions: Vec<usize> = order
791 .iter()
792 .map(|value| canonical.find(value).unwrap())
793 .collect();
794 let sorted = {
795 let mut sorted = positions.clone();
796 sorted.sort_unstable();
797 sorted
798 };
799 assert_eq!(positions, sorted, "{canonical}");
800 positions.dedup();
801 assert_eq!(positions.len(), order.len());
802 }
803
804 fn w3c_signed_credential() -> Value {
810 serde_json::json!({
811 "@context": [
812 "https://www.w3.org/ns/credentials/v2",
813 "https://www.w3.org/ns/credentials/examples/v2"
814 ],
815 "id": "urn:uuid:58172aac-d8ba-11ed-83dd-0b3aef56cc33",
816 "type": ["VerifiableCredential", "AlumniCredential"],
817 "name": "Alumni Credential",
818 "description": "A minimum viable example of an Alumni Credential.",
819 "issuer": "https://vc.example/issuers/5678",
820 "validFrom": "2023-01-01T00:00:00Z",
821 "credentialSubject": {
822 "id": "did:example:abcdefgh",
823 "alumniOf": "The School of Examples"
824 },
825 "proof": {
826 "type": "DataIntegrityProof",
827 "cryptosuite": "eddsa-jcs-2022",
828 "created": "2023-02-24T23:36:38Z",
829 "verificationMethod": "did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2#z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2",
830 "proofPurpose": "assertionMethod",
831 "@context": [
832 "https://www.w3.org/ns/credentials/v2",
833 "https://www.w3.org/ns/credentials/examples/v2"
834 ],
835 "proofValue": "z2HnFSSPPBzR36zdDgK8PbEHeXbR56YF24jwMpt3R1eHXQzJDMWS93FCzpvJpwTWd3GAVFuUfjoJdcnTMuVor51aX"
836 }
837 })
838 }
839
840 #[test]
841 fn verifies_w3c_eddsa_jcs_2022_vector() {
842 let doc = w3c_signed_credential();
843 let (proof, suite, vm) = extract_integrity_proof(&doc, NOW).expect("proof present");
844 assert_eq!(suite, Cryptosuite::EddsaJcs2022);
845 assert!(vm.starts_with("did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2"));
846
847 let key = decode_multikey("z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2")
849 .expect("decode multikey");
850
851 verify_object_integrity_proof(&doc, &proof, &key).expect("valid proof must verify");
852 }
853
854 #[test]
855 fn rejects_tampered_document() {
856 let mut doc = w3c_signed_credential();
857 doc["issuer"] = serde_json::json!("https://vc.example/issuers/evil");
858 let (proof, _, _) = extract_integrity_proof(&doc, NOW).expect("proof present");
859 let key = decode_multikey("z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2")
860 .expect("decode multikey");
861 assert!(verify_object_integrity_proof(&doc, &proof, &key).is_err());
862 }
863
864 #[test]
871 fn verifies_w3c_mldsa44_jcs_2024_vector() {
872 let doc: Value =
873 serde_json::from_str(include_str!("../tests/fixtures/w3c_mldsa44_jcs_2024.json"))
874 .expect("parse vector");
875
876 let (proof, suite, vm) = extract_integrity_proof(&doc, NOW).expect("proof present");
877 assert_eq!(suite, Cryptosuite::Mldsa44Jcs2024);
878
879 let multibase = vm
881 .strip_prefix("did:key:")
882 .and_then(|rest| rest.split('#').next())
883 .expect("did:key verification method");
884 let key = decode_multikey(multibase).expect("decode multikey");
885 assert!(
886 matches!(&key, PublicKey::MlDsa44(bytes) if bytes.len() == 1312),
887 "expected a 1312-byte ML-DSA-44 key"
888 );
889
890 verify_object_integrity_proof(&doc, &proof, &key).expect("valid proof must verify");
891 }
892
893 #[test]
894 fn rejects_a_tampered_mldsa44_document() {
895 let mut doc: Value =
896 serde_json::from_str(include_str!("../tests/fixtures/w3c_mldsa44_jcs_2024.json"))
897 .expect("parse vector");
898 doc["issuer"] = serde_json::json!("did:example:evil");
899
900 let (proof, _, vm) = extract_integrity_proof(&doc, NOW).expect("proof present");
901 let multibase = vm
902 .strip_prefix("did:key:")
903 .and_then(|rest| rest.split('#').next())
904 .unwrap();
905 let key = decode_multikey(multibase).unwrap();
906 assert!(verify_object_integrity_proof(&doc, &proof, &key).is_err());
907 }
908
909 #[test]
913 fn refuses_a_key_of_the_wrong_type() {
914 let doc: Value =
915 serde_json::from_str(include_str!("../tests/fixtures/w3c_mldsa44_jcs_2024.json"))
916 .expect("parse vector");
917 let (proof, _, _) = extract_integrity_proof(&doc, NOW).expect("proof present");
918
919 let ed25519 = decode_multikey("z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2").unwrap();
920 let err = verify_object_integrity_proof(&doc, &proof, &ed25519).unwrap_err();
921 assert!(matches!(err, Error::WrongKey(_)), "{err}");
922 }
923
924 #[test]
925 fn decodes_both_multibase_encodings() {
926 assert_eq!(decode_multibase("zCn8eVZg").unwrap(), b"hello");
928 assert_eq!(decode_multibase("uaGVsbG8").unwrap(), b"hello");
929 assert!(decode_multibase("Qhello").is_err());
930 assert!(decode_multibase("").is_err());
931 }
932
933 #[test]
934 fn ignores_an_expired_proof() {
935 let mut doc = w3c_signed_credential();
936 doc["proof"]["expires"] = serde_json::json!("2020-01-01T00:00:00Z");
937 assert!(
938 extract_integrity_proof(&doc, NOW).is_none(),
939 "an expired proof must not be offered for verification"
940 );
941
942 doc["proof"]["expires"] = serde_json::json!("2999-01-01T00:00:00Z");
943 assert!(extract_integrity_proof(&doc, NOW).is_some());
944 }
945
946 #[test]
950 fn signs_a_proof_its_own_verifier_accepts() {
951 use ed25519_dalek::SigningKey;
952
953 let seed = [7u8; 32];
954 let public = SigningKey::from_bytes(&seed).verifying_key().to_bytes();
955 let multikey = encode_ed25519_multikey(&public);
956
957 let document = serde_json::json!({
958 "@context": "https://www.w3.org/ns/activitystreams",
959 "id": "https://local.example/activities/1",
960 "type": "Create",
961 "actor": "https://local.example/users/alice",
962 "object": {"id": "https://local.example/notes/1", "type": "Note", "content": "hi"},
963 });
964
965 let vm = "https://local.example/users/alice#ed25519-key";
966 let signed = sign_object_integrity_proof(&document, vm, &seed, NOW).unwrap();
967
968 let (proof, suite, found_vm) =
969 extract_integrity_proof(&signed, NOW).expect("proof attached");
970 assert_eq!(suite, Cryptosuite::EddsaJcs2022);
971 assert_eq!(found_vm, vm);
972
973 let key = decode_multikey(&multikey).unwrap();
974 verify_object_integrity_proof(&signed, &proof, &key).expect("own proof must verify");
975 }
976
977 #[test]
980 fn a_signed_document_cannot_be_altered() {
981 use ed25519_dalek::SigningKey;
982
983 let seed = [9u8; 32];
984 let public = SigningKey::from_bytes(&seed).verifying_key().to_bytes();
985 let multikey = encode_ed25519_multikey(&public);
986
987 let document = serde_json::json!({
988 "@context": "https://www.w3.org/ns/activitystreams",
989 "id": "https://local.example/activities/2",
990 "type": "Create",
991 "actor": "https://local.example/users/alice",
992 });
993 let mut signed = sign_object_integrity_proof(
994 &document,
995 "https://local.example/users/alice#k",
996 &seed,
997 NOW,
998 )
999 .unwrap();
1000 signed["actor"] = serde_json::json!("https://local.example/users/mallory");
1001
1002 let (proof, _, _) = extract_integrity_proof(&signed, NOW).unwrap();
1003 let key = decode_multikey(&multikey).unwrap();
1004 assert!(verify_object_integrity_proof(&signed, &proof, &key).is_err());
1005 }
1006
1007 #[test]
1012 fn generates_a_pem_key_that_parses_back() {
1013 let pem = generate_ed25519_key(&mut rand::rng()).unwrap();
1014 assert!(pem.starts_with("-----BEGIN PRIVATE KEY-----"));
1015
1016 let (seed, public) = parse_ed25519_key(&pem).unwrap();
1017 assert_eq!(
1018 ed25519_dalek::SigningKey::from_bytes(&seed)
1019 .verifying_key()
1020 .to_bytes(),
1021 public,
1022 "the public half must belong to the seed"
1023 );
1024
1025 let document = serde_json::json!({
1027 "@context": "https://www.w3.org/ns/activitystreams",
1028 "type": "Create",
1029 });
1030 let signed =
1031 sign_object_integrity_proof(&document, "https://x.test/a#k", &seed, NOW).unwrap();
1032 let (proof, _, _) = extract_integrity_proof(&signed, NOW).unwrap();
1033 let key = decode_multikey(&encode_ed25519_multikey(&public)).unwrap();
1034 verify_object_integrity_proof(&signed, &proof, &key).unwrap();
1035
1036 assert_ne!(
1037 pem,
1038 generate_ed25519_key(&mut rand::rng()).unwrap(),
1039 "keys must differ"
1040 );
1041 }
1042
1043 #[test]
1044 fn a_published_multikey_round_trips() {
1045 let public = [3u8; 32];
1046 let multikey = encode_ed25519_multikey(&public);
1047 assert!(multikey.starts_with('z'));
1048 assert_eq!(decode_ed25519_multikey(&multikey).unwrap(), public);
1049
1050 match decode_multikey(&multikey).unwrap() {
1051 PublicKey::Ed25519(bytes) => assert_eq!(*bytes, public),
1052 PublicKey::MlDsa44(_) => panic!("decoded as the wrong key type"),
1053 }
1054 }
1055
1056 #[test]
1057 fn missing_proof_is_none() {
1058 let doc = serde_json::json!({ "type": "Note", "content": "hi" });
1059 assert!(extract_integrity_proof(&doc, NOW).is_none());
1060 }
1061}