# did:x509 Method Specification ## Status, Version, and Authors Status: DRAFT Method version: `0` Authors: - Maik Riechert (Microsoft) - Antoine Delignat-Lavaud (Microsoft) ## Abstract This draft aims to define an interoperable and flexible issuer identifier format for messages that transport or refer to X.509 certificates, including COSE messages using [RFC 9360](https://www.rfc-editor.org/rfc/rfc9360). The did:x509 identifier format implements a direct, resolvable binding between a certificate chain and a compact issuer string. It can be conveyed as an issuer value in a COSE Header CWT Claims map as defined in [RFC 9597](https://www.rfc-editor.org/rfc/rfc9597), in JOSE/JWT messages such as the `iss` claim defined in [RFC 7519](https://www.rfc-editor.org/rfc/rfc7519), or through other protocol-specific mechanisms that associate the identifier with the certificate chain. This issuer identifier is convenient for references and policy evaluation, for example in the context of transparency ledgers. ## Introduction The RWOT11 workshop outlined the need for hybrid solutions that combine X.509 certificates with DIDs: ["Analysis of hybrid wallet solutions - Implementation options for combining x509 certificates with DIDs and VCs"](https://github.com/WebOfTrustInfo/rwot11-the-hague/blob/master/advance-readings/hybrid_wallet_solutions_x509_DIDs_VCs.md). The did:x509 method takes a simple approach that does not introduce additional infrastructure. Creating and resolving a did:x509 is a local operation. It relies on X.509 chain validation and matches elements contained in the DID to certificate properties within the chain. The main difference to other DID methods is that did:x509 requires a certificate chain to be passed using a new [DID resolution option](https://www.w3.org/TR/did-core/#did-resolution-options) `x509chain` while resolving a DID. This certificate chain is typically embedded in the signing envelope, for example within the `x5c` header parameter of JWS/JWT documents. Embedding certificate chains in configuration or policy is cumbersome. References to individual chain elements can also be too broad, or too unstable when those elements are short-lived. did:x509 combines authority pinning with certificate predicates in a compact identifier, for example `request.issuer == "did:x509:..."`. ## DID Method Name The DID method name is `x509`. A did:x509 DID starts with `did:x509:` and binds a CA fingerprint to one or more certificate predicates. ## DID Syntax The did:x509 ABNF definitions use [RFC 5234](https://www.rfc-editor.org/rfc/rfc5234.html), with case-sensitive string literals written as `%s"..."` per [RFC 7405](https://www.rfc-editor.org/rfc/rfc7405). The DID Core `idchar` and `pct-encoded` definitions are repeated for readability. ```abnf idchar = ALPHA / DIGIT / "." / "-" / "_" / pct-encoded pct-encoded = "%" HEXDIG HEXDIG ``` ```abnf did-x509 = %s"did:x509:" method-specific-id method-specific-id = version ":" ca-fingerprint-alg ":" ca-fingerprint 1*("::" predicate-name ":" predicate-value) version = 1*DIGIT ca-fingerprint-alg = %s"sha256" / %s"sha384" / %s"sha512" ca-fingerprint = base64url predicate-name = %s"subject" / %s"san" / %s"eku" / %s"fulcio-issuer" / %s"fulcio" predicate-value = *(1*idchar ":") 1*idchar base64url = 1*(ALPHA / DIGIT / "-" / "_") ``` The current version value is `0`. The `ca-fingerprint-alg` value is one of `sha256`, `sha384`, or `sha512`. The `ca-fingerprint` value is a base64url-encoded digest of a non-leaf certificate in the certificate chain, that is, either an intermediate or root CA certificate. The `::` separator introduces predicates. Each predicate has a `predicate-name` and a `predicate-value`. ## Method-specific Identifier The method-specific identifier has three parts: 1. A version number. 2. A certificate authority fingerprint algorithm and value. 3. One or more predicates that match fields in the leaf certificate. The DID subject is the logical identity selected by the CA fingerprint and sequence of predicates. It is not necessarily the X.509 subject name; `subject` is only one predicate type. did:x509 does not define any DID URL path or query semantics. A did:x509 DID URL MUST NOT include a path or query component. Fragment identifiers remain valid for identifying resources within a resolved DID document, for example `#0`. Example: `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::subject:C:US:ST:California:O:Example%20Organisation` In this example, the identifier pins to a certificate authority using a SHA-256 certificate hash and uses the `subject` predicate to express criteria that a leaf certificate subject must fulfil. This identifier will match certificate chains with matching leaf certificate subject fields and a matching intermediate or root CA certificate. ### Predicate validation model DID syntax validation and predicate validation are defined in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) to avoid ambiguous pseudo-code. This has no bearing on implementations, which can be written in any language. The input to the Rego runtime is a JSON document: `{"did": "", "chain": }`, where `did` is the DID string and `chain` is the parsed representation of the certificate chain derived from the `x509chain` resolution option. Core Rego policy: ```rego package did_x509 import future.keywords.if import future.keywords.in idchars := `([A-Za-z0-9._-]|%[0-9A-Fa-f]{2})+` predicate_pattern := sprintf(`::(subject|san|eku|fulcio-issuer|fulcio):%s(:%s)*`, [idchars, idchars]) did_pattern := sprintf(`^did:x509:0:(sha256|sha384|sha512):[A-Za-z0-9_-]+(%s)+$`, [predicate_pattern]) decode_scalar(encoded) := decoded if { decoded := urlquery.decode(encoded) json.unmarshal(json.marshal(decoded)) == decoded } parse_did(did) := [ca_fingerprint_alg, ca_fingerprint, predicates] if { prefix := "did:x509:0:" regex.match(did_pattern, did) rest := trim_prefix(did, prefix) parts := split(rest, "::") [ca_fingerprint_alg, ca_fingerprint] := split(parts[0], ":") predicates_raw := array.slice(parts, 1, count(parts)) predicates := [y | some i s := predicates_raw[i] j := indexof(s, ":") y := [substring(s, 0, j), substring(s, j+1, -1)] ] } valid if { [ca_fingerprint_alg, ca_fingerprint, predicates] := parse_did(input.did) count(predicates) > 0 ca := [c | some i; i != 0; c := input.chain[i]] ca[_].fingerprint[ca_fingerprint_alg] == ca_fingerprint valid_predicates := [i | some i [name, value] := predicates[i] validate_predicate(name, value) ] count(valid_predicates) == count(predicates) } ``` The overall Rego policy is assembled by concatenating the core Rego policy with the Rego policy fragments in the following sections, each one defining a `validate_predicate` function. ### Percent-encoding Some predicates require values to be percent-encoded. Percent-encoding is specified in [RFC 3986 Section 2.1](https://www.rfc-editor.org/rfc/rfc3986#section-2.1). Characters are encoded as UTF-8 before percent-encoding. All characters that are not in the allowed set below must be percent-encoded: ```abnf allowed = ALPHA / DIGIT / "-" / "." / "_" ``` Note that most libraries implement percent-encoding in the context of URLs and do not encode `~` (`%7E`). Resolution fails if a percent-decoded value is not valid UTF-8. The Rego `decode_scalar` helper decodes once and checks that a JSON string round trip preserves the decoded bytes. OPA's `urlquery.decode` alone does not validate UTF-8; JSON serialization would replace invalid byte sequences, so those values fail the check. A correctly encoded U+FFFD remains a valid scalar. ### `subject` predicate ```abnf predicate-name = %s"subject" predicate-value = key ":" value *(":" key ":" value) key = label / oid value = 1*idchar label = %s"CN" / %s"L" / %s"ST" / %s"O" / %s"OU" / %s"C" / %s"STREET" oid = 1*DIGIT *("." 1*DIGIT) ``` `:` are the subject name fields in `chain[0].subject` in any order. Key repetitions are not allowed. Values must be percent-encoded. Example: `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::subject:C:US:ST:California:O:Example%20Organisation` Rego policy: ```rego validate_predicate(name, value) := true if { name == "subject" items := split(value, ":") count(items) % 2 == 0 keys := {k | some i; i % 2 == 0; k := items[i]} count(keys) == count(items) / 2 subject := {k: v | some i i % 2 == 0 k := items[i] v := decode_scalar(items[i+1]) } count(subject) >= 1 count(subject) == count(keys) object.subset(input.chain[0].subject, subject) == true } ``` ### `san` predicate ```abnf predicate-name = %s"san" predicate-value = san-type ":" san-value / %s"othername" ":" othername-type-oid ":" san-value san-type = %s"email" / %s"dns" / %s"uri" othername-type-oid = oid san-value = 1*idchar ``` The `email`, `dns`, and `uri` forms require exactly a type and a nonempty percent-encoded value. The pair `["", ""]` must be one of the items in `chain[0].extensions.san`. Note that `dn` is not supported as a predicate; directory names remain part of the JSON model. The `othername` form requires exactly a type, a literal dotted type OID, and a nonempty percent-encoded value: `::san:othername::`. The triple `["othername", "", ""]` must be one of the items in `chain[0].extensions.san`. The OID MUST be an exact literal from the closed registry below; it is not percent-decoded or normalized. Keeping both the OID and the scalar prevents collisions between OtherName namespaces. #### OtherName type registry RFC 5280 defines `OtherName` as a type OID plus a `[0] EXPLICIT ANY DEFINED BY type-id` value. OtherName values can be structured or binary; a string-like ASN.1 tag alone does not establish their schema. Only the following type is registered: | Type OID | Schema and comparison | |---|---| | `1.3.6.1.4.1.57264.1.7` | Fulcio username identity: exactly one complete primitive universal DER UTF8String, compared as the full decoded string, exactly. | X.509 parsing validates the GeneralName and the explicit wrapper. The exposed inner value MUST have tag `0x0C`, canonical minimal definite-length encoding, complete content, no trailing bytes, and strictly valid UTF-8. Wrong tags or classes, constructed strings, raw UTF-8 in place of DER, indefinite or nonminimal lengths, truncated encodings, trailing bytes, and invalid UTF-8 cause resolution to fail. Decoders MUST NOT substitute replacement characters. Future registry entries must define their own schema and comparison rules rather than inheriting this string interpretation. This `.7` OID is a type identifier inside the Subject Alternative Name extension (`2.5.29.17`), not a standalone extension. A standalone `.7` extension or the standalone Fulcio Token Subject extension (`1.3.6.1.4.1.57264.1.24`) cannot supply a SAN match. `.7` predates Issuer V2 and does not require either the legacy issuer extension (`.1`) or Issuer V2 (`.8`). Issuer V2 names the `.8` issuer extension, not a certificate format. Split the DID's structural `::` and `:` separators before decoding the scalar exactly once. Comparison performs no case folding, Unicode normalization, trimming, wildcard expansion, username/domain reconstruction, or URI rewriting. In particular, `%2521` decodes to the literal string `%21`, not `!`. Each repeated predicate must have a match; one matching entry is sufficient, and matching entries need not be unique or consumed. Example: `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::san:email:bob%40example.com` OtherName example: `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::san:othername:1.3.6.1.4.1.57264.1.7:alice%21example.com` The corresponding JSON entry is `["othername", "1.3.6.1.4.1.57264.1.7", "alice!example.com"]`. Fulcio's [username identity mapping](https://github.com/sigstore/fulcio/blob/e9c49671828cf7bf21ae92fa2927e3add18b3055/pkg/identity/username/principal.go) combines `!`. The SAN value can therefore be `alice!example.com` while the raw Token Subject is `alice`; this predicate matches the whole SAN string, not inferred token claims. Rego policy: ```rego othername_type_oids := {"1.3.6.1.4.1.57264.1.7"} validate_predicate(name, value) := true if { name == "san" [san_type, san_value_encoded] := split(value, ":") san_type in {"email", "dns", "uri"} san_value := decode_scalar(san_value_encoded) [san_type, san_value] == input.chain[0].extensions.san[_] } validate_predicate(name, value) := true if { name == "san" [san_type, type_oid, san_value_encoded] := split(value, ":") san_type == "othername" type_oid in othername_type_oids san_value := decode_scalar(san_value_encoded) ["othername", type_oid, san_value] == input.chain[0].extensions.san[_] } ``` This is an additive expansion of method version `0`: well-formed registered `.7` SANs that previously failed as unsupported now map successfully, including when only an existing predicate is selected. Previously accepted DIDs and chains, existing SAN pairs, and DID Document shapes are unchanged. Older method-0 resolvers reject the new selector; no automatic migration is defined. ### `eku` predicate ```abnf predicate-name = %s"eku" predicate-value = eku eku = oid oid = 1*DIGIT *("." 1*DIGIT) ``` `eku` is one of the OIDs within `chain[0].extensions.eku`. Example: `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::eku:1.3.6.1.4.1.311.10.3.13` Rego policy: ```rego validate_predicate(name, value) := true if { name == "eku" value == input.chain[0].extensions.eku[_] } ``` ### `fulcio-issuer` predicate ```abnf predicate-name = %s"fulcio-issuer" predicate-value = fulcio-issuer fulcio-issuer = 1*idchar ``` `fulcio-issuer` selects only the legacy standalone extension `1.3.6.1.4.1.57264.1.1`, whose payload is raw UTF-8, mapped to `chain[0].extensions.fulcio_issuer`. Its predicate value is the issuer without leading `https://`, percent-encoded. Matching decodes the value once, prepends `https://`, and compares exactly. Issuer V2 (`.8`) never supplies this value. The `fulcio_issuer` extension MUST be present on `chain[0]` when this predicate is used; resolution fails if it is absent. The extension MUST NOT be marked critical. Example: `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::fulcio-issuer:accounts.google.com::san:email:bob%40example.com` Example 2: `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::fulcio-issuer:issuer.example.com::san:uri:https%3A%2F%2Fexample.com%2Focto-org%2Focto-automation%2Fworkflows%2Foidc.yml%40refs%2Fheads%2Fmain` Rego policy: ```rego validate_predicate(name, value) := true if { name == "fulcio-issuer" suffix := decode_scalar(value) concat("", ["https://", suffix]) == input.chain[0].extensions.fulcio_issuer } ``` ### `fulcio` predicate ```abnf predicate-name = %s"fulcio" predicate-value = fulcio-field ":" fulcio-value fulcio-value = 1*idchar fulcio-field = %s"issuer" / %s"build-signer-uri" / %s"build-signer-digest" / %s"runner-environment" / %s"source-repository-uri" / %s"source-repository-digest" / %s"source-repository-ref" / %s"source-repository-identifier" / %s"source-repository-owner-uri" / %s"source-repository-owner-identifier" / %s"build-config-uri" / %s"build-config-digest" / %s"build-trigger" / %s"run-invocation-uri" / %s"source-repository-visibility-at-signing" / %s"deployment-environment" / %s"token-subject" ``` Each occurrence is exactly `::fulcio::`, with one registered literal field and one nonempty encoded scalar. Unknown fields, encoded field names, missing or empty values, extra structural components, malformed percent escapes, and percent-decoded values that are not valid UTF-8 cause resolution to fail. Multiple predicates, including repeated `fulcio` predicates, are ANDed. #### Standalone Fulcio extension registry These OIDs are standalone extensions, separate from the SAN OtherName registry. Each `.8`-`.24` `extnValue` payload contains exactly one complete primitive universal DER UTF8String (tag `0x0C`). Every field uses the same certificate encoding, scalar DID encoding, and exact comparison: | OID | Literal field / JSON key in `extensions.fulcio` | |---|---| | `1.3.6.1.4.1.57264.1.8` | `issuer` (Issuer V2) | | `1.3.6.1.4.1.57264.1.9` | `build-signer-uri` | | `1.3.6.1.4.1.57264.1.10` | `build-signer-digest` | | `1.3.6.1.4.1.57264.1.11` | `runner-environment` | | `1.3.6.1.4.1.57264.1.12` | `source-repository-uri` | | `1.3.6.1.4.1.57264.1.13` | `source-repository-digest` | | `1.3.6.1.4.1.57264.1.14` | `source-repository-ref` | | `1.3.6.1.4.1.57264.1.15` | `source-repository-identifier` | | `1.3.6.1.4.1.57264.1.16` | `source-repository-owner-uri` | | `1.3.6.1.4.1.57264.1.17` | `source-repository-owner-identifier` | | `1.3.6.1.4.1.57264.1.18` | `build-config-uri` | | `1.3.6.1.4.1.57264.1.19` | `build-config-digest` | | `1.3.6.1.4.1.57264.1.20` | `build-trigger` | | `1.3.6.1.4.1.57264.1.21` | `run-invocation-uri` | | `1.3.6.1.4.1.57264.1.22` | `source-repository-visibility-at-signing` | | `1.3.6.1.4.1.57264.1.23` | `deployment-environment` | | `1.3.6.1.4.1.57264.1.24` | `token-subject` | Use the same strict DER UTF8String decoder as for the registered `.7` OtherName inner value, without sharing or expanding its OID registry. Decode every registered standalone extension eagerly when present on any certificate in the chain, even when it is not selected. Wrong tags or classes, constructed strings, raw UTF-8 instead of DER, indefinite or nonminimal lengths, truncated values, trailing bytes, and invalid UTF-8 cause mapping and resolution to fail. Do not replace invalid characters or accept historical malformed `.8` encodings. Standalone Fulcio extensions MUST NOT be critical; both JSON conversion and resolution reject their critical forms, without bypassing path validation. Map present values to the corresponding keys in `extensions.fulcio`; do not require any other registry field to be present. Missing requested fields fail. An empty DER UTF8String is retained as an empty JSON string but cannot match a nonempty predicate, and neither empty nor absent values are wildcards. Do not synthesize values from other claims or alias deprecated `.2`-`.6` extensions. The `.7` type inside SAN is not part of this standalone registry. Split structural separators before decoding only the scalar, exactly once. Percent-encode the decoded UTF-8 string, not its DER wrapper, leaving only ASCII letters, digits, `-`, `.`, and `_` unescaped. Comparison is opaque and exact: no URL normalization, Unicode normalization, case folding, trimming, numeric conversion, digest re-encoding, enum restrictions, or provider-specific format assumptions. A colon is `%3A`, and a percent sign is `%25`. For example, `sha1:abc123` is `sha1%3Aabc123`; a URI containing literal `%2F` must use `%252F` so that one decode preserves the existing escape. Example using Issuer V2 and a token subject: `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::fulcio:issuer:https%3A%2F%2Ftoken.actions.githubusercontent.com::fulcio:token-subject:repo%3Apydantic%2Fpydantic-ai%3Aenvironment%3Arelease` Rego policy: ```rego fulcio_fields := { "issuer", "build-signer-uri", "build-signer-digest", "runner-environment", "source-repository-uri", "source-repository-digest", "source-repository-ref", "source-repository-identifier", "source-repository-owner-uri", "source-repository-owner-identifier", "build-config-uri", "build-config-digest", "build-trigger", "run-invocation-uri", "source-repository-visibility-at-signing", "deployment-environment", "token-subject", } validate_predicate(name, value) := true if { name == "fulcio" [field, encoded] := split(value, ":") field in fulcio_fields regex.match(sprintf(`^%s$`, [idchars]), encoded) scalar := decode_scalar(encoded) scalar == input.chain[0].extensions.fulcio[field] } ``` #### Explicit issuer migration `fulcio:issuer` selects only `.8` and compares its full decoded issuer string, including any scheme. It maps to `extensions.fulcio.issuer`, independently of the raw UTF-8 `.1` value in `extensions.fulcio_issuer`. There are no aliases, fallbacks, precedence rules, agreement checks, or resolver rewrites between them. A `.8`-only certificate fails `fulcio-issuer`, and a `.1`-only certificate fails `fulcio:issuer`. Both valid extensions can hold different values and remain independently selectable, regardless of their order in the certificate. Migration explicitly changes the DID: replace `::fulcio-issuer:issuer.example.com` with `::fulcio:issuer:https%3A%2F%2Fissuer.example.com` only when choosing the `.8` identity. The DID Document uses the selected DID unchanged; it is not a canonicalized alias of the legacy DID. This is an additive method-version-`0` predicate; older resolvers MUST reject unsupported new predicates. Existing predicate meanings are unchanged, but previously ignored malformed registered standalone extensions now fail eagerly, even for an existing predicate. ## Verifiable Data Registry and Trust Model did:x509 does not define a persistent registry of DID Documents. Resolution uses the DID string and the `x509chain` resolution option. The `x509chain` option carries the certificate chain as a comma-separated list of base64url-encoded DER certificates: ```text x509chain = b64url(DER(leaf)) "," b64url(DER(intermediate)) "," b64url(DER(root)) ``` The chain is ordered leaf first and root or trust anchor last. Each comma-separated item is the DER encoding of one complete X.509 certificate, not a public key, fingerprint, or DER encoding of the whole chain. Trust is established by validating the certificate chain, matching the CA fingerprint, and validating the predicates against the leaf certificate. Applications can add revocation, certificate transparency, signing time, or endorsement checks. ## Certificate Chain JSON Model For predicate evaluation, the resolver maps the certificate chain to a limited JSON model. This model contains only the fields did:x509 matches on; it does not replace X.509 parsing or RFC 5280 path validation. The model is a JSON array with at least two certificate objects. The leaf certificate is first, followed by issuer certificates, with the root or trust anchor last. Each certificate object can contain: | Field | Meaning | |---|---| | `fingerprint` | Base64url-encoded hashes of the DER-encoded certificate, keyed by `sha256`, `sha384`, and `sha512`. | | `issuer` | X.509 issuer name, represented as an object of name attributes. | | `subject` | X.509 subject name, represented as an object of name attributes. | | `extensions.eku` | Extended Key Usage OIDs from RFC 5280 Section 4.2.1.12. | | `extensions.san` | Subject Alternative Name entries from RFC 5280 Section 4.2.1.6. | | `extensions.fulcio_issuer` | The legacy `.1` standalone issuer, decoded as raw UTF-8. | | `extensions.fulcio` | Present `.8`-`.24` standalone fields keyed by the registered literal names, decoded from strict DER UTF8Strings. | Name objects use the RFC 4514 labels `CN`, `L`, `ST`, `O`, `OU`, `C`, and `STREET` for common attributes. Other attributes use dotted OID strings as keys. Repeated attributes are not supported. Values are converted to UTF-8 strings. SAN entries are variable-arity arrays. The first item identifies the SAN type. Existing types use pairs; registered OtherNames use triples retaining both their type OID and their decoded scalar: | SAN type | JSON shape | |---|---| | RFC 822 name | `["email", "user@example.com"]` | | DNS name | `["dns", "example.com"]` | | URI | `["uri", "https://example.com"]` | | Directory name | `["dn", {"CN": "Example"}]` | | Registered OtherName | `["othername", "1.3.6.1.4.1.57264.1.7", "alice!example.com"]` | Preserve every well-formed entry in order, including identical duplicates and different values for the same OtherName OID. An unregistered OtherName OID, a malformed registered value, or any other unsupported SAN form causes mapping to fail, even in a noncritical SAN and even when another entry or an unrelated predicate would match. Duplicate SAN extensions remain invalid X.509; allowing repeated entries does not allow repeated extensions. The `extensions.fulcio` group is additive. Keep `extensions.fulcio_issuer` separate and omit groups and fields that are absent; do not fill in default values. All registered Fulcio fields are decoded eagerly as specified above. Duplicate standalone extensions remain invalid X.509. Example certificate chain model: ```json [ { "fingerprint": { "sha256": "leaf-sha256", "sha384": "leaf-sha384", "sha512": "leaf-sha512" }, "issuer": { "CN": "Example CA" }, "subject": { "CN": "Example" }, "extensions": { "eku": ["1.3.6.1.4.1.311.10.3.13"], "san": [ ["email", "user@example.com"], ["dns", "example.com"], ["uri", "https://example.com"], ["othername", "1.3.6.1.4.1.57264.1.7", "alice!example.com"], [ "dn", { "CN": "Example" } ] ], "fulcio_issuer": "https://issuer.example.com", "fulcio": { "issuer": "https://issuer-v2.example.com", "deployment-environment": "release", "token-subject": "repo:pydantic/pydantic-ai:environment:release" } } }, { "fingerprint": { "sha256": "ca-sha256", "sha384": "ca-sha384", "sha512": "ca-sha512" }, "issuer": { "CN": "Example Root CA" }, "subject": { "CN": "Example CA" }, "extensions": {} } ] ``` In the rest of this document, `chain` refers to the certificate chain mapped to this JSON model. ## DID Document Shape Resolving a did:x509 identifier produces a DID Document with a `JsonWebKey` verification method derived from the leaf certificate public key. The DID Document is self-controlled: verification methods use the DID itself as `controller`. If the leaf certificate has the key usage bit for `digitalSignature`, or is missing the key usage extension, the DID Document includes `authentication` and `assertionMethod`. If the leaf certificate has the key usage bit for `keyAgreement`, or is missing the key usage extension, the DID Document includes `keyAgreement`. If the leaf certificate includes the key usage extension but has neither `digitalSignature` nor `keyAgreement`, resolution fails. The registered media type for a DID Document is `application/did`. Resolvers may also support `application/did+ld+json` or `application/did+json` for compatibility with DID Core 1.0 tooling. The media type is selected by the resolution request, not by the DID string. The JSON-LD `@context` must define every term used. The example below uses the [Controlled Identifiers v1 context](https://www.w3.org/ns/cid/v1). Example DID Document: ```json { "@context": "https://www.w3.org/ns/cid/v1", "id": "did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::subject:CN:Example", "verificationMethod": [ { "id": "did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::subject:CN:Example#0", "type": "JsonWebKey", "controller": "did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::subject:CN:Example", "publicKeyJwk": { "kty": "EC", "crv": "P-256", "x": "usNb0QXAk6R76GPFvKT5a46LC0_qRpxNoLn9WAX8K0I", "y": "dTtI2j8aV0Mdk5fNWP9rCJvFIo6QfLjCm8V5v10J4Xg" } } ], "authentication": [ "did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::subject:CN:Example#0" ], "assertionMethod": [ "did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::subject:CN:Example#0" ], "keyAgreement": [ "did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::subject:CN:Example#0" ] } ``` ## Method Operations ### Create Creating a did:x509 identifier is a local operation. The DID must be constructed according to the syntax rules in this specification. No registration action is required, and no registry authorization is checked. When constructing a did:x509 identifier, determine what constitutes a logical identity within a given certificate authority. Concretely, determine which certificate fields the authority uses to uniquely represent an identity. After that, choose one or more matching predicates that express such an identity as faithfully as possible. As an example, a certificate authority may use email addresses as a way to separate identities and use the SAN extension to store the email address. In that case, the did:x509 identifier should be constructed using the `san` predicate, for example, `did:x509:0:sha256:::san:email:bob%40example.com`. In other cases, an authority may not include email addresses at all and instead rely on a specific set of subject fields to separate identities. In that case, the `subject` predicate should be used. In yet other cases, authorities may assign unique numbers or other types of stable identifiers to logical identities. Typically, this is done to have a stable reference even if a person changes their name or email address. In all cases, the goal is to craft a did:x509 identifier that is stable yet not too loose in its predicates. An example of a loose did:x509 identifier may be to use the `subject` predicate and only include the `O` field without location fields like country (`C`) or state/locality (`ST`). Whether a did:x509 identifier should pin to an intermediate CA instead of a root CA depends on whether there is value in distinguishing between them. Pinning to an intermediate CA typically means that the lifetime of the did:x509 identifier will be shorter, since intermediate CA certificates usually have a shorter validity period than root CA certificates. ### Read / Resolve The Read operation is DID resolution. The operation takes as input a DID to resolve, together with the `x509chain` DID resolution option. The DID resolver uses the DID, the certificate chain, and the process in the DID Resolution section to generate a DID Document. No caller authorization is required; authenticity is checked by certificate chain validation, CA fingerprint matching, and predicate validation. ### Update This DID method does not support updating the DID Document, assuming a fixed certificate chain. There is no update authorization operation. However, the public key included in the DID Document varies depending on the certificate chain that was used as input to the DID resolution process. Typically, multiple chains, in particular leaf certificates, are valid for a given did:x509 identifier. ### Deactivate This DID method does not support deactivating the DID. There is no deactivation authorization operation. However, if the certificate authority revokes all certificates for the matching DID, or they expire, and does not issue new certificates matching the same DID, then this can be considered equivalent to deactivation of the DID. There is no technical guarantee in this case and the certificate authority can revert its decision. ## DID Resolution If the DID to resolve is given as a DID URL, its fragment, if any, is removed first, and `` below refers to the result. Resolution fails if the DID URL has a path or query component. The following steps must be used to generate a corresponding DID Document: 1. Decode the `x509chain` resolution option value into individual certificates by splitting the string on `","` and base64url-decoding each resulting string. The result is a list of DER-encoded certificates that can be loaded in standard libraries. Fail if the list contains fewer than two certificates. 2. Check whether the list of certificates forms a valid certificate chain using [RFC 5280 certification path validation](https://www.rfc-editor.org/rfc/rfc5280#section-6) procedures with the last certificate in the chain as trust anchor. Implementations MUST perform RFC 5280 certification path validation. Additionally, fail if any certificate in the chain contains a critical extension that is neither (a) one of the extensions represented in the JSON model (`eku`, `san`), nor (b) one of the following standard RFC 5280 extensions: `basicConstraints`, `keyUsage`, `nameConstraints`, `policyConstraints`, `policyMappings`, `certificatePolicies`, `inhibitAnyPolicy`. The enclosing SAN extension may be critical; individual SAN entries have no critical flag. RFC 5280 requires a critical SAN when the leaf subject is empty, as used by Fulcio username identities. Registered OtherName support does not bypass normal path validation, critical-extension processing, or name constraints. Unknown critical extensions and unsupported critical name constraints still cause resolution to fail. The standalone Fulcio `.1` and `.8`-`.24` extensions are deliberately not on that list, even though they are represented in the JSON model. Fulcio does not mark them critical; for example, a [dump of a Fulcio-issued certificate shows the legacy issuer's `critical` field as `BOOL ABSENT`](https://github.com/sigstore/gitsign/blob/44f5e17fac6944fdde71c94d2e77ab075c9dca9f/docs/timestamp.md#L102-L107). Issuers MUST NOT mark them critical: they are unrecognized extensions for RFC 5280 path validation. JSON mapping also rejects critical standalone Fulcio extensions, even when their fields are not selected. Instead of using the current time as specified in [Section 6.1.3 of RFC 5280](https://www.rfc-editor.org/info/rfc5280/#section-6.1.3) when validating the chain, applications may choose a context-relevant point in time. For example, applications handling signed documents may choose to use the signing time instead, which might come from a CWT `iat` claim ([RFC 8392](https://www.rfc-editor.org/rfc/rfc8392)) or JWT `iat` claim ([RFC 7519](https://www.rfc-editor.org/rfc/rfc7519)). Such a claim is not trusted time by itself and needs to be integrity protected and accepted by application policy. 3. If required by the application, check whether any certificate in the chain is revoked using CRL, OCSP, or other mechanisms. 4. Apply any further application-specific checks, for example disallowing insecure certificate signature algorithms. 5. Map the certificate chain to the JSON model. 6. Check whether the DID is valid against the certificate chain in the JSON model according to the Rego policy or equivalent rules defined in this document. 7. Extract the public key of the first certificate in the chain. 8. Convert the public key to a JSON Web Key. 9. Create the following partial DID Document: ```json { "@context": "https://www.w3.org/ns/cid/v1", "id": "", "verificationMethod": [{ "id": "#0", "type": "JsonWebKey", "controller": "", "publicKeyJwk": { "kty": "" } }] } ``` 10. If the first certificate in the chain has the key usage bit position for `digitalSignature` set or is missing the key usage extension, add the following to the DID Document: ```json { "authentication": ["#0"], "assertionMethod": ["#0"] } ``` 11. If the first certificate in the chain has the key usage bit position for `keyAgreement` set or is missing the key usage extension, add the following to the DID Document: ```json { "keyAgreement": ["#0"] } ``` 12. If the first certificate in the chain includes the key usage extension but has neither `digitalSignature` nor `keyAgreement` set as key usage bits, fail. 13. Return the complete DID Document. ## Security Considerations ### Identifier ambiguity This DID method maps characteristics of X.509 certificate chains to identifiers. It allows a single identifier to map to multiple certificate chains, giving the identifier stability across the expiry of individual chains. However, if the predicates used in the identifier are chosen too loosely, the identifier may match too wide a set of certificate chains. This may have security implications as it may authorize an identity for actions it was not meant to be authorized for. To mitigate this issue, the certificate authority should publish their expected usage of certificate fields and indicate which ones constitute a unique identity, versus any additional fields that may be of an informational nature. This will help users create an appropriate did:x509 identifier as well as consumers of signed content to decide whether it is appropriate to trust a given did:x509 identifier. ### X.509 trust stores Resolution validates the supplied certificate chain with the last certificate in `x509chain` as the path-validation trust anchor. This checks the chain and DID predicates, but does not decide whether the CA or DID is acceptable to a relying party. Relying parties make that trust decision by policy, for example through a CA trust store, an allowlist of DIDs or CA fingerprints, or other application-specific rules. ### Use of identifier contents While it is acceptable to use a did:x509 identifier as an opaque handle to implement a relying-party policy, implementers MUST NOT parse or interpret individual components of the identifier string for authorization decisions unless the identifier has been resolved against a verified certificate chain. Specifically, extracting and relying upon subject names, organizational information, or other embedded values directly from the identifier string, without performing full resolution and chain validation, is insecure. An attacker could craft a syntactically valid did:x509 identifier containing arbitrary values that do not correspond to any legitimate certificate chain. Only after successful resolution, which includes verification of the CA fingerprint against the provided chain and validation of all predicates, can the identifier be considered authentic. Systems that bypass this resolution process and instead parse identifier components directly are vulnerable to impersonation and privilege escalation attacks. ## Privacy Considerations The did:x509 identifier can contain certificate subject names, subject alternative names, extended key usage values, Fulcio issuer and build/source/token metadata, and a certificate authority fingerprint. These values can reveal personal names, email addresses, domain names, organizational affiliations, credential issuers, repository identities, deployment environments, token subjects, or other identifying information. DID creators should choose predicates that are specific enough for relying-party policy but disclose no more certificate attributes than necessary. The `x509chain` resolution option carries the certificate chain used as resolution evidence. Certificates can contain additional metadata beyond the predicates encoded in the DID, including subject attributes, SAN entries, validity periods, certificate policies, and extension values. Resolvers and verifiers should treat certificate chains as potentially identifying data, avoid unnecessary logging or redistribution, and apply data minimization when retaining resolution inputs or outputs. Stable did:x509 identifiers can enable correlation across transactions, transparency logs, ledgers, and verifiable credentials. If unlinkability is required, relying parties should avoid reusing the same did:x509 identifier across contexts, and issuers should prefer predicates based on role- or service-specific identifiers rather than human-identifying certificate fields. ## Test Vectors The machine-readable certificate chains, DIDs, expected resolution outcomes, and expected DID Documents are maintained in [`test-vectors.json`](test-vectors.json). The file is an array of independent input/output test cases. Each input embeds its certificate chain as an array of unpadded base64url-encoded DER certificates in leaf-first order. Each output contains either the expected DID Document or an expected error pattern. Certificate validity periods are not checked by these vectors. ## References ### Normative references [Decentralized Identifiers (DIDs) v1.0](https://www.w3.org/TR/2022/REC-did-core-20220719/). Manu Sporny, Amy Guy, Markus Sabadello, Drummond Reed. W3C. 19 July 2022. W3C Recommendation. [RFC 5280 - Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile](https://www.rfc-editor.org/rfc/rfc5280). D. Cooper, S. Santesson, S. Farrell, S. Boeyen, R. Housley, W. Polk. IETF. May 2008. Proposed Standard. [RFC 4514 - Lightweight Directory Access Protocol (LDAP): String Representation of Distinguished Names](https://www.rfc-editor.org/rfc/rfc4514). K. Zeilenga. IETF. June 2006. Proposed Standard. [RFC 4648 - The Base16, Base32, and Base64 Data Encodings](https://www.rfc-editor.org/rfc/rfc4648). S. Josefsson. IETF. October 2006. Proposed Standard. [RFC 5234 - Augmented BNF for Syntax Specifications: ABNF](https://www.rfc-editor.org/rfc/rfc5234.html). D. Crocker, P. Overell. IETF. January 2008. Internet Standard. [RFC 7405 - Case-Sensitive String Support in ABNF](https://www.rfc-editor.org/rfc/rfc7405). P. Kyzivat. IETF. December 2014. Proposed Standard. [RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986). T. Berners-Lee, R. Fielding, L. Masinter. IETF. January 2005. Internet Standard. [FIPS 180-4 - Secure Hash Standard](https://csrc.nist.gov/publications/detail/fips/180/4/final). NIST. August 2015. FIPS Publication. ### Informative references [Analysis of hybrid wallet solutions - Implementation options for combining x509 certificates with DIDs and VCs](https://github.com/WebOfTrustInfo/rwot11-the-hague/blob/master/advance-readings/hybrid_wallet_solutions_x509_DIDs_VCs.md). Carsten Stoecker (Spherity) and Christiane Wirrig (Spherity) with support of Paul Bastian (Bundesdruckerei) and Steffen Schwalm (msg Group) in the IDunion Project. 20 July 2022. RWOT11 topic paper. [RFC 9360 - CBOR Object Signing and Encryption (COSE): Header Parameters for Carrying and Referencing X.509 Certificates](https://www.rfc-editor.org/rfc/rfc9360). J. Schaad. IETF. February 2023. Proposed Standard. [RFC 9597 - CBOR Web Token (CWT) Claims in COSE Headers](https://www.rfc-editor.org/rfc/rfc9597). M. Jones. IETF. June 2024. Proposed Standard. [RFC 8392 - CBOR Web Token (CWT)](https://www.rfc-editor.org/rfc/rfc8392). M. Jones, E. Wahlstroem, S. Erdtman, H. Tschofenig. IETF. May 2018. Proposed Standard. [RFC 7519 - JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519). M. Jones, J. Bradley, N. Sakimura. IETF. May 2015. Proposed Standard. [Verifiable Credentials Data Model v1.1](https://www.w3.org/TR/2022/REC-vc-data-model-20220303/). Manu Sporny, Dave Longley, David Chadwick. W3C. 03 March 2022. W3C Recommendation. [Rego Policy Language](https://www.openpolicyagent.org/docs/latest/policy-language/). Open Policy Agent contributors. [Fulcio](https://github.com/sigstore/fulcio). Fulcio contributors.