├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── did_x509.py ├── requirements.txt ├── specification.md ├── test-data ├── README.txt ├── fulcio-email.pem ├── fulcio-github-actions.pem └── ms-code-signing.pem └── test.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - '**.md' 8 | pull_request: 9 | paths-ignore: 10 | - '**.md' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | ci: 15 | name: CI 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - uses: actions/setup-python@v4 21 | with: 22 | python-version: '3.10' 23 | 24 | - run: pip install -r requirements.txt 25 | 26 | - run: pytest test.py 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to 4 | agree to a Contributor License Agreement (CLA) declaring that you have the right to, 5 | and actually do, grant us the rights to use your contribution. For details, visit 6 | https://cla.microsoft.com. 7 | 8 | When you submit a pull request, a CLA-bot will automatically determine whether you need 9 | to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the 10 | instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 11 | 12 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 13 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 14 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # did:x509 2 | 3 | This repository contains the DRAFT specification of the did:x509 [DID](https://www.w3.org/TR/did-core/) method. It aims to achieve interoperability between existing X.509 solutions and Decentralized Identifiers (DIDs) to support operational models in which a full transition to DIDs is not achievable or desired yet. 4 | 5 | NOTE: This specification is in its early development and is published to invite feedback from the community. Please contribute by opening issues and pull requests! 6 | 7 | ## Specification 8 | 9 | See [specification.md](specification.md). 10 | 11 | ## Reference implementation 12 | 13 | This repository contains a non-production reference implementation written in Python. 14 | 15 | First, install the required Python packages: 16 | 17 | ``` 18 | pip install -r requirements.txt 19 | ``` 20 | 21 | Then, run the resolver with an example DID and matching certificate chain: 22 | 23 | ```sh 24 | python did_x509.py resolve did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::subject:CN:Microsoft%20Corporation --chain test-data/ms-code-signing.pem 25 | # Output: { } 26 | ``` 27 | 28 | To convert a certificate chain to the JSON data model defined in the specification, run: 29 | 30 | ```sh 31 | python did_x509.py convert test-data/ms-code-signing.pem 32 | # Output: [ Certificate chain in JSON ] 33 | ``` 34 | 35 | To percent-encode a string for use in policies, run: 36 | 37 | ```sh 38 | python did_x509.py encode "My Org" 39 | # Output: My%20Org 40 | ``` 41 | 42 | Run tests with: 43 | 44 | ``` 45 | pytest -v test.py 46 | ``` 47 | 48 | ## Contributing 49 | 50 | This project welcomes contributions and suggestions. Please see the [Contribution guidelines](CONTRIBUTING.md). 51 | 52 | ### Trademarks 53 | 54 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft’s Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party’s policies. 55 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /did_x509.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | from typing import List 5 | import argparse 6 | import json 7 | import datetime 8 | from base64 import urlsafe_b64encode 9 | from urllib.parse import unquote, quote 10 | 11 | from cryptography import x509 12 | from cryptography.hazmat.primitives import hashes 13 | from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding 14 | import jwcrypto.jwk 15 | 16 | 17 | NAME_OID_STRINGS = { 18 | # https://datatracker.ietf.org/doc/html/rfc4514.html 19 | "2.5.4.3": "CN", 20 | "2.5.4.7": "L", 21 | "2.5.4.8": "ST", 22 | "2.5.4.10": "O", 23 | "2.5.4.11": "OU", 24 | "2.5.4.6": "C", 25 | "2.5.4.9": "STREET", 26 | } 27 | 28 | 29 | def b64url(data: bytes) -> str: 30 | return urlsafe_b64encode(data).decode().rstrip("=") 31 | 32 | 33 | def pctencode(data: str) -> str: 34 | return quote(data, safe="").replace("~", "%7E") 35 | 36 | 37 | def pctdecode(data: str) -> str: 38 | return unquote(data) 39 | 40 | 41 | def parse_name(name: x509.Name) -> dict: 42 | oids = [item.oid for item in name] 43 | if len(oids) != len(set(oids)): 44 | raise ValueError("duplicates not allowed") 45 | 46 | items = {} 47 | for attribute in name: 48 | oid = attribute.oid.dotted_string 49 | if oid in NAME_OID_STRINGS: 50 | items[NAME_OID_STRINGS[oid]] = attribute.value 51 | else: 52 | items[oid] = attribute.value 53 | return items 54 | 55 | 56 | def parse_extensions(exts: x509.Extensions): 57 | extensions = {} 58 | for ext in exts: 59 | value = ext.value 60 | if isinstance(value, x509.BasicConstraints): 61 | # handled by verify_chain 62 | continue 63 | elif isinstance(value, x509.KeyUsage): 64 | # handled by create_did_document 65 | continue 66 | elif isinstance(value, x509.ExtendedKeyUsage): 67 | ext_name = "eku" 68 | ext_value = [] 69 | for eku in value: 70 | oid = eku.dotted_string 71 | ext_value.append(oid) 72 | elif isinstance(value, x509.SubjectAlternativeName): 73 | ext_name = "san" 74 | ext_value = [] 75 | for san in value: 76 | if isinstance(san, x509.RFC822Name): 77 | ext_value.append(["email", san.value]) 78 | elif isinstance(san, x509.DNSName): 79 | ext_value.append(["dns", san.value]) 80 | elif isinstance(san, x509.UniformResourceIdentifier): 81 | ext_value.append(["uri", san.value]) 82 | elif isinstance(san, x509.DirectoryName): 83 | ext_value.append(["dn", parse_name(san.value)]) 84 | else: 85 | raise RuntimeError(f"unsupported SAN: {san}") 86 | elif ext.oid.dotted_string == "1.3.6.1.4.1.57264.1.1": 87 | ext_name = "fulcio_issuer" 88 | assert isinstance(value, x509.UnrecognizedExtension) 89 | ext_value = value.value.decode("utf-8") 90 | elif not ext.critical: 91 | continue 92 | else: 93 | raise RuntimeError(f"unsupported critical extension: {ext}") 94 | extensions[ext_name] = ext_value 95 | return extensions 96 | 97 | 98 | def decode_certificate(c: x509.Certificate) -> dict: 99 | exts = parse_extensions(c.extensions) 100 | return { 101 | "fingerprint": { 102 | "sha256": b64url(c.fingerprint(hashes.SHA256())), 103 | "sha384": b64url(c.fingerprint(hashes.SHA384())), 104 | "sha512": b64url(c.fingerprint(hashes.SHA512())), 105 | }, 106 | "issuer": parse_name(c.issuer), 107 | "subject": parse_name(c.subject), 108 | "extensions": exts, 109 | } 110 | 111 | 112 | def load_certificate(path) -> x509.Certificate: 113 | with open(path, "rb") as f: 114 | return x509.load_pem_x509_certificate(f.read()) 115 | 116 | 117 | def load_certificate_chain(path) -> List[x509.Certificate]: 118 | sep = "-----END CERTIFICATE-----" 119 | with open(path, "r") as f: 120 | chain = [ 121 | x509.load_pem_x509_certificate((d + sep).encode()) 122 | for d in f.read().split(sep) 123 | if d.strip() 124 | ] 125 | return chain 126 | 127 | 128 | def verify_certificate_is_issued_by( 129 | certificate: x509.Certificate, other: x509.Certificate 130 | ): 131 | if other.subject != certificate.issuer: 132 | raise RuntimeError( 133 | "Certificate issuer does not match subject of issuer certificate" 134 | ) 135 | public_key = other.public_key() 136 | signature = certificate.signature 137 | data = certificate.tbs_certificate_bytes 138 | if isinstance(public_key, rsa.RSAPublicKeyWithSerialization): 139 | public_key.verify( 140 | signature, 141 | data, 142 | padding=padding.PKCS1v15(), 143 | algorithm=certificate.signature_hash_algorithm, 144 | ) 145 | elif isinstance(public_key, ec.EllipticCurvePublicKeyWithSerialization): 146 | public_key.verify( 147 | signature, 148 | data, 149 | signature_algorithm=ec.ECDSA(certificate.signature_hash_algorithm), 150 | ) 151 | else: 152 | raise NotImplementedError("Unsupported public key type") 153 | 154 | 155 | def verify_certificate_in_chain( 156 | chain: List[x509.Certificate], i: int, skip_validity_period_check=False 157 | ): 158 | cert = chain[i] 159 | 160 | if i > 0: 161 | try: 162 | bc_ext = cert.extensions.get_extension_for_class(x509.BasicConstraints) 163 | except x509.ExtensionNotFound: 164 | pass 165 | else: 166 | basic_constraints = bc_ext.value 167 | if not basic_constraints.ca: 168 | raise ValueError(f"Certificate {i} basic constraints: CA bit missing") 169 | if ( 170 | basic_constraints.path_length is not None 171 | and basic_constraints.path_length > i 172 | ): 173 | raise ValueError( 174 | f"Certificate {i} basic constraints: path length constraint violated" 175 | ) 176 | 177 | if not skip_validity_period_check: 178 | now = datetime.datetime.now() 179 | if cert.not_valid_before > now or cert.not_valid_after < now: 180 | raise ValueError(f"Certificate {i} is not valid now") 181 | 182 | 183 | def verify_certificate_chain( 184 | chain: List[x509.Certificate], skip_validity_period_check=False 185 | ): 186 | if len(chain) < 2: 187 | raise ValueError("Certificate chain must have at least two certificates") 188 | for i in range(len(chain) - 1): 189 | verify_certificate_is_issued_by(chain[i], chain[i + 1]) 190 | for i in range(len(chain)): 191 | verify_certificate_in_chain(chain, i, skip_validity_period_check) 192 | 193 | 194 | def check_did_x509(did: str, chain: List[x509.Certificate]): 195 | decoded = [decode_certificate(cert) for cert in chain] 196 | 197 | prefix = "did:x509:0:" 198 | if not did.startswith(prefix): 199 | raise ValueError("invalid did prefix") 200 | parts = did[len(prefix) :].split("::") 201 | [ca_fingerprint_alg, ca_fingerprint] = parts[0].split(":") 202 | policies = [p.split(":", 1) for p in parts[1:]] 203 | if len(policies) == 0: 204 | raise ValueError("no policies specified") 205 | 206 | expected_ca_fingerprints = [ 207 | c["fingerprint"][ca_fingerprint_alg] for c in decoded[1:] 208 | ] 209 | if ca_fingerprint not in expected_ca_fingerprints: 210 | raise ValueError( 211 | f"invalid CA fingerprint, expected one of: {expected_ca_fingerprints}" 212 | ) 213 | 214 | for [name, value] in policies: 215 | if name == "subject": 216 | parts = value.split(":") 217 | if not parts or len(parts) % 2 != 0: 218 | raise ValueError("key-value pairs required") 219 | fields = list(zip(parts[::2], parts[1::2])) 220 | if len(fields) != len(set(fields)): 221 | raise ValueError("duplicate subject fields") 222 | for key, value in fields: 223 | if key not in decoded[0]["subject"]: 224 | raise ValueError(f"invalid subject key: {key}") 225 | value = pctdecode(value) 226 | expected_value = decoded[0]["subject"][key] 227 | if value != expected_value: 228 | raise ValueError( 229 | f"invalid subject value: {key} = {pctencode(value)}, expected: {pctencode(expected_value)}" 230 | ) 231 | 232 | elif name == "san": 233 | parts = value.split(":") 234 | if len(parts) != 2: 235 | raise ValueError("exactly one SAN type and value required") 236 | san_type = parts[0] 237 | san_value = pctdecode(parts[1]) 238 | san = [san_type, san_value] 239 | sans = decoded[0]["extensions"]["san"] 240 | if san not in sans: 241 | raise ValueError(f"invalid SAN: {san}, expected one of: {sans}") 242 | 243 | elif name == "eku": 244 | if "eku" not in decoded[0]["extensions"]: 245 | raise ValueError("no EKU extension in certificate") 246 | eku = value 247 | ekus = decoded[0]["extensions"]["eku"] 248 | if eku not in ekus: 249 | raise ValueError(f"invalid EKU: {eku}, expected one of: {ekus}") 250 | 251 | elif name == "fulcio-issuer": 252 | fulcio_issuer = "https://" + pctdecode(value) 253 | expected_fulcio_issuer = decoded[0]["extensions"]["fulcio_issuer"] 254 | if fulcio_issuer != expected_fulcio_issuer: 255 | raise ValueError( 256 | f"invalid Fulcio issuer: {pctencode(fulcio_issuer)}, expected: {pctencode(expected_fulcio_issuer)}" 257 | ) 258 | 259 | else: 260 | raise ValueError(f"unknown did:x509 policy: {name}") 261 | 262 | 263 | def to_jwk(cert: x509.Certificate) -> dict: 264 | return jwcrypto.jwk.JWK.from_pyca(cert.public_key()).export_public(as_dict=True) 265 | 266 | 267 | def create_did_document(did: str, chain: List[x509.Certificate]): 268 | leaf = chain[0] 269 | doc = { 270 | "@context": "https://www.w3.org/ns/did/v1", 271 | "id": did, 272 | "verificationMethod": [ 273 | { 274 | "id": f"{did}#key-1", 275 | "type": "JsonWebKey2020", 276 | "controller": did, 277 | "publicKeyJwk": to_jwk(leaf), 278 | } 279 | ], 280 | } 281 | 282 | try: 283 | key_usage = leaf.extensions.get_extension_for_class(x509.KeyUsage).value 284 | except x509.ExtensionNotFound: 285 | key_usage = None 286 | 287 | include_assertion_method = key_usage is None or key_usage.digital_signature 288 | include_key_agreement = key_usage is None or key_usage.key_agreement 289 | if include_assertion_method: 290 | doc["assertionMethod"] = [f"{did}#key-1"] 291 | if include_key_agreement: 292 | doc["keyAgreement"] = [f"{did}#key-1"] 293 | if not include_assertion_method and not include_key_agreement: 294 | raise ValueError( 295 | "leaf certificate key usage must include digital signature or key agreement" 296 | ) 297 | 298 | return doc 299 | 300 | 301 | def resolve_did( 302 | did: str, chain: List[x509.Certificate], skip_validity_period_check=False 303 | ) -> dict: 304 | verify_certificate_chain(chain, skip_validity_period_check) 305 | check_did_x509(did, chain) 306 | doc = create_did_document(did, chain) 307 | return doc 308 | 309 | 310 | def cli_resolve(did: str, chain_path: str, skip_validity_period_check: bool): 311 | chain = load_certificate_chain(chain_path) 312 | doc = resolve_did(did, chain, skip_validity_period_check) 313 | print(json.dumps(doc, indent=2)) 314 | 315 | 316 | def cli_convert(chain_path: str): 317 | chain = load_certificate_chain(chain_path) 318 | decoded = [decode_certificate(cert) for cert in chain] 319 | print(json.dumps(decoded, indent=2)) 320 | 321 | 322 | def cli_encode(s: str): 323 | print(pctencode(s)) 324 | 325 | 326 | def main(): 327 | parser = argparse.ArgumentParser() 328 | subparsers = parser.add_subparsers(dest="cmd") 329 | 330 | p = subparsers.add_parser("resolve") 331 | p.add_argument("did", help="The DID to resolve") 332 | p.add_argument( 333 | "--chain", required=True, help="Path to the certificate chain in PEM format" 334 | ) 335 | p.add_argument( 336 | "--skip-validity-period-check", action="store_true", help="Testing only." 337 | ) 338 | p.set_defaults( 339 | func=lambda args: cli_resolve( 340 | args.did, args.chain, args.skip_validity_period_check 341 | ) 342 | ) 343 | 344 | p = subparsers.add_parser("convert") 345 | p.add_argument("chain", help="Path to the certificate chain in PEM format") 346 | p.set_defaults(func=lambda args: cli_convert(args.chain)) 347 | 348 | p = subparsers.add_parser("encode") 349 | p.add_argument("string", help="The string to percent-encode") 350 | p.set_defaults(func=lambda args: cli_encode(args.string)) 351 | 352 | args = parser.parse_args() 353 | args.func(args) 354 | 355 | 356 | if __name__ == "__main__": 357 | main() 358 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cryptography 2 | jwcrypto 3 | pytest 4 | -------------------------------------------------------------------------------- /specification.md: -------------------------------------------------------------------------------- 1 | # did:x509 Method Specification 2 | 3 | Status: DRAFT 4 | 5 | Authors: 6 | - Maik Riechert (Microsoft) 7 | - Antoine Delignat-Lavaud (Microsoft) 8 | 9 | ## Abstract 10 | 11 | The did:x509 method aims to achieve interoperability between existing X.509 solutions and Decentralized Identifiers (DIDs) to support operational models in which a full transition to DIDs is not achievable or desired yet. It supports X.509-only verifiers as well as DID-based verifiers supporting this DID method. 12 | 13 | ## Introduction 14 | 15 | 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). 16 | 17 | 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. 18 | 19 | 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. 20 | 21 | ## Example 22 | 23 | `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::subject:C:US:ST:California:O:My%20Organisation` 24 | 25 | In this example, the identifier pins to a certificate authority using the SHA-256 certificate hash and uses the `subject` policy to express criteria which a leaf certificate's subject must fulfil. This identifier will match any certificate chains with matching leaf certificate subject fields and a matching intermediate or root CA certificate. 26 | 27 | ## JSON data model for X.509 certificate chains 28 | 29 | This section defines a JSON data model for X.509 certificate chains that is the basis for evaluating whether a certificate chain matches a given did:x509 identifier. The language used for defining the JSON data model is CDDL ([RFC 8610](https://www.rfc-editor.org/rfc/rfc8610)). 30 | 31 | ```cddl 32 | CertificateChain = [2*Certificate] ; leaf is first 33 | 34 | Certificate = { 35 | fingerprint: { 36 | ; base64url-encoded hashes of the DER-encoded certificate 37 | sha256: base64url, ; FIPS 180-4, SHA-256 38 | sha384: base64url, ; FIPS 180-4, SHA-384 39 | sha512: base64url ; FIPS 180-4, SHA-512 40 | }, 41 | issuer: Name, ; RFC 5280, Section 4.1.2.4 42 | subject: Name, ; RFC 5280, Section 4.1.2.6 43 | extensions: { 44 | ? eku: [+OID], ; RFC 5280, Section 4.2.1.12 45 | ? san: [+SAN], ; RFC 5280, Section 4.2.1.6 46 | ? fulcio_issuer: tstr ; http://oid-info.com/get/1.3.6.1.4.1.57264.1.1 47 | } 48 | } 49 | 50 | ; X.509 Name as an object of attributes 51 | ; Repeated attribute types are not supported 52 | ; Common attribute types have human-readable labels (see below) 53 | ; Other attribute types use dotted OIDs 54 | ; Values are converted to UTF-8 55 | Name = { 56 | ; See RFC 4514, Section 3, for meaning of common attribute types 57 | ? CN: tstr, 58 | ? L: tstr, 59 | ? ST: tstr, 60 | ? O: tstr, 61 | ? OU: tstr, 62 | ? C: tstr, 63 | ? STREET: tstr, 64 | * OID => tstr 65 | } 66 | 67 | ; base64url-encoded data, see RFC 4648, Section 5 68 | base64url = tstr 69 | 70 | ; ASN.1 Object Identifier 71 | ; Dotted string, for example "1.2.3" 72 | OID = tstr 73 | 74 | ; X.509 Subject Alternative Name 75 | ; Strings are converted to UTF-8 76 | SAN = rfc822Name / DNSName / URI / DirectoryName 77 | rfc822Name = ["email", tstr] ; Example: ["email", "bill@microsoft.com"] 78 | DNSName = ["dns", tstr] ; Example: ["dns", "microsoft.com"] 79 | URI = ["uri", tstr] ; Example: ["uri", "https://microsoft.com"] 80 | DirectoryName = ["dn", Name] ; Example: ["dn", {CN: "Microsoft"}] 81 | ``` 82 | 83 | In the rest of this document, `chain` refers to the certificate chain mapped to the above JSON data model. 84 | 85 | ## Identifier Syntax 86 | 87 | The did:x509 ABNF definition can be found below, which uses the syntax in [RFC 5234](https://www.rfc-editor.org/rfc/rfc5234.html) and the corresponding definitions for `ALPHA` and `DIGIT`. The [W3C DID v1.0 specification](https://www.w3.org/TR/2022/REC-did-core-20220719/) contains the definition for `idchar`. 88 | 89 | ```abnf 90 | did-x509 = "did:" method-name ":" method-specific-id 91 | method-name = "x509" 92 | method-specific-id = version ":" ca-fingerprint-alg ":" ca-fingerprint 1*("::" policy-name ":" policy-value) 93 | version = 1*DIGIT 94 | ca-fingerprint-alg = "sha256" / "sha384" / "sha512" 95 | ca-fingerprint = base64url 96 | policy-name = 1*ALPHA 97 | policy-value = *(1*idchar ":") 1*idchar 98 | base64url = 1*(ALPHA / DIGIT / "-" / "_") 99 | ``` 100 | 101 | In this draft, version is `0`. 102 | 103 | `ca-fingerprint-alg` is one of `sha256`, `sha384`, or `sha512`. 104 | 105 | `ca-fingerprint` is `chain[i].fingerprint[ca-fingerprint-alg]` with i > 0, that is, either an intermediate or root CA certificate. 106 | 107 | `policy-name` is a policy name and `policy-value` is a policy-specific value. 108 | 109 | `::` is used to separate multiple policies from each other. 110 | 111 | The following sections define the policies and their policy-specific syntax. 112 | 113 | Validation of policies is formally defined using [Rego policies](https://www.openpolicyagent.org/docs/latest/policy-language/), though there is no expectation that implementations use Rego. 114 | 115 | The input to the Rego engine is the JSON document `{"did": "", "chain": }`. 116 | 117 | Core Rego policy: 118 | 119 | ```rego 120 | import future.keywords.if 121 | import future.keywords.in 122 | 123 | parse_did(did) := [ca_fingerprint_alg, ca_fingerprint, policies] if { 124 | prefix := "did:x509:0:" 125 | startswith(did, prefix) == true 126 | rest := trim_prefix(did, prefix) 127 | parts := split(rest, "::") 128 | [ca_fingerprint_alg, ca_fingerprint] := split(parts[0], ":") 129 | policies_raw := array.slice(parts, 1, count(parts)) 130 | policies := [y | 131 | some i 132 | s := policies_raw[i] 133 | j := indexof(s, ":") 134 | y := [substring(s, 0, j), substring(s, j+1, -1)] 135 | ] 136 | } 137 | 138 | valid if { 139 | [ca_fingerprint_alg, ca_fingerprint, policies] := parse_did(input.did) 140 | ca := [c | some i; i != 0; c := input.chain[i]] 141 | ca[_].fingerprint[ca_fingerprint_alg] == ca_fingerprint 142 | valid_policies := [i | 143 | some i 144 | [name, value] := policies[i] 145 | validate_policy(name, value) 146 | ] 147 | count(valid_policies) == count(policies) 148 | } 149 | ``` 150 | 151 | 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_policy` function. 152 | 153 | ### Percent-encoding 154 | 155 | Some of the policies that are defined in subsequent sections 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). All characters that are not in the allowed set defined below must be percent-encoded: 156 | 157 | ```abnf 158 | allowed = ALPHA / DIGIT / "-" / "." / "_" 159 | ``` 160 | 161 | Note that most libraries implement percent-encoding in the context of URLs and do NOT encode `~` (`%7E`). 162 | 163 | ### "subject" policy 164 | 165 | ```abnf 166 | policy-name = "subject" 167 | policy-value = key ":" value *(":" key ":" value) 168 | key = label / oid 169 | value = 1*idchar 170 | label = "CN" / "L" / "ST" / "O" / "OU" / "C" / "STREET" 171 | oid = 1*DIGIT *("." 1*DIGIT) 172 | ``` 173 | 174 | `:` are the subject name fields in `chain[0].subject` in any order. Field repetitions are not allowed. Values must be percent-encoded. 175 | 176 | Example: 177 | 178 | `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::subject:C:US:ST:California:L:San%20Francisco:O:GitHub%2C%20Inc.` 179 | 180 | Rego policy: 181 | ```rego 182 | validate_policy(name, value) := true if { 183 | name == "subject" 184 | items := split(value, ":") 185 | count(items) % 2 == 0 186 | subject := {k: v | 187 | some i 188 | i % 2 == 0 189 | k := items[i] 190 | v := urlquery.decode(items[i+1]) 191 | } 192 | count(subject) >= 1 193 | object.subset(input.chain[0].subject, subject) == true 194 | } 195 | ``` 196 | 197 | ### "san" policy 198 | 199 | ```abnf 200 | policy-name = "san" 201 | policy-value = san-type ":" san-value 202 | san-type = "email" / "dns" / "uri" 203 | san-value = 1*idchar 204 | ``` 205 | 206 | `san-type` is the SAN type and must be one of `email`, `dns`, or `uri`. Note that `dn` is not supported. 207 | 208 | `san-value` is the SAN value, percent-encoded. 209 | 210 | The pair [``, ``] is one of the items in `chain[0].extensions.san`. 211 | 212 | Example: 213 | 214 | `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::san:email:bob%40example.com` 215 | 216 | Rego policy: 217 | ```rego 218 | validate_policy(name, value) := true if { 219 | name == "san" 220 | [san_type, san_value_encoded] := split(value, ":") 221 | san_value := urlquery.decode(san_value_encoded) 222 | [san_type, san_value] == input.chain[0].extensions.san[_] 223 | } 224 | ``` 225 | 226 | ### "eku" policy 227 | 228 | ```abnf 229 | policy-name = "eku" 230 | policy-value = eku 231 | eku = oid 232 | oid = 1*DIGIT *("." 1*DIGIT) 233 | ``` 234 | 235 | `eku` is one of the OIDs within `chain[0].extensions.eku`. 236 | 237 | Example: 238 | 239 | `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::eku:1.3.6.1.4.1.311.10.3.13` 240 | 241 | Rego policy: 242 | ```rego 243 | validate_policy(name, value) := true if { 244 | name == "eku" 245 | value == input.chain[0].extensions.eku[_] 246 | } 247 | ``` 248 | 249 | ### "fulcio-issuer" policy 250 | 251 | ```abnf 252 | policy-name = "fulcio-issuer" 253 | policy-value = fulcio-issuer 254 | fulcio-issuer = 1*idchar 255 | ``` 256 | 257 | `fulcio-issuer` is `chain[0].extensions.fulcio_issuer` without leading `https://`, percent-encoded. 258 | 259 | Example: 260 | 261 | `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::fulcio-issuer:accounts.google.com::san:email:bob%40example.com` 262 | 263 | Example 2: 264 | 265 | `did:x509:0:sha256:WE4P5dd8DnLHSkyHaIjhp4udlkF9LqoKwCvu9gl38jk::fulcio-issuer:token.actions.githubusercontent.com::san:uri:https%3A%2F%2Fgithub.com%2Focto-org%2Focto-automation%2F.github%2Fworkflows%2Foidc.yml%40refs%2Fheads%2Fmain` 266 | 267 | Rego policy: 268 | ```rego 269 | validate_policy(name, value) := true if { 270 | name == "fulcio-issuer" 271 | suffix := urlquery.decode(value) 272 | concat("", ["https://", suffix]) == input.chain[0].extensions.fulcio_issuer 273 | } 274 | ``` 275 | 276 | ## DID resolution options 277 | 278 | This DID method introduces a new DID resolution option called `x509chain`: 279 | 280 | Name: `x509chain` 281 | 282 | Value type: string 283 | 284 | The value is constructed as follows: 285 | 286 | 1. Encode each certificate `C` that is part of the chain as the string `b64url(DER(C))`. 287 | 288 | 2. Concatenate the resulting strings in order, separated by comma `","`. 289 | 290 | ## Operations 291 | 292 | ### Create 293 | 294 | Creating a did:x509 identifier is a local operation. The DID must be constructed according to the syntax rules in the previous sections. No other actions are required. 295 | 296 | When constructing a did:x509, the first step is to determine what constitutes a logical identity within a given certificate authority. Concretely, which certificate fields does an authority use to uniquely represent an identity. After that, one or more matching policies must be chosen that allow to express such an identity as faithfully as possible. 297 | 298 | As an example, a certificate authority may exclusively use email addresses as a way to separate identities, and it may use the SAN extension to store the email address. In that case, the did:x509 identifier should be constructed using the `san` policy, for example, `did:x509:0:sha256:::san:email:bob%40example.com`. The certificate may contain other information about the identity, like full name and address, but the primary field that uniquely identifies the identity in this case is just the email address. 299 | 300 | 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` policy should be used. 301 | 302 | 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. 303 | 304 | In all cases, the goal is to craft a did:x509 that is both stable yet not too loose in its policies. An example of a loose did:x509 may be to use the `subject` policy and only include the `O` field without location fields like country (`C`) or state/locality (`ST`). See also the Security and Privacy Considerations section. 305 | 306 | Finally, whether a did:x509 should pin to an intermediate CA instead of a root CA (via the certificate fingerprint) depends on whether there is value in distinguishing between them. Pinning to an intermediate CA typically means that the lifetime of the did:x509 will be shorter, since intermediate CA certificates typically have a shorter validity period than root CA certificates. 307 | 308 | ### Read 309 | 310 | The Read operation takes as input a DID to resolve, together with the `x509chain` DID resolution option. 311 | 312 | The following steps must be used to generate a corresponding DID document: 313 | 314 | 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. 315 | 316 | 2. Check whether the list of certificates form a valid certificate chain using the [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. If any extension, excluding the basic constraints and key usage extensions, is marked critical but is not part of the JSON data model, fail. 317 | 318 | 3. If required by the application, check whether any certificate in the chain is revoked (using CRL, OCSP, or other mechanisms). 319 | 320 | 4. Apply any further application-specific checks, for example disallowing insecure certificate signature algorithms. 321 | 322 | 5. Map the certificate chain to the JSON data model. 323 | 324 | 6. Check whether the DID is valid against the certificate chain in the JSON data model according to the Rego policy (or equivalent rules) defined in this document. 325 | 326 | 7. Extract the public key of the first certificate in the chain. 327 | 328 | 8. Convert the public key to a JSON Web Key. 329 | 330 | 9. Create the following partial DID document: 331 | 332 | ```json 333 | { 334 | "@context": "https://www.w3.org/ns/did/v1", 335 | "id": "", 336 | "verificationMethod": [{ 337 | "id": "#key-1", 338 | "type": "JsonWebKey2020", 339 | "controller": "", 340 | "publicKeyJwk": { 341 | // JSON Web Key 342 | } 343 | }] 344 | } 345 | ``` 346 | 347 | 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: 348 | 349 | ```json 350 | { 351 | "assertionMethod": ["#key-1"] 352 | } 353 | ``` 354 | 355 | 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: 356 | 357 | ```json 358 | { 359 | "keyAgreement": ["#key-1"] 360 | } 361 | ``` 362 | 363 | 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. 364 | 365 | 13. Return the complete DID document. 366 | 367 | ### Update 368 | 369 | This DID Method does not support updating the DID Document, assuming a fixed certificate chain. 370 | 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. 371 | 372 | ### Deactivate 373 | 374 | This DID Method does not support deactivating the DID. 375 | 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, though there is no technical guarantee in this case and the certificate authority can revert its decision. 376 | 377 | ## Security and Privacy Considerations 378 | 379 | ### Identifier ambiguity 380 | 381 | 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 policies 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. 382 | 383 | 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 as well as consumers of signed content to decide whether it is appropriate to trust a given did:x509. 384 | 385 | ### X.509 trust stores 386 | 387 | Typically, a verifier trusts an X.509 certificate by applying [chain validation](https://www.rfc-editor.org/rfc/rfc5280#section-6) (RFC 5280) using a set of certificate authority (CA) certificates as trust store, together with additional application-specific policies. 388 | 389 | This DID method does not require an X.509 trust store but rather relies on verifiers either trusting an individual DID directly or using third-party endorsements for a given DID, like [W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model/), to establish trust. 390 | 391 | By layering this DID method on top of X.509, verifiers are free to use traditional chain validation (for example, verifiers unaware of DID), or rely on DID as an ecosystem to establish trust. 392 | 393 | ## References 394 | 395 | ### Normative references 396 | 397 | [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. 398 | 399 | [RFC 8610 - Concise Data Definition Language (CDDL)](https://www.rfc-editor.org/rfc/rfc8610). H. Birkholz, C. Vigano, C. Bormann. IETF. June 2019. Proposed Standard. 400 | 401 | [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. 402 | 403 | [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. 404 | 405 | [RFC 4648 - The Base16, Base32, and Base64 Data Encodings](https://www.rfc-editor.org/rfc/rfc4648). S. Josefsson. IETF. October 2006. Proposed Standard. 406 | 407 | [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. 408 | 409 | [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. 410 | 411 | [FIPS 180-4 - Secure Hash Standard](https://csrc.nist.gov/publications/detail/fips/180/4/final). NIST. August 2015. FIPS Publication. 412 | 413 | ### Informative references 414 | 415 | [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 Stöcker (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. 416 | 417 | [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. 418 | 419 | [Rego Policy Language](https://www.openpolicyagent.org/docs/latest/policy-language/). Open Policy Agent contributors. 420 | 421 | [Fulcio](https://github.com/sigstore/fulcio). Fulcio contributors. 422 | -------------------------------------------------------------------------------- /test-data/README.txt: -------------------------------------------------------------------------------- 1 | The test certificates in this folder are from public sources: 2 | 3 | - ms-*.pem are public Microsoft certificates 4 | - fulcio-*.pem are public certificates from the Fulcio CT log 5 | -------------------------------------------------------------------------------- /test-data/fulcio-email.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICEDCCAZagAwIBAgITIK73YV52uJcmxL9ZeKo+wZbm3zAKBggqhkjOPQQDAzAq 3 | MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIy 4 | MDgwOTEyNDcxNFoXDTIyMDgwOTEyNTcxM1owADBZMBMGByqGSM49AgEGCCqGSM49 5 | AwEHA0IABPmQP4xa5TxXg/HkUrw3CUcqmW6F5eEBQSU8tcGMIIzIHnMCVwTa4uoq 6 | ZGgdCN+0Erk+toNwkGG+pS3Qc2EocbejgcQwgcEwDgYDVR0PAQH/BAQDAgeAMBMG 7 | A1UdJQQMMAoGCCsGAQUFBwMDMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFJITi/Hz 8 | 4QkD5qz2gKoi4UBYfaRRMB8GA1UdIwQYMBaAFFjAHl+RRaVmqXrMkKGTItAqxcX6 9 | MB4GA1UdEQEB/wQUMBKBEGlnYXJjaWFAc3VzZS5jb20wLAYKKwYBBAGDvzABAQQe 10 | aHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMAoGCCqGSM49BAMDA2gAMGUC 11 | MQDPO3n+JgPlTbXSQy942esSy7KQ6OI4N9Q9MsqN4UR2tkML7tUm5feKTQUkfwTs 12 | 6BsCMADuoj3fJGAiRDMlSphfrZ0tAEIFaVZtJmvKWXpElHQo9y39W0w9bJTEVgTa 13 | 4xvX4w== 14 | -----END CERTIFICATE----- 15 | -----BEGIN CERTIFICATE----- 16 | MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw 17 | KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y 18 | MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl 19 | LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7 20 | XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex 21 | X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j 22 | YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY 23 | wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ 24 | KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM 25 | WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9 26 | TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /test-data/fulcio-github-actions.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDTzCCAtSgAwIBAgIUAOuDsEYQXN1cbwfqYOy5ADUqqDAwCgYIKoZIzj0EAwMw 3 | KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y 4 | MjA2MDkwMjM4MTJaFw0yMjA2MDkwMjQ4MTFaMAAwWTATBgcqhkjOPQIBBggqhkjO 5 | PQMBBwNCAAR8Qujd0dQ2F7uSANd+0M7VXVkhXlGvFERJc1oPxk+R/ApEantKDVd/ 6 | 5/+e2AOoS1ltjcZkCt1oP1mAZ/+2G3i6o4ICADCCAfwwDgYDVR0PAQH/BAQDAgeA 7 | MBMGA1UdJQQMMAoGCCsGAQUFBwMDMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFLcK 8 | jVaYdV1BcZimzcrE8foM3xAPMB8GA1UdIwQYMBaAFFjAHl+RRaVmqXrMkKGTItAq 9 | xcX6MIGFBgNVHREBAf8EezB5hndodHRwczovL2dpdGh1Yi5jb20vYnJlbmRhbmNh 10 | c3NlbGxzL21jdy1jb250aW51b3VzLWRlbGl2ZXJ5LWxhYi1maWxlcy8uZ2l0aHVi 11 | L3dvcmtmbG93cy9mYWJyaWthbS13ZWIueW1sQHJlZnMvaGVhZHMvbWFpbjAWBgor 12 | BgEEAYO/MAECBAhzY2hlZHVsZTA/BgorBgEEAYO/MAEFBDFicmVuZGFuY2Fzc2Vs 13 | bHMvbWN3LWNvbnRpbnVvdXMtZGVsaXZlcnktbGFiLWZpbGVzMDkGCisGAQQBg78w 14 | AQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20w 15 | NgYKKwYBBAGDvzABAwQoMTIxMDQ4ZDVkMmViNTc3OTg3NTFlY2Y1N2FjNWNlNTg2 16 | NmVmMWEyZDAUBgorBgEEAYO/MAEEBAZEb2NrZXIwHQYKKwYBBAGDvzABBgQPcmVm 17 | cy9oZWFkcy9tYWluMAoGCCqGSM49BAMDA2kAMGYCMQDfg/L1SnH1EdkPtmPN197q 18 | Y+oc+mdFz1ica3Xlx8/En/JcxHUtBkHx1Lv5w++Wg3sCMQC4YKWGL0AfIIES1XT7 19 | b96WTdxBSnBx2Nvrqg0VuzLvM+wIjaz3dXWf41eKB2sXvwo= 20 | -----END CERTIFICATE----- 21 | -----BEGIN CERTIFICATE----- 22 | MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw 23 | KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y 24 | MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl 25 | LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7 26 | XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex 27 | X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j 28 | YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY 29 | wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ 30 | KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM 31 | WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9 32 | TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /test-data/ms-code-signing.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIF/zCCA+egAwIBAgITMwAAAs+gJZDjEwTvFQAAAAACzzANBgkqhkiG9w0BAQsF 3 | ADB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH 4 | UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQD 5 | Ex9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMB4XDTIyMDUxMjIwNDYw 6 | NFoXDTIzMDUxMTIwNDYwNFowdDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp 7 | bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw 8 | b3JhdGlvbjEeMBwGA1UEAxMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMIIBIjANBgkq 9 | hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs9HduD2rvmO+SGksB4HR+qvSK379St8N 10 | nUZBH8xBiQvt2zONOLUHWQibeBW4NLUfHfzMaOM77RhNlqPNiDRKhChlG1aHqEHS 11 | AaQBGrmr0ULGIzq+1YvqQufMGYBFfq0sc10UdvWqT0RjwkPQTu4bjg37zSYF9OcG 12 | xS9uGnPMdWRM0ThOsYUcDmMoCaJRebsLUBpMmYXkcUYXJrcSGAaUNd0wjhwIpEog 13 | OD+AbWW/7TPZOl+JciMj40a78EEXIc2p06lWHfe5hegQ7uGIlSAPG6zDzjhjNkzE 14 | 63/+GoqJU+6QLazbL5/y27ZDUAEYJokbb305A+dOp930CjTar3BvWQIDAQABo4IB 15 | fjCCAXowHwYDVR0lBBgwFgYKKwYBBAGCNwoDFQYIKwYBBQUHAwMwHQYDVR0OBBYE 16 | FHs4/z9sVQLrJJTk5iEaOQwyHU0iMFAGA1UdEQRJMEekRTBDMSkwJwYDVQQLEyBN 17 | aWNyb3NvZnQgT3BlcmF0aW9ucyBQdWVydG8gUmljbzEWMBQGA1UEBRMNMjMwMjE3 18 | KzQ3MDUzMjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8E 19 | TTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9N 20 | aWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBR 21 | BggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0 22 | cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAw 23 | DQYJKoZIhvcNAQELBQADggIBAIBiYcrj5Ph8uwFKZXw0eCS9qv2lk4lZY4Semy2D 24 | 4sfKDNUqKsqP5Q0zJcAq3Z+uEKc9Q8boxkm9/3PPESQKWhTRqLY+LL2XTjbm1S/L 25 | AhtQ09ftHkxwienGU+Xo8ntz6Z7iQV2xCqjTMRWGFysEKgMgdAMPftWPXNa9k1G9 26 | qEJpPcCLeiM6UEJdxnRDHKgDSugW4fYvcEXlOJJXn/VZr4fFJZ+xLGT+US/NwGwb 27 | 8DdoUYls2u5o2250nm0TA0cZkJCzrxzV6Fptv14jbPcTZpRU6D0zGSSLPaM2cA/A 28 | Q3yxRi9FZOpcbrJM+2Rp6aufmyxUgIN6MvG2IH2D++Xq3a4Zy+Gmce9thBRBff1i 29 | IROq6CdGJHbOVbfdivV3L7qBD9pQYqSKitq4fJV95iYEchgMoXGwkJwagXix+f8g 30 | jnOmlSjysSwzAmDwtAxUkX+lNoU5xUJLwf9/4nIXp7drjWptpn9IIiARLPFxLRYg 31 | 7S9digox7quSKM/xXb1bFzp346lwjuvK+QHC8pUOF8OojQ0YAZ+Q0EKKukchQ3wF 32 | 7RiHk/INqYgEFli/xpMzwVM2k91UlArvYylUKLGDGy8QabMosUrZdNQvBCWiePYR 33 | AaJR5t+IR5QeBNdaKEqh2EQ/VzCu7J247Q3UrZrPLUJ9bGp2INwL8jynhVOeZteW 34 | CEKV 35 | -----END CERTIFICATE----- 36 | -----BEGIN CERTIFICATE----- 37 | MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkG 38 | A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx 39 | HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9z 40 | b2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1 41 | OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz 42 | aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv 43 | cnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAy 44 | MDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqC 45 | EE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC 46 | 04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlIm 47 | Ei/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPe 48 | Bw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb 49 | 2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXD 50 | OW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yx 51 | kq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA 52 | 4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uD 53 | jexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ec 54 | XL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4Ta 55 | sIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQD 56 | AgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIE 57 | DB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNV 58 | HSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklo 59 | dHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29D 60 | ZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEF 61 | BQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29D 62 | ZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQB 63 | gjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br 64 | aW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBn 65 | AGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqG 66 | SIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8u 67 | LD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxC 68 | i1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQ 69 | u6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n 70 | 7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVY 71 | ODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38c 72 | bxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3 73 | BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmA 74 | H9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5 75 | GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF6 76 | 70EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzQ== 77 | -----END CERTIFICATE----- 78 | -----BEGIN CERTIFICATE----- 79 | MIIF7TCCA9WgAwIBAgIQP4vItfyfspZDtWnWbELhRDANBgkqhkiG9w0BAQsFADCB 80 | iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl 81 | ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMp 82 | TWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEw 83 | MzIyMjIwNTI4WhcNMzYwMzIyMjIxMzA0WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV 84 | BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv 85 | c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm 86 | aWNhdGUgQXV0aG9yaXR5IDIwMTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK 87 | AoICAQCygEGqNThNE3IyaCJNuLLx/9VSvGzH9dJKjDbu0cJcfoyKrq8TKG/Ac+M6 88 | ztAlqFo6be+ouFmrEyNozQwph9FvgFyPRH9dkAFSWKxRxV8qh9zc2AodwQO5e7BW 89 | 6KPeZGHCnvjzfLnsDbVU/ky2ZU+I8JxImQxCCwl8MVkXeQZ4KI2JOkwDJb5xalwL 90 | 54RgpJki49KvhKSn+9GY7Qyp3pSJ4Q6g3MDOmT3qCFK7VnnkH4S6Hri0xElcTzFL 91 | h93dBWcmmYDgcRGjuKVB4qRTufcyKYMME782XgSzS0NHL2vikR7TmE/dQgfI6B0S 92 | /Jmpaz6SfsjWaTr8ZL22CZ3K/QwLopt3YEsDlKQwaRLWQi3BQUzK3Kr9j1uDRprZ 93 | /LHR47PJf0h6zSTwQY9cdNCssBAgBkm3xy0hyFfj0IbzA2j70M5xwYmZSmQBbP3s 94 | MJHPQTySx+W6hh1hhMdfgzlirrSSL0fzC/hV66AfWdC7dJse0Hbm8ukG1xDo+mTe 95 | acY1logC8Ea4PyeZb8txiSk190gWAjWP1Xl8TQLPX+uKg09FcYj5qQ1OcunCnAfP 96 | SRtOBA5jUYxe2ADBVSy2xuDCZU7JNDn1nLPEfuhhbhNfFcRf2X7tHc7uROzLLoax 97 | 7Dj2cO2rXBPB2Q8Nx4CyVe0096yb5MPa50c8prWPMd/FS6/r8QIDAQABo1EwTzAL 98 | BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUci06AjGQQ7kU 99 | BU7h6qfHMdEjiTQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQELBQADggIB 100 | AH9yzw+3xRXbm8BJyiZb/p4T5tPw0tuXX/JLP02zrhmu7deXoKzvqTqjwkGw5biR 101 | nhOBJAPmCf0/V0A5ISRW0RAvS0CpNoZLtFNXmvvxfomPEf4YbFGq6O0JlbXlccmh 102 | 6Yd1phV/yX43VF50k8XDZ8wNT2uoFwxtCJJ+i92Bqi1wIcM9BhS7vyRep4TXPw8h 103 | Ir1LAAbblxzYXtTFC1yHblCk6MM4pPvLLMWSZpuFXst6bJN8gClYW1e1QGm6CHmm 104 | ZGIVnYeWRbVmIyADixxzoNOieTPgUFmG2y/lAiXqcyqfABTINseSO+lOAOzYVgm5 105 | M0kS0lQLAausR7aRKX1MtHWAUgHoyoL2n8ysnI8X6i8msKtyrAv+nlEex0NVZ09R 106 | s1fWtuzuUrc66U7h14GIvE+OdbtLqPA1qibUZ2dJsnBMO5PcHd94kIZysjik0dyS 107 | TclY6ysSXNQ7roxrsIPlAT/4CTL2kzU0Iq/dNw13CYArzUgA8YyZGUcFAenRv9FO 108 | 0OYoQzeZpApKCNmacXPSqs0xE2N2oTdvkjgefRI8ZjLny23h/FKJ3crWZgWalmG+ 109 | oijHHKOnNlA8OqTfSm7mhzvO6/DggTedEzxSjr25HTTGHdUKaj2YKXCMiSrRq4IQ 110 | SB/c9O+lxbtVGjhjhE63bK2VVOxlIhBJF7jAHscPrFRH 111 | -----END CERTIFICATE----- 112 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | import pytest 5 | from did_x509 import load_certificate_chain, resolve_did 6 | 7 | 8 | def test_root_ca(): 9 | chain = load_certificate_chain("test-data/ms-code-signing.pem") 10 | 11 | resolve_did( 12 | r"did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::subject:CN:Microsoft%20Corporation", 13 | chain, 14 | skip_validity_period_check=True, 15 | ) 16 | 17 | 18 | def test_intermediate_ca(): 19 | chain = load_certificate_chain("test-data/ms-code-signing.pem") 20 | 21 | resolve_did( 22 | r"did:x509:0:sha256:VtqHIq_ZQGb_4eRZVHOkhUiSuEOggn1T-32PSu7R4Ys::subject:CN:Microsoft%20Corporation", 23 | chain, 24 | skip_validity_period_check=True, 25 | ) 26 | 27 | 28 | def test_invalid_leaf_ca(): 29 | chain = load_certificate_chain("test-data/ms-code-signing.pem") 30 | 31 | with pytest.raises(ValueError): 32 | resolve_did( 33 | r"did:x509:0:sha256:UXBZJ2K9iZ6KYBN7WzuRXxqz-3CB2nKpuhEYghJPDww::subject:CN:Microsoft%20Corporation", 34 | chain, 35 | skip_validity_period_check=True, 36 | ) 37 | 38 | 39 | def test_invalid_ca(): 40 | chain = load_certificate_chain("test-data/ms-code-signing.pem") 41 | 42 | with pytest.raises(ValueError): 43 | resolve_did( 44 | r"did:x509:0:sha256:abc::CN:Microsoft%20Corporation", 45 | chain, 46 | skip_validity_period_check=True, 47 | ) 48 | 49 | 50 | def test_multiple_policies(): 51 | chain = load_certificate_chain("test-data/ms-code-signing.pem") 52 | 53 | resolve_did( 54 | r"did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::eku:1.3.6.1.5.5.7.3.3::eku:1.3.6.1.4.1.311.10.3.21", 55 | chain, 56 | skip_validity_period_check=True, 57 | ) 58 | 59 | 60 | def test_subject(): 61 | chain = load_certificate_chain("test-data/ms-code-signing.pem") 62 | 63 | resolve_did( 64 | r"did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::subject:CN:Microsoft%20Corporation", 65 | chain, 66 | skip_validity_period_check=True, 67 | ) 68 | 69 | 70 | def test_subject_invalid_name(): 71 | chain = load_certificate_chain("test-data/ms-code-signing.pem") 72 | 73 | with pytest.raises(ValueError): 74 | resolve_did( 75 | r"did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::subject:CN:MicrosoftCorporation", 76 | chain, 77 | skip_validity_period_check=True, 78 | ) 79 | 80 | 81 | def test_subject_duplicate_field(): 82 | chain = load_certificate_chain("test-data/ms-code-signing.pem") 83 | 84 | with pytest.raises(ValueError): 85 | resolve_did( 86 | r"did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::subject:CN:Microsoft%20Corporation:CN:Microsoft%20Corporation", 87 | chain, 88 | skip_validity_period_check=True, 89 | ) 90 | 91 | 92 | def test_san(): 93 | chain = load_certificate_chain("test-data/fulcio-email.pem") 94 | 95 | resolve_did( 96 | r"did:x509:0:sha256:O6e2zE6VRp1NM0tJyyV62FNwdvqEsMqH_07P5qVGgME::san:email:igarcia%40suse.com", 97 | chain, 98 | skip_validity_period_check=True, 99 | ) 100 | 101 | 102 | def test_san_invalid_type(): 103 | chain = load_certificate_chain("test-data/fulcio-email.pem") 104 | 105 | with pytest.raises(ValueError): 106 | resolve_did( 107 | r"did:x509:0:sha256:O6e2zE6VRp1NM0tJyyV62FNwdvqEsMqH_07P5qVGgME::san:uri:igarcia%40suse.com", 108 | chain, 109 | skip_validity_period_check=True, 110 | ) 111 | 112 | 113 | def test_san_invalid_value(): 114 | chain = load_certificate_chain("test-data/fulcio-email.pem") 115 | 116 | with pytest.raises(ValueError): 117 | resolve_did( 118 | r"did:x509:0:sha256:O6e2zE6VRp1NM0tJyyV62FNwdvqEsMqH_07P5qVGgME::email:bob%40example.com", 119 | chain, 120 | skip_validity_period_check=True, 121 | ) 122 | 123 | 124 | def test_eku(): 125 | chain = load_certificate_chain("test-data/ms-code-signing.pem") 126 | 127 | resolve_did( 128 | r"did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::eku:1.3.6.1.5.5.7.3.3", 129 | chain, 130 | skip_validity_period_check=True, 131 | ) 132 | 133 | 134 | def test_eku_invalid_value(): 135 | chain = load_certificate_chain("test-data/ms-code-signing.pem") 136 | 137 | with pytest.raises(ValueError): 138 | resolve_did( 139 | r"did:x509:0:sha256:hH32p4SXlD8n_HLrk_mmNzIKArVh0KkbCeh6eAftfGE::eku:1.2.3", 140 | chain, 141 | skip_validity_period_check=True, 142 | ) 143 | 144 | 145 | def test_fulcio_issuer_with_email_san(): 146 | chain = load_certificate_chain("test-data/fulcio-email.pem") 147 | 148 | resolve_did( 149 | r"did:x509:0:sha256:O6e2zE6VRp1NM0tJyyV62FNwdvqEsMqH_07P5qVGgME::fulcio-issuer:github.com%2Flogin%2Foauth::san:email:igarcia%40suse.com", 150 | chain, 151 | skip_validity_period_check=True, 152 | ) 153 | 154 | 155 | def test_fulcio_issuer_with_uri_san(): 156 | chain = load_certificate_chain("test-data/fulcio-github-actions.pem") 157 | 158 | resolve_did( 159 | r"did:x509:0:sha256:O6e2zE6VRp1NM0tJyyV62FNwdvqEsMqH_07P5qVGgME::fulcio-issuer:token.actions.githubusercontent.com::san:uri:https%3A%2F%2Fgithub.com%2Fbrendancassells%2Fmcw-continuous-delivery-lab-files%2F.github%2Fworkflows%2Ffabrikam-web.yml%40refs%2Fheads%2Fmain", 160 | chain, 161 | skip_validity_period_check=True, 162 | ) 163 | --------------------------------------------------------------------------------