@owf/x509 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,164 @@
1
- # Temporary Holding Version
1
+ # @owf/x509
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@owf/x509)](https://npmjs.com/package/@owf/x509)
4
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/openwallet-foundation-labs/identity-common-ts/blob/main/LICENSE)
5
+
6
+ Environment agnostic X.509 certificates and certificate revocation lists (RFC 5280): parse, create, verify and check
7
+ revocation.
8
+
9
+ The package is pure TypeScript on top of [`@peculiar/asn1-x509`](https://www.npmjs.com/package/@peculiar/asn1-x509) and
10
+ [`asn1js`](https://www.npmjs.com/package/asn1js). It uses neither Node.js built-ins, global crypto nor
11
+ `reflect-metadata`, so it runs on servers, in browsers and in React Native. Signing and signature verification are
12
+ callbacks, so private keys never enter the package and can stay in WebCrypto, a KMS or an HSM.
13
+
14
+ Profiles such as [`@owf/eudi-access-certificate`](../eudi-access-certificate) (ETSI TS 119 411-8) are built on top of it.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @owf/x509
20
+ ```
21
+
22
+ ## Certificates
23
+
24
+ ### Parsing
25
+
26
+ ```typescript
27
+ import { parseCertificate } from '@owf/x509'
28
+
29
+ const cert = parseCertificate(pem) // PEM, base64 DER or DER bytes
30
+
31
+ cert.subject.commonName
32
+ cert.subject.rfc4514 // 'CN=Example,O=Example GmbH,C=DE'
33
+ cert.serialNumber // lowercase hex
34
+ cert.notBefore / cert.notAfter
35
+ cert.publicKey.jwk // EC (P-256/384/521) and RSA keys as JWK
36
+ cert.policies // [{ oid, cpsUris, userNotices }]
37
+ cert.subjectAlternativeNames // { dnsNames, uris, emails, ipAddresses, directoryNames, otherNames }
38
+ cert.keyUsage, cert.extendedKeyUsage, cert.basicConstraints
39
+ cert.subjectKeyIdentifier, cert.authorityKeyIdentifier
40
+ cert.crlDistributionPoints, cert.ocspUrls, cert.caIssuersUrls
41
+ cert.qcStatements // RFC 3739 / ETSI EN 319 412-5
42
+ cert.extensions // every extension as { oid, critical, value }
43
+ ```
44
+
45
+ ### Verifying a signature
46
+
47
+ ```typescript
48
+ import { verifyCertificateSignature } from '@owf/x509'
49
+
50
+ const ok = await verifyCertificateSignature(cert, issuerPem, async ({ data, signatureP1363, publicKey }) => {
51
+ const key = await crypto.subtle.importKey('jwk', publicKey.jwk!, { name: 'ECDSA', namedCurve: 'P-256' }, false, [
52
+ 'verify',
53
+ ])
54
+ return crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, signatureP1363, data)
55
+ })
56
+ ```
57
+
58
+ The verifier receives the signed bytes, the signature both as encoded (DER for ECDSA) and as IEEE P1363 (`r || s`, what
59
+ WebCrypto expects), and the issuer public key as SPKI and JWK. Issuer name and key identifiers have to match as well.
60
+ This checks a single link. Path building to a trust anchor is up to the caller.
61
+
62
+ ### Creating
63
+
64
+ ```typescript
65
+ import { createCertificate, stringOtherName, ATTRIBUTE_TYPES } from '@owf/x509'
66
+ import { digest } from '@owf/crypto'
67
+
68
+ const cert = await createCertificate({
69
+ subject: [
70
+ ['countryName', 'DE'],
71
+ ['organizationName', 'Example GmbH'],
72
+ ['commonName', 'Example Service'],
73
+ ],
74
+ subjectPublicKey: publicJwk, // or SPKI bytes; private JWKs are rejected
75
+ issuerCertificate: caPem, // omit for a self-signed certificate
76
+ serialNumber: randomSerialHex,
77
+ notBefore: new Date(),
78
+ notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
79
+ signatureAlgorithm: 'ES256', // ES256, ES384, ES512, RS256, RS384, RS512
80
+ certificatePolicies: [{ oid: '1.2.3.4', cpsUris: ['https://ca.example.com/cps'] }],
81
+ subjectAlternativeNames: {
82
+ dnsNames: ['service.example.com'],
83
+ otherNames: [stringOtherName(ATTRIBUTE_TYPES.telephoneNumber, '+4930123456')],
84
+ },
85
+ crlDistributionPoints: ['https://ca.example.com/crl'],
86
+ hasher: digest, // optional: subject key identifier (RFC 7093 method 1)
87
+ signer: async (tbs) => new Uint8Array(await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, caKey, tbs)),
88
+ signatureEncoding: 'ieee-p1363', // default (WebCrypto); use 'der' for most KMS / HSM APIs
89
+ })
90
+ ```
91
+
92
+ Defaults: `basicConstraints` `cA=false` and `keyUsage` `digitalSignature` for end-entity certificates, and
93
+ `keyCertSign, crlSign` for CAs. Both are marked critical. The authority key identifier is taken from the issuer
94
+ certificate. Custom extensions can be appended through `extensions`.
95
+
96
+ For keys in a separate system, split creation into two steps:
97
+
98
+ ```typescript
99
+ const prepared = await prepareCertificate(template)
100
+ const signature = await remoteSigningService.sign(prepared.tbs)
101
+ const cert = assembleCertificate(prepared, signature, 'der')
102
+ ```
103
+
104
+ ## CRLs
105
+
106
+ ### Parsing and checking revocation
107
+
108
+ ```typescript
109
+ import { checkRevocation, parseCrl } from '@owf/x509'
110
+
111
+ // Fetch the CRL yourself, e.g. from cert.crlDistributionPoints
112
+ const crl = parseCrl(new Uint8Array(await (await fetch(cert.crlDistributionPoints[0])).arrayBuffer()))
113
+
114
+ const status = await checkRevocation({ certificate: cert, crl, crlIssuerCertificate: issuerPem, verifier })
115
+ // { status: 'good' }
116
+ // { status: 'revoked', revocationDate, reason?, invalidityDate? }
117
+ // { status: 'unknown', reason, message }
118
+ ```
119
+
120
+ `checkRevocation` verifies the CRL signature first. The CRL issuer's key usage has to allow `cRLSign` if it is present.
121
+ Then it returns `unknown` instead of guessing in these cases:
122
+
123
+ | Reason | When |
124
+ |--------|------|
125
+ | `invalid_signature` | The CRL signature does not verify, or the CRL is not issued by the given certificate |
126
+ | `issuer_mismatch` | The CRL is not issued by the certificate issuer, or is signed by a different key |
127
+ | `crl_not_yet_valid` / `crl_expired` | `now` is before `thisUpdate` or after `nextUpdate` |
128
+ | `out_of_scope` | The issuing distribution point excludes the certificate (user/CA/attribute only, different distribution point, only some reasons) |
129
+ | `delta_crl` | The CRL is a delta CRL, which needs its base CRL |
130
+ | `unsupported_critical_extension` | The CRL or the matching entry has a critical extension this package does not understand |
131
+
132
+ Indirect CRLs (with `certificateIssuer` entries) are supported. Entries with reason `removeFromCRL` count as not
133
+ revoked. `getRevocationStatus(cert, crl)` performs the same lookup without checking the signature, for CRLs you have
134
+ already verified.
135
+
136
+ `parseCrl` limits the number of entries to 100 000 by default, to protect against resource exhaustion by untrusted
137
+ input. Raise it with `parseCrl(input, { maxEntries })`.
138
+
139
+ ### Creating
140
+
141
+ ```typescript
142
+ import { createCrl } from '@owf/x509'
143
+
144
+ const crl = await createCrl({
145
+ issuerCertificate: caPem,
146
+ thisUpdate: new Date(),
147
+ nextUpdate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
148
+ crlNumber: 42n,
149
+ signatureAlgorithm: 'ES256',
150
+ revokedCertificates: [{ serialNumber: '0badc0de', revocationDate: new Date(), reason: 'keyCompromise' }],
151
+ issuingDistributionPoint: { distributionPointUris: ['https://ca.example.com/crl'], onlyContainsUserCerts: true },
152
+ signer,
153
+ })
154
+
155
+ crl.pem // -----BEGIN X509 CRL-----
156
+ ```
157
+
158
+ `prepareCrl` / `assembleCrl` provide the same two-step flow as for certificates.
159
+
160
+ ## Out of scope
161
+
162
+ - Certification path building and validation (RFC 5280 clause 6)
163
+ - OCSP
164
+ - Certificate signing requests