|
1 | 1 | package credential |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "crypto/subtle" |
4 | 5 | "errors" |
5 | 6 | "fmt" |
| 7 | + "strings" |
6 | 8 | "time" |
7 | 9 | ) |
8 | 10 |
|
|
20 | 22 | ErrIssuerNotApproved = errors.New("credential: issuer not approved") |
21 | 23 | // ErrNoProof is returned when a credential has no proof. |
22 | 24 | ErrNoProof = errors.New("credential: no proof") |
| 25 | + // ErrInvalidProof is returned when a credential proof is malformed or invalid. |
| 26 | + ErrInvalidProof = errors.New("credential: invalid proof") |
| 27 | + // ErrIssuerKeyNotTrusted is returned when no trusted proof key is configured for the issuer. |
| 28 | + ErrIssuerKeyNotTrusted = errors.New("credential: issuer proof key not trusted") |
23 | 29 | // ErrSchemaNotFound is returned when the credential schema is not found. |
24 | 30 | ErrSchemaNotFound = errors.New("credential: schema not found") |
25 | 31 | ) |
@@ -49,17 +55,36 @@ type VerificationResult struct { |
49 | 55 | // Verifier verifies verifiable credentials against on-chain status, |
50 | 56 | // expiry, and issuer authorization. |
51 | 57 | type Verifier struct { |
52 | | - schemas SchemaRegistry |
53 | | - issuers IssuerRegistry |
54 | | - now func() time.Time |
| 58 | + schemas SchemaRegistry |
| 59 | + issuers IssuerRegistry |
| 60 | + trustedIssuerKeys map[string]string |
| 61 | + now func() time.Time |
55 | 62 | } |
56 | 63 |
|
57 | 64 | // NewVerifier creates a new credential verifier with the given registries. |
58 | 65 | func NewVerifier(schemas SchemaRegistry, issuers IssuerRegistry) *Verifier { |
59 | 66 | return &Verifier{ |
60 | | - schemas: schemas, |
61 | | - issuers: issuers, |
62 | | - now: time.Now, |
| 67 | + schemas: schemas, |
| 68 | + issuers: issuers, |
| 69 | + trustedIssuerKeys: make(map[string]string), |
| 70 | + now: time.Now, |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +// NewVerifierWithTrustedIssuerKeys creates a verifier with issuer-scoped proof keys. |
| 75 | +func NewVerifierWithTrustedIssuerKeys(schemas SchemaRegistry, issuers IssuerRegistry, keys map[string]string) *Verifier { |
| 76 | + v := NewVerifier(schemas, issuers) |
| 77 | + v.SetTrustedIssuerKeys(keys) |
| 78 | + return v |
| 79 | +} |
| 80 | + |
| 81 | +// SetTrustedIssuerKeys replaces the issuer DID to proof-key mapping used to |
| 82 | +// authenticate credential proofs. Keys are copied so callers can safely mutate |
| 83 | +// their input map after configuration. |
| 84 | +func (v *Verifier) SetTrustedIssuerKeys(keys map[string]string) { |
| 85 | + v.trustedIssuerKeys = make(map[string]string, len(keys)) |
| 86 | + for issuer, key := range keys { |
| 87 | + v.trustedIssuerKeys[issuer] = key |
63 | 88 | } |
64 | 89 | } |
65 | 90 |
|
@@ -145,9 +170,63 @@ func (v *Verifier) checkProof(cred *VerifiableCredential, result *VerificationRe |
145 | 170 | result.Valid = false |
146 | 171 | result.Checks["proof"] = false |
147 | 172 | result.Errors = append(result.Errors, ErrNoProof.Error()) |
148 | | - } else { |
149 | | - result.Checks["proof"] = true |
| 173 | + return |
| 174 | + } |
| 175 | + |
| 176 | + proofErrors := make([]string, 0) |
| 177 | + proof := cred.Proof |
| 178 | + |
| 179 | + if proof.Type != "ZeroIDCredentialProof2026" { |
| 180 | + proofErrors = append(proofErrors, "credential: unsupported proof type") |
| 181 | + } |
| 182 | + if proof.ProofPurpose != "assertionMethod" { |
| 183 | + proofErrors = append(proofErrors, "credential: unsupported proof purpose") |
| 184 | + } |
| 185 | + if proof.Created.IsZero() { |
| 186 | + proofErrors = append(proofErrors, "credential: missing proof creation time") |
| 187 | + } |
| 188 | + if proof.VerificationMethod == "" { |
| 189 | + proofErrors = append(proofErrors, "credential: missing verification method") |
| 190 | + } else if !strings.HasPrefix(proof.VerificationMethod, cred.Issuer+"#") { |
| 191 | + proofErrors = append(proofErrors, "credential: verification method is not controlled by issuer") |
| 192 | + } |
| 193 | + if proof.ProofValue == "" { |
| 194 | + proofErrors = append(proofErrors, "credential: missing proof value") |
150 | 195 | } |
| 196 | + if proof.MerkleRoot == "" { |
| 197 | + proofErrors = append(proofErrors, "credential: missing subject Merkle root") |
| 198 | + } |
| 199 | + |
| 200 | + subjectRoot, err := ComputeCredentialSubjectMerkleRoot(cred.CredentialSubject) |
| 201 | + if err != nil { |
| 202 | + proofErrors = append(proofErrors, err.Error()) |
| 203 | + } else if proof.MerkleRoot != "" && proof.MerkleRoot != subjectRoot { |
| 204 | + proofErrors = append(proofErrors, "credential: subject does not match proof root") |
| 205 | + } |
| 206 | + |
| 207 | + issuerKey := v.trustedIssuerKeys[cred.Issuer] |
| 208 | + if issuerKey == "" { |
| 209 | + proofErrors = append(proofErrors, ErrIssuerKeyNotTrusted.Error()) |
| 210 | + } else if proof.ProofValue != "" && proof.MerkleRoot != "" { |
| 211 | + payload, err := BuildCredentialProofPayload(cred, proof.MerkleRoot) |
| 212 | + if err != nil { |
| 213 | + proofErrors = append(proofErrors, err.Error()) |
| 214 | + } else { |
| 215 | + expected := ComputeCredentialProofValue(issuerKey, payload) |
| 216 | + if subtle.ConstantTimeCompare([]byte(proof.ProofValue), []byte(expected)) != 1 { |
| 217 | + proofErrors = append(proofErrors, "credential: proof signature is invalid") |
| 218 | + } |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + if len(proofErrors) > 0 { |
| 223 | + result.Valid = false |
| 224 | + result.Checks["proof"] = false |
| 225 | + result.Errors = append(result.Errors, proofErrors...) |
| 226 | + return |
| 227 | + } |
| 228 | + |
| 229 | + result.Checks["proof"] = true |
151 | 230 | } |
152 | 231 |
|
153 | 232 | func (v *Verifier) checkIssuer(cred *VerifiableCredential, result *VerificationResult) error { |
|
0 commit comments