|
| 1 | +<!-- Copyright (c) Microsoft Corporation. Licensed under the MIT License. --> |
| 2 | + |
| 3 | +# did_x509 |
| 4 | + |
| 5 | +DID:x509 identifier parsing, building, validation, and resolution. |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +This crate implements the [DID:x509 method specification](https://github.com/nicosResworworking-group/did-x509), |
| 10 | +which creates Decentralized Identifiers (DIDs) from X.509 certificate chains. |
| 11 | +A DID:x509 identifier binds a trust anchor (CA certificate fingerprint) to one |
| 12 | +or more policy constraints (EKU, subject, SAN, Fulcio issuer) that must be |
| 13 | +satisfied by the leaf certificate in a presented chain. |
| 14 | + |
| 15 | +Key capabilities: |
| 16 | + |
| 17 | +- **Parsing** — zero-copy-friendly DID:x509 string parsing with full validation |
| 18 | +- **Building** — fluent construction of DID:x509 identifiers from certificate chains |
| 19 | +- **Validation** — validate DID:x509 identifiers against certificate chains |
| 20 | +- **Resolution** — resolve DID:x509 identifiers to W3C DID Documents with JWK public keys |
| 21 | +- **Policy validators** — EKU, Subject DN, SAN (email/dns/uri/dn), and Fulcio issuer |
| 22 | +- **FFI** — complete C/C++ projection via the companion `did_x509_ffi` crate |
| 23 | + |
| 24 | +## DID:x509 Format |
| 25 | + |
| 26 | +``` |
| 27 | +did:x509:0:sha256:<base64url_CA_fingerprint>::eku:<oid1>:<oid2>::subject:CN:<value> |
| 28 | +│ │ │ │ │ │ |
| 29 | +│ │ │ │ │ └─ Subject policy |
| 30 | +│ │ │ │ └─ EKU policy |
| 31 | +│ │ │ └─ Base64url-encoded CA certificate fingerprint |
| 32 | +│ │ └─ Hash algorithm (sha256, sha384, sha512) |
| 33 | +│ └─ Version (always 0) |
| 34 | +└─ DID method prefix |
| 35 | +``` |
| 36 | + |
| 37 | +Multiple policies are separated by `::` (double colon). Within a policy, values |
| 38 | +are separated by `:` (single colon). Special characters are percent-encoded. |
| 39 | + |
| 40 | +## Architecture |
| 41 | + |
| 42 | +``` |
| 43 | +┌─────────────────────────────────────────────────┐ |
| 44 | +│ did_x509 │ |
| 45 | +├─────────────┬───────────────┬───────────────────┤ |
| 46 | +│ parsing/ │ builder │ validator │ |
| 47 | +│ ├ Parser │ ├ build() │ ├ validate() │ |
| 48 | +│ ├ Percent │ ├ build_ │ └ policy match │ |
| 49 | +│ │ encode │ │ sha256() │ │ |
| 50 | +│ └ Percent │ ├ build_ ├───────────────────┤ |
| 51 | +│ decode │ │ from_ │ resolver │ |
| 52 | +│ │ │ chain() │ ├ resolve() │ |
| 53 | +│ │ └ build_ │ ├ RSA→JWK │ |
| 54 | +│ │ from_ │ └ EC→JWK │ |
| 55 | +│ │ chain_ │ │ |
| 56 | +│ │ with_eku()│ │ |
| 57 | +├─────────────┴───────────────┴───────────────────┤ |
| 58 | +│ models/ │ |
| 59 | +│ ├ DidX509ParsedIdentifier │ |
| 60 | +│ ├ DidX509Policy (Eku, Subject, San, Fulcio) │ |
| 61 | +│ ├ DidX509ValidationResult │ |
| 62 | +│ ├ SanType (Email, Dns, Uri, Dn) │ |
| 63 | +│ ├ CertificateInfo, X509Name │ |
| 64 | +│ └ SubjectAlternativeName │ |
| 65 | +├─────────────────────────────────────────────────┤ |
| 66 | +│ policy_validators │ x509_extensions │ |
| 67 | +│ ├ validate_eku() │ ├ extract_eku_oids() │ |
| 68 | +│ ├ validate_subject()│ ├ extract_extended_ │ |
| 69 | +│ ├ validate_san() │ │ key_usage() │ |
| 70 | +│ └ validate_fulcio() │ ├ extract_fulcio_issuer()│ |
| 71 | +│ │ └ extract_san() │ |
| 72 | +├──────────────────────┴──────────────────────────┤ |
| 73 | +│ did_document │ constants │ |
| 74 | +│ ├ DidDocument │ ├ OID constants │ |
| 75 | +│ ├ Verification │ ├ Attribute labels │ |
| 76 | +│ │ Method │ └ oid_to_attribute_ │ |
| 77 | +│ └ to_json() │ label() │ |
| 78 | +└─────────────────────────────────────────────────┘ |
| 79 | + │ |
| 80 | + ▼ |
| 81 | + x509-parser (DER parsing) |
| 82 | + sha2 (fingerprint hashing) |
| 83 | + serde/serde_json (DID Document serialization) |
| 84 | +``` |
| 85 | + |
| 86 | +## Modules |
| 87 | + |
| 88 | +| Module | Description | |
| 89 | +|--------|-------------| |
| 90 | +| `parsing` | `DidX509Parser::parse()` — parses DID:x509 strings into structured identifiers | |
| 91 | +| `builder` | `DidX509Builder` — constructs DID:x509 strings from certificates and policies | |
| 92 | +| `validator` | `DidX509Validator::validate()` — validates DIDs against certificate chains | |
| 93 | +| `resolver` | `DidX509Resolver::resolve()` — resolves DIDs to W3C DID Documents | |
| 94 | +| `models` | Core types: `DidX509ParsedIdentifier`, `DidX509Policy`, `DidX509ValidationResult` | |
| 95 | +| `policy_validators` | Per-policy validation: EKU, Subject DN, SAN, Fulcio issuer | |
| 96 | +| `x509_extensions` | X.509 extension extraction utilities (EKU, SAN, Fulcio) | |
| 97 | +| `san_parser` | Subject Alternative Name parsing from certificates | |
| 98 | +| `did_document` | W3C DID Document model with JWK-based verification methods | |
| 99 | +| `constants` | DID:x509 format constants, well-known OIDs, attribute labels | |
| 100 | +| `error` | `DidX509Error` with 24 descriptive variants | |
| 101 | + |
| 102 | +## Key Types |
| 103 | + |
| 104 | +### `DidX509Parser` |
| 105 | + |
| 106 | +Parses a DID:x509 string into its structured components with full validation |
| 107 | +of version, hash algorithm, fingerprint length, and policy syntax. |
| 108 | + |
| 109 | +```rust |
| 110 | +use did_x509::DidX509Parser; |
| 111 | + |
| 112 | +let did = "did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkExample::eku:1.3.6.1.5.5.7.3.3"; |
| 113 | +let parsed = DidX509Parser::parse(did).unwrap(); |
| 114 | + |
| 115 | +assert_eq!(parsed.hash_algorithm, "sha256"); |
| 116 | +assert!(parsed.has_eku_policy()); |
| 117 | +assert_eq!(parsed.policies.len(), 1); |
| 118 | +``` |
| 119 | + |
| 120 | +### `DidX509Builder` |
| 121 | + |
| 122 | +Constructs DID:x509 identifier strings from CA certificates and policy constraints. |
| 123 | + |
| 124 | +```rust |
| 125 | +use did_x509::{DidX509Builder, DidX509Policy}; |
| 126 | + |
| 127 | +// Build from a CA certificate with EKU policy |
| 128 | +let did = DidX509Builder::build_sha256( |
| 129 | + ca_cert_der, |
| 130 | + &[DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".into()])], |
| 131 | +).unwrap(); |
| 132 | + |
| 133 | +// Build from a certificate chain (automatically uses root as CA) |
| 134 | +let did = DidX509Builder::build_from_chain( |
| 135 | + &[leaf_der, intermediate_der, root_der], |
| 136 | + &[DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".into()])], |
| 137 | +).unwrap(); |
| 138 | + |
| 139 | +// Build with EKU extracted from the leaf certificate |
| 140 | +let did = DidX509Builder::build_from_chain_with_eku( |
| 141 | + &[leaf_der, intermediate_der, root_der], |
| 142 | +).unwrap(); |
| 143 | +``` |
| 144 | + |
| 145 | +### `DidX509Validator` |
| 146 | + |
| 147 | +Validates a DID:x509 identifier against a certificate chain by verifying the |
| 148 | +CA fingerprint matches a certificate in the chain and all policy constraints |
| 149 | +are satisfied by the leaf certificate. |
| 150 | + |
| 151 | +```rust |
| 152 | +use did_x509::DidX509Validator; |
| 153 | + |
| 154 | +let result = DidX509Validator::validate(did_string, &[leaf_der, root_der]).unwrap(); |
| 155 | + |
| 156 | +if result.is_valid { |
| 157 | + println!("CA matched at chain index: {}", result.matched_ca_index.unwrap()); |
| 158 | +} else { |
| 159 | + for error in &result.errors { |
| 160 | + eprintln!("Validation error: {}", error); |
| 161 | + } |
| 162 | +} |
| 163 | +``` |
| 164 | + |
| 165 | +### `DidX509Resolver` |
| 166 | + |
| 167 | +Resolves a DID:x509 identifier to a W3C DID Document containing the leaf |
| 168 | +certificate's public key in JWK format. Performs full validation first. |
| 169 | + |
| 170 | +```rust |
| 171 | +use did_x509::DidX509Resolver; |
| 172 | + |
| 173 | +let doc = DidX509Resolver::resolve(did_string, &[leaf_der, root_der]).unwrap(); |
| 174 | + |
| 175 | +// DID Document contains the public key as a JsonWebKey2020 verification method |
| 176 | +assert_eq!(doc.id, did_string); |
| 177 | +assert_eq!(doc.verification_method[0].type_, "JsonWebKey2020"); |
| 178 | + |
| 179 | +// Serialize to JSON |
| 180 | +let json = doc.to_json(true).unwrap(); |
| 181 | +``` |
| 182 | + |
| 183 | +### `DidX509Policy` |
| 184 | + |
| 185 | +Policy constraints that can be included in a DID:x509 identifier: |
| 186 | + |
| 187 | +```rust |
| 188 | +use did_x509::{DidX509Policy, SanType}; |
| 189 | + |
| 190 | +// Extended Key Usage — OID list |
| 191 | +let eku = DidX509Policy::Eku(vec!["1.3.6.1.5.5.7.3.3".into()]); |
| 192 | + |
| 193 | +// Subject Distinguished Name — key-value pairs |
| 194 | +let subject = DidX509Policy::Subject(vec![ |
| 195 | + ("CN".to_string(), "example.com".to_string()), |
| 196 | + ("O".to_string(), "Example Corp".to_string()), |
| 197 | +]); |
| 198 | + |
| 199 | +// Subject Alternative Name — typed value |
| 200 | +let san = DidX509Policy::San(SanType::Email, "user@example.com".to_string()); |
| 201 | + |
| 202 | +// Fulcio issuer — OIDC issuer URL |
| 203 | +let fulcio = DidX509Policy::FulcioIssuer("https://accounts.google.com".to_string()); |
| 204 | +``` |
| 205 | + |
| 206 | +### `DidX509Error` |
| 207 | + |
| 208 | +Comprehensive error type with 24 variants covering every failure mode: |
| 209 | + |
| 210 | +| Category | Variants | |
| 211 | +|----------|----------| |
| 212 | +| Format | `EmptyDid`, `InvalidPrefix`, `InvalidFormat`, `MissingPolicies` | |
| 213 | +| Version | `UnsupportedVersion` | |
| 214 | +| Hash | `UnsupportedHashAlgorithm`, `EmptyFingerprint`, `FingerprintLengthMismatch`, `InvalidFingerprintChars` | |
| 215 | +| Policy syntax | `EmptyPolicy`, `InvalidPolicyFormat`, `EmptyPolicyName`, `EmptyPolicyValue` | |
| 216 | +| EKU | `InvalidEkuOid` | |
| 217 | +| Subject | `InvalidSubjectPolicyComponents`, `EmptySubjectPolicyKey`, `DuplicateSubjectPolicyKey` | |
| 218 | +| SAN | `InvalidSanPolicyFormat`, `InvalidSanType` | |
| 219 | +| Fulcio | `EmptyFulcioIssuer` | |
| 220 | +| Chain | `InvalidChain`, `CertificateParseError`, `NoCaMatch` | |
| 221 | +| Validation | `PolicyValidationFailed`, `ValidationFailed` | |
| 222 | +| Encoding | `PercentDecodingError`, `InvalidHexCharacter` | |
| 223 | + |
| 224 | +## Supported Hash Algorithms |
| 225 | + |
| 226 | +| Algorithm | Fingerprint Length | Constant | |
| 227 | +|-----------|--------------------|----------| |
| 228 | +| SHA-256 | 32 bytes (43 base64url chars) | `HASH_ALGORITHM_SHA256` | |
| 229 | +| SHA-384 | 48 bytes (64 base64url chars) | `HASH_ALGORITHM_SHA384` | |
| 230 | +| SHA-512 | 64 bytes (86 base64url chars) | `HASH_ALGORITHM_SHA512` | |
| 231 | + |
| 232 | +## Supported Policies |
| 233 | + |
| 234 | +| Policy | DID Syntax | Description | |
| 235 | +|--------|-----------|-------------| |
| 236 | +| EKU | `eku:<oid1>:<oid2>` | Extended Key Usage OIDs must all be present on the leaf cert | |
| 237 | +| Subject | `subject:<attr>:<val>` | Subject DN attributes must match (CN, O, OU, L, ST, C, STREET) | |
| 238 | +| SAN | `san:<type>:<value>` | Subject Alternative Name must match (email, dns, uri, dn) | |
| 239 | +| Fulcio Issuer | `fulcio-issuer:<url>` | Fulcio OIDC issuer extension must match | |
| 240 | + |
| 241 | +## FFI Support |
| 242 | + |
| 243 | +The companion `did_x509_ffi` crate exposes the full API through C-compatible functions: |
| 244 | + |
| 245 | +| FFI Function | Purpose | |
| 246 | +|-------------|---------| |
| 247 | +| `did_x509_parse` | Parse a DID:x509 string into a handle | |
| 248 | +| `did_x509_parsed_get_fingerprint` | Get the CA fingerprint bytes | |
| 249 | +| `did_x509_parsed_get_hash_algorithm` | Get the hash algorithm string | |
| 250 | +| `did_x509_parsed_get_policy_count` | Get the number of policies | |
| 251 | +| `did_x509_parsed_free` | Free a parsed handle | |
| 252 | +| `did_x509_build_with_eku` | Build a DID:x509 string with EKU policy | |
| 253 | +| `did_x509_build_from_chain` | Build from a certificate chain | |
| 254 | +| `did_x509_validate` | Validate a DID against a certificate chain | |
| 255 | +| `did_x509_resolve` | Resolve a DID to a JSON DID Document | |
| 256 | +| `did_x509_error_message` | Get last error message | |
| 257 | +| `did_x509_error_code` | Get last error code | |
| 258 | +| `did_x509_error_free` | Free an error handle | |
| 259 | +| `did_x509_string_free` | Free a Rust-allocated string | |
| 260 | + |
| 261 | +C and C++ headers are available at: |
| 262 | +- **C**: `native/c/include/cose/did/x509.h` |
| 263 | +- **C++**: `native/c_pp/include/cose/did/x509.hpp` |
| 264 | + |
| 265 | +## Usage Example: SCITT Compliance |
| 266 | + |
| 267 | +A common pattern for SCITT (Supply Chain Integrity, Transparency, and Trust) |
| 268 | +compliance is to build a DID:x509 identifier from a signing certificate chain |
| 269 | +and embed it as the `iss` (issuer) claim in CWT protected headers: |
| 270 | + |
| 271 | +```rust |
| 272 | +use did_x509::{DidX509Builder, DidX509Policy, DidX509Validator}; |
| 273 | + |
| 274 | +// 1. Build the DID from the signing chain (leaf-first order) |
| 275 | +let did = DidX509Builder::build_from_chain_with_eku( |
| 276 | + &[leaf_der, intermediate_der, root_der], |
| 277 | +).expect("Failed to build DID:x509"); |
| 278 | + |
| 279 | +// 2. The DID string can be used as the CWT `iss` claim |
| 280 | +// e.g., "did:x509:0:sha256:<fingerprint>::eku:1.3.6.1.5.5.7.3.3" |
| 281 | + |
| 282 | +// 3. During validation, verify the DID against the presented chain |
| 283 | +let result = DidX509Validator::validate(&did, &[leaf_der, intermediate_der, root_der]) |
| 284 | + .expect("Validation error"); |
| 285 | +assert!(result.is_valid); |
| 286 | +``` |
| 287 | + |
| 288 | +## Dependencies |
| 289 | + |
| 290 | +| Crate | Purpose | |
| 291 | +|-------|---------| |
| 292 | +| `x509-parser` | DER certificate parsing, extension extraction | |
| 293 | +| `sha2` | SHA-256/384/512 fingerprint computation | |
| 294 | +| `serde` / `serde_json` | DID Document JSON serialization | |
| 295 | + |
| 296 | +## Memory Design |
| 297 | + |
| 298 | +- **Parsing**: `DidX509Parser::parse()` returns owned `DidX509ParsedIdentifier` (allocation required for fingerprint bytes and policy data extracted from the DID string) |
| 299 | +- **Policies**: `DidX509Policy::Eku` uses `Vec<Cow<'static, str>>` — static OID strings use `Cow::Borrowed` (zero allocation), dynamic OIDs use `Cow::Owned` |
| 300 | +- **DID Documents**: `VerificationMethod` JWK maps use `HashMap<Cow<'static, str>, String>` — all JWK field names (`kty`, `crv`, `x`, `y`, `n`, `e`) are `Cow::Borrowed` |
| 301 | +- **Validation**: `DidX509ValidationResult` collects errors as `Vec<String>` — only allocated on validation failure |
| 302 | +- **Fingerprinting**: SHA digests use `to_vec()` for cross-algorithm uniform handling (structurally required) |
| 303 | +- **Policy validators**: Borrow certificate data (zero-copy) — only allocate on error paths |
| 304 | + |
| 305 | +## Test Coverage |
| 306 | + |
| 307 | +The crate has 23 test files covering: |
| 308 | + |
| 309 | +- Parser tests: format validation, edge cases, percent encoding/decoding |
| 310 | +- Builder tests: SHA-256/384/512, chain construction, EKU extraction |
| 311 | +- Validator tests: fingerprint matching, policy validation, error cases |
| 312 | +- Resolver tests: RSA and EC key conversion, DID Document generation |
| 313 | +- Policy validator tests: EKU, Subject DN, SAN, Fulcio issuer |
| 314 | +- X.509 extension tests: extraction utilities |
| 315 | +- Comprehensive edge case and coverage-targeted tests |
| 316 | + |
| 317 | +## License |
| 318 | + |
| 319 | +Licensed under the MIT License. See [LICENSE](../../../../LICENSE) for details. |
0 commit comments