Generic verify over a JSON-LD value — the same three checks as the typed path: (1) the content hash recomputes over `hashing_view`, (2) the signature verifies over `jcs(signing_view)`, (3) the proof's verificationMethod DID belongs to this key.
(value: &Value, public_key: &PublicKey)
| 155 | /// verifies over `jcs(signing_view)`, (3) the proof's verificationMethod DID |
| 156 | /// belongs to this key. |
| 157 | pub fn verify_value(value: &Value, public_key: &PublicKey) -> Result<()> { |
| 158 | let obj = value |
| 159 | .as_object() |
| 160 | .ok_or_else(|| CanonicalError::Proof("value is not a JSON object".into()))?; |
| 161 | |
| 162 | // 1. Content hash integrity. |
| 163 | let expected = obj |
| 164 | .get(PROP_CONTENT_HASH) |
| 165 | .and_then(Value::as_str) |
| 166 | .ok_or_else(|| CanonicalError::Proof("node carries no contentHash".into()))?; |
| 167 | let actual = hash::content_hash(&hashing_view(value)); |
| 168 | if expected != actual { |
| 169 | return Err(CanonicalError::HashMismatch { |
| 170 | expected: expected.to_string(), |
| 171 | actual, |
| 172 | }); |
| 173 | } |
| 174 | |
| 175 | // 2. Signature over the canonical signing bytes. |
| 176 | let proof: Proof = obj |
| 177 | .get(PROP_PROOF) |
| 178 | .cloned() |
| 179 | .and_then(|p| serde_json::from_value(p).ok()) |
| 180 | .ok_or_else(|| CanonicalError::Proof("node carries no proof".into()))?; |
| 181 | |
| 182 | // The proof's own metadata is stripped by `signing_view`, so it is not |
| 183 | // covered by the signature. Enforce the suite/purpose/type we expect here, |
| 184 | // otherwise a stored node's declared cryptosuite is silently mutable. |
| 185 | if proof.type_ != PROOF_TYPE |
| 186 | || proof.cryptosuite != CRYPTOSUITE |
| 187 | || proof.proof_purpose != PROOF_PURPOSE |
| 188 | { |
| 189 | return Err(CanonicalError::Verification(format!( |
| 190 | "unexpected proof metadata: type='{}' cryptosuite='{}' proofPurpose='{}'", |
| 191 | proof.type_, proof.cryptosuite, proof.proof_purpose |
| 192 | ))); |
| 193 | } |
| 194 | |
| 195 | let signature = decode_proof_value(&proof.proof_value)?; |
| 196 | let signing_bytes = jcs::canonicalize(&signing_view(value)).into_bytes(); |
| 197 | signature |
| 198 | .verify(&signing_bytes, public_key) |
| 199 | .map_err(|e| CanonicalError::Verification(e.to_string()))?; |
| 200 | |
| 201 | // 3. The proof's verificationMethod DID must belong to this key. |
| 202 | let did = did::did_from_verification_method(&proof.verification_method); |
| 203 | if !did::did_matches_public_key(did, public_key) { |
| 204 | return Err(CanonicalError::Verification( |
| 205 | "verificationMethod DID does not match the public key".into(), |
| 206 | )); |
| 207 | } |
| 208 | Ok(()) |
| 209 | } |
| 210 | |
| 211 | /// Produce a fully attested Intent node — a thin wrapper over |
| 212 | /// [`attest_value`] so hash and signature can never drift from any other node |