@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 +163 -2
- package/dist/index.cjs +1169 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +600 -0
- package/dist/index.d.mts +600 -0
- package/dist/index.mjs +1115 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -4
package/README.md
CHANGED
|
@@ -1,3 +1,164 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @owf/x509
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://npmjs.com/package/@owf/x509)
|
|
4
|
+
[](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
|