1use serde_json::Value;
8use sha2::{Digest, Sha256};
9
10use crate::error::{NormativeReason, ValidationError};
11use crate::jcs::{self, JcsError};
12
13pub(crate) fn sha256_hex(bytes: &[u8]) -> String {
17 let hash = Sha256::digest(bytes);
18 hash.iter().map(|b| format!("{b:02x}")).collect()
19}
20
21pub fn registration_digest(registrations_json: &str) -> Result<String, JcsError> {
26 let parsed = jcs::parse_strict(registrations_json)?;
27 digest_unique_registrations(&parsed)
28}
29
30fn digest_unique_registrations(registrations: &Value) -> Result<String, JcsError> {
31 if !registrations.is_array() {
32 return Err(JcsError::Unsupported);
33 }
34 let canonical = jcs::encode_unique(registrations)?;
35 Ok(sha256_hex(&canonical))
36}
37
38pub(crate) fn verify_registration_digest(
41 registrations: &Value,
42 claimed: &str,
43) -> Result<(), ValidationError> {
44 let got = digest_unique_registrations(registrations).map_err(|_| {
45 ValidationError::normative(
46 "/registration_digest",
47 "rfc8785_sha256",
48 NormativeReason::RegistrationDigestMismatch,
49 )
50 })?;
51 if got != claimed {
52 return Err(ValidationError::normative(
53 "/registration_digest/value",
54 "digest_mismatch",
55 NormativeReason::RegistrationDigestMismatch,
56 ));
57 }
58 Ok(())
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn empty_input_matches_known_vector() {
67 assert_eq!(
68 sha256_hex(b""),
69 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
70 );
71 }
72
73 #[test]
74 fn example_registration_digest_matches_pin() {
75 let registrations = r#"[{
76 "registration_id": "reg:job-complete-1",
77 "method_id": "job_complete",
78 "subject_kind": "service_job",
79 "subject_id": "job:transcribe-1",
80 "baseline_policy": "latest",
81 "required": true,
82 "source_instance_ref": "source:provider-a",
83 "predicate_ref": "pred:job-complete",
84 "capability_ref": "cap:wait",
85 "lease_expires_at": "2026-08-16T00:00:00Z",
86 "bounds": {
87 "max_events": 50,
88 "max_bytes": 524288
89 }
90 }]"#;
91 assert_eq!(
92 registration_digest(registrations).unwrap(),
93 "cb5c843991542fca328ea9916d810e601f83429a496bd94986a1e7b5cfbeb7c1"
94 );
95 }
96
97 #[test]
98 fn registration_digest_rejects_duplicate_keys_in_raw_array() {
99 let err = registration_digest(r#"[{"a":1,"a":2}]"#).unwrap_err();
100 assert!(matches!(err, JcsError::DuplicateKey));
101 }
102}