@noz-ele/edgca 0.5.2 → 0.6.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 +74 -26
- package/SECURITY.md +3 -2
- package/dist/ca.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/issuer.d.ts +3 -0
- package/dist/issuer.d.ts.map +1 -0
- package/dist/issuer.js +1 -0
- package/dist/parser.d.ts +23 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +273 -58
- package/dist/verify.d.ts +35 -30
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +221 -32
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -2,40 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
> [日本語](https://github.com/noz-ele/EdgCA/blob/main/docs/jp/README.md) | English
|
|
4
4
|
|
|
5
|
-
EdgCA is a small TypeScript library for issuing mTLS client certificates and document-signing certificates from a self-managed CA on Cloudflare Workers-compatible runtimes.
|
|
5
|
+
EdgCA is a small TypeScript library for issuing mTLS client certificates and document-signing certificates from a self-managed CA, and for bounded certificate validation against explicit trust anchors, on Cloudflare Workers-compatible runtimes.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **WebCrypto-only, zero runtime dependencies.** All cryptographic operations go through `globalThis.crypto.subtle`. The same code runs on Cloudflare Workers, Node.js 20+, and modern browsers without polyfills or bundler shims.
|
|
10
|
-
- **Lightweight.** v0.
|
|
10
|
+
- **Lightweight.** v0.6.0 — tarball **49.8 kB** · unpacked **192.8 kB** · 76 files. No transitive dependencies; the CLI uses only `node:util.parseArgs`. (Re-measured on every release.)
|
|
11
11
|
- **CA hierarchy (two-level).** Create a self-signed root CA and, optionally, issue an intermediate CA from it. Three or more levels of intermediates are intentionally out of scope.
|
|
12
12
|
- **PFX (PKCS#12) bundling.** Wrap a cert + private key (and optional chain) into a password-protected `.pfx` / `.p12` for OS keystore import (Win11+, macOS 15+, iOS/iPadOS 18+, modern Linux). Algorithm-agnostic — accepts arbitrary PKCS#8 DER bytes (ECDSA, RSA, Ed25519, …).
|
|
13
13
|
- **mTLS client certificate issuance.** Issue a leaf with internal key generation, or from a caller-managed key via the CSR path below.
|
|
14
14
|
- **PKCS#10 CSR support.** Build a CSR (with proof-of-possession), parse a received CSR (subject, requested SAN, public key, raw extensions/attributes), verify its POP signature, and issue a cert from a CSR's public key without ever handling the private key.
|
|
15
15
|
- **Document-signing certificates (RFC 9336).** Issue a leaf with EKU `id-kp-documentSigning`, usable as the signer cert for CAdES / CMS / ASiC tooling (containers themselves are built separately).
|
|
16
|
-
- **Issuance check.** Decide whether a received client certificate was issued by your own CA (issuer-identity match — not full mTLS verification).
|
|
16
|
+
- **Issuance check.** Decide whether a received client certificate was issued by your own CA (issuer-identity match — not full mTLS verification).
|
|
17
|
+
- **Bounded chain validation.** Validate a caller-ordered `leaf → intermediate → trusted root` chain, including signatures, validity, CA constraints, and target purpose. Automatic PKI path building and revocation are intentionally out of scope.
|
|
17
18
|
- **PEM/DER encode/decode** for certificates and PKCS#10 CSRs.
|
|
18
19
|
- **Secret key hygiene at the API boundary.** Private keys flow through the public API only as `CryptoKey` (issuance path) or `Uint8Array` PKCS#8 bytes (`exportPkcs12`); never as `string`. JS strings are immutable and stay on the heap until GC, so they cannot be wiped — secret material must not be held in that form. PEM ↔ CryptoKey conversion is the caller's job (so the lifetime of any string representation stays under caller control).
|
|
19
20
|
|
|
20
21
|
### Supported algorithms
|
|
21
22
|
|
|
22
|
-
- **Issuance layer**: ECDSA on NIST P-256 / P-384 / P-521 (with the standard SHA-256 / SHA-384 / SHA-512 pairings). RSA, EdDSA, and other curves are intentionally out of scope at issuance.
|
|
23
|
+
- **Issuance layer**: ECDSA on NIST P-256 / P-384 / P-521 (with the standard SHA-256 / SHA-384 / SHA-512 pairings). RSA, EdDSA, and other curves are intentionally out of scope at issuance.
|
|
24
|
+
- **Verification layer**: the same ECDSA NIST P-256 / P-384 / P-521 set. It is intentionally not a general algorithm verifier.
|
|
23
25
|
- **PFX bundling (`exportPkcs12`)**: algorithm-agnostic — accepts any PKCS#8 DER bytes verbatim.
|
|
24
26
|
|
|
25
|
-
> ⚠ **Not a PKI runtime.**
|
|
27
|
+
> ⚠ **Not a general PKI runtime.** Chain validation is limited to a caller-ordered hierarchy no deeper than `root → intermediate → leaf`, with explicit trusted roots. EdgCA does not provide automatic path building, AIA fetching, OS trust-store access, revocation (CRL/OCSP), key storage, or rotation. Certificate validation also does not prove that the presenter holds the private key — see [Verify](#verify-cloudflare-worker) below. Full list: [docs/en/NON_GOALS.md](https://github.com/noz-ele/EdgCA/blob/main/docs/en/NON_GOALS.md).
|
|
26
28
|
|
|
27
29
|
## Contents
|
|
28
30
|
|
|
29
31
|
- [CLI](#cli) — `npx edgca …` one-liners for the four most common tasks
|
|
30
32
|
- [Quick Start](#quick-start) — root → intermediate → client cert (incl. PFX bundling)
|
|
31
33
|
- [Issue a document-signing certificate](#issue-a-document-signing-certificate) — RFC 9336 `id-kp-documentSigning` leaf
|
|
32
|
-
- [Verify on Cloudflare Worker](#verify-cloudflare-worker) — confirm a cert was issued by your CA
|
|
34
|
+
- [Verify on Cloudflare Worker](#verify-cloudflare-worker) — confirm a cert was issued by your CA
|
|
35
|
+
- [Certificate chain verification](#certificate-chain-verification) — validate an explicit chain to a trusted root
|
|
33
36
|
- [Issue from a CSR](#issue-from-a-csr) — accept a caller-managed key via PKCS#10 + POP
|
|
34
37
|
- [Subject](#subject) · [Scope](#scope) · [Key Handling](#key-handling) · [Development](#development) · [API Documentation](#api-documentation)
|
|
35
38
|
|
|
36
39
|
## Status
|
|
37
40
|
|
|
38
|
-
EdgCA is in **v0.
|
|
41
|
+
EdgCA is in **v0.6.x — early stabilization**. The author is currently validating the library against real Cloudflare Workers deployments, and the API surface may still shift. To keep that validation focused, **external Issues and PRs are temporarily restricted** and will be re-opened once the API settles. Reading, cloning, forking, and `npm install` are unaffected.
|
|
39
42
|
|
|
40
43
|
## Install
|
|
41
44
|
|
|
@@ -43,7 +46,19 @@ EdgCA is in **v0.3.x — early stabilization**. The author is currently validati
|
|
|
43
46
|
npm install @noz-ele/edgca
|
|
44
47
|
```
|
|
45
48
|
|
|
46
|
-
ESM-only (`"type": "module"`). Runs on any runtime where `globalThis.crypto.subtle` is available (Cloudflare Workers, Node.js 20+, modern browsers, etc.). CommonJS `require` is not supported.
|
|
49
|
+
ESM-only (`"type": "module"`). Runs on any runtime where `globalThis.crypto.subtle` is available (Cloudflare Workers, Node.js 20+, modern browsers, etc.). CommonJS `require` is not supported.
|
|
50
|
+
|
|
51
|
+
### Package entry points
|
|
52
|
+
|
|
53
|
+
The root `@noz-ele/edgca` entry point remains an aggregate surface for compatibility. Use a purpose-specific subpath when you want an issuance-only bundle to avoid statically importing the verification implementation regardless of tree shaking:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { createRootCA, issueClientCert } from "@noz-ele/edgca/issuer";
|
|
57
|
+
import { verifyCertificateChain } from "@noz-ele/edgca/verify";
|
|
58
|
+
import { exportPkcs12 } from "@noz-ele/edgca/pkcs12";
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`./issuer` owns CA creation/import and intermediate/leaf issuance. `./verify` validates direct issuers and chains using public certificates only; it does not statically import the issuer module. The package remains `sideEffects: false`.
|
|
47
62
|
|
|
48
63
|
## CLI
|
|
49
64
|
|
|
@@ -241,7 +256,7 @@ There is no `issueDocumentSigningCertForPublicKey` (CSR variant) in v1, and EdgC
|
|
|
241
256
|
>
|
|
242
257
|
> Also out of scope (not checked by this function): `BasicConstraints CA=false`, `EKU clientAuth`, revocation, and chain walking.
|
|
243
258
|
|
|
244
|
-
This section assumes a deployment where **Cloudflare has already extracted the client certificate** and exposes it to your application via `request.cf.tlsClientAuth`. EdgCA
|
|
259
|
+
This section assumes a deployment where **Cloudflare has already extracted the client certificate** and exposes it to your application via `request.cf.tlsClientAuth`. EdgCA does not participate in the TLS handshake; it parses the certificate fields needed for the issuance check above.
|
|
245
260
|
|
|
246
261
|
### Formats Cloudflare exposes after extraction
|
|
247
262
|
|
|
@@ -323,7 +338,36 @@ export default {
|
|
|
323
338
|
|
|
324
339
|
- Omitting `validity` performs only the identity check (issuer DN + AKI/SKI + signature). If you instead inline the time check as two comparisons in the application, the result is equivalent.
|
|
325
340
|
- "Not issued by us" and "outside the validity window" return `false`; malformed PEM/DER throws. The two error categories are deliberately split.
|
|
326
|
-
- Pass the **direct issuer (one cert)** as `ca`. Verifying a leaf issued via an intermediate against the root will return `false` — chain walking is not performed.
|
|
341
|
+
- Pass the **direct issuer (one cert)** as `ca`. Verifying a leaf issued via an intermediate against the root will return `false` — chain walking is not performed.
|
|
342
|
+
|
|
343
|
+
## Certificate chain verification
|
|
344
|
+
|
|
345
|
+
The `@noz-ele/edgca/verify` surface validates a direct issuer or a bounded chain using public certificates only. No CA private key is required.
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
import { verifyCertificateChain } from "@noz-ele/edgca/verify";
|
|
349
|
+
|
|
350
|
+
const result = await verifyCertificateChain({
|
|
351
|
+
certificatePem: clientPem,
|
|
352
|
+
// Direct issuer first. EdgCA accepts zero or one intermediate.
|
|
353
|
+
intermediateCertificatesPem: [intermediatePem],
|
|
354
|
+
// No OS trust store is consulted; trust anchors are explicit.
|
|
355
|
+
trustedRootCertificatesPem: [rootPem],
|
|
356
|
+
purpose: "clientAuth"
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (!result.valid) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
`certificate chain rejected at ${result.certificateIndex}: ${result.reason}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
The verifier checks each child/issuer DN, AKI/SKI, and signature; DER validity times; issuer `BasicConstraints`, `keyCertSign`, and `pathLenConstraint`; the requested target purpose; duplicate and unsupported critical extensions; and inner/outer signature-algorithm consistency. The terminal issuer must be one of the explicitly supplied trusted-root certificates.
|
|
367
|
+
|
|
368
|
+
This is not an automatic PKI path builder. The caller supplies intermediates in order, and the supported maximum is `root → intermediate → leaf`. EdgCA does not fetch AIA URLs, consult an OS trust store, check CRL/OCSP, verify TLS proof-of-possession, or perform server hostname matching. A valid chain does not prove that the presenter holds the leaf private key.
|
|
369
|
+
|
|
370
|
+
The existing `verifyClientCertificateIssuedBy` remains unchanged for compatibility. New code should use `verifyCertificateIssuedBy` for one direct link or `verifyCertificateChain` to reach an explicit trusted root.
|
|
327
371
|
|
|
328
372
|
## Issue from a CSR
|
|
329
373
|
|
|
@@ -393,7 +437,10 @@ In scope:
|
|
|
393
437
|
- mTLS client certificate issuance (with internal key generation, or from a caller-provided public key).
|
|
394
438
|
- Document-signing certificate issuance with EKU `id-kp-documentSigning` (RFC 9336) and `keyUsage digitalSignature, contentCommitment` — internal key generation only, no SAN.
|
|
395
439
|
- CSR (PKCS#10) parsing and proof-of-possession signature verification.
|
|
396
|
-
- Identity check that a cert was issued by your own CA (`verifyClientCertificateIssuedBy`, with optional time-validity check).
|
|
440
|
+
- Identity check that a cert was issued by your own CA (`verifyClientCertificateIssuedBy`, with optional time-validity check).
|
|
441
|
+
- Direct public-certificate issuer validation (`verifyCertificateIssuedBy`).
|
|
442
|
+
- Caller-ordered chain validation up to `root → intermediate → leaf` (`verifyCertificateChain`), including DER validity, CA/Key Usage/EKU/path-length constraints, and critical-extension policy.
|
|
443
|
+
- Purpose-specific entry points (`@noz-ele/edgca/issuer` and `@noz-ele/edgca/verify`).
|
|
397
444
|
- PEM/DER helpers (certificates only — keys are exchanged as `CryptoKey`).
|
|
398
445
|
- PFX (PKCS#12) export of an issued cert + private key with PBES2 (PBKDF2-HMAC-SHA-256 + AES-256-CBC) and HMAC-SHA-256 MAC, scoped to modern consumers (Win11+, Server 2019+, macOS 15+, iOS/iPadOS 18+).
|
|
399
446
|
- Basic Constraints, Key Usage, Extended Key Usage, Subject Alternative Name, SKI, AKI.
|
|
@@ -404,9 +451,12 @@ Intentionally out of scope:
|
|
|
404
451
|
- Document-signing certificate issuance from a caller-provided public key (`issueDocumentSigningCertForPublicKey`) — not in v1.
|
|
405
452
|
- SAN (`dnsNames` / `ipAddresses` / `emailAddresses`) on document-signing leaves.
|
|
406
453
|
- CAdES / CMS / PAdES / XAdES / ASiC document signing or container building. EdgCA only issues the signing certificate; producing a signed document or container is a separate concern.
|
|
407
|
-
-
|
|
408
|
-
-
|
|
409
|
-
-
|
|
454
|
+
- Automatic PKI path building from unordered certificates, issuer discovery, or AIA fetching.
|
|
455
|
+
- OS/runtime trust-store access; trusted roots must be supplied explicitly.
|
|
456
|
+
- Chains with two or more intermediate CAs.
|
|
457
|
+
- Parsing Cloudflare-specific textual times. The verification module reads DER certificate times; the legacy API still accepts caller-supplied validity values.
|
|
458
|
+
- CRL, OCSP, revocation databases, revocation checks.
|
|
459
|
+
- TLS-handshake proof-of-possession and server hostname/SAN identity verification.
|
|
410
460
|
- Key storage, encryption-at-rest, rotation-state persistence, and integration with KV/D1/R2/Secrets.
|
|
411
461
|
- RSA, EdDSA, other elliptic curves (CSRs signed with these algorithms are rejected at parse time).
|
|
412
462
|
- Legacy PKCS#12 algorithms (3DES, RC2, SHA-1 PBE), PBMAC1, empty passwords, crlBag / secretBag / nested safeContents, and consumers older than the modern targets above are intentionally not produced or supported by `exportPkcs12`.
|
|
@@ -423,13 +473,16 @@ EdgCA only handles key generation, signing, and SPKI export of public keys. Wher
|
|
|
423
473
|
|
|
424
474
|
### Bringing your own CA key (recommended)
|
|
425
475
|
|
|
426
|
-
Root and intermediate CAs are long-lived. To keep key management on the caller's side, `createRootCA` and `issueIntermediateCA` accept an existing `keyPair: CryptoKeyPair`. This lets the caller's key-management infrastructure handle the full key lifecycle (generation, storage, rotation) consistently — including the choice of persistence format — which is the recommended path.
|
|
476
|
+
Root and intermediate CAs are long-lived. To keep key management on the caller's side, `createRootCA` and `issueIntermediateCA` accept an existing `keyPair: CryptoKeyPair`. This lets the caller's key-management infrastructure handle the full key lifecycle (generation, storage, rotation) consistently — including the choice of persistence format — which is the recommended path.
|
|
477
|
+
|
|
478
|
+
EdgCA verifies a supplied public/private pair with a sign/verify round trip before issuance. If the private key is imported with `extractable: false`, its public key cannot be reconstructed by exporting that private key; persist and import the public SPKI separately.
|
|
427
479
|
|
|
428
480
|
```ts
|
|
429
481
|
// Restore a CryptoKeyPair from whatever persistence format you use.
|
|
430
|
-
// Below is one example that
|
|
431
|
-
async function loadKeyPair(label: string): Promise<CryptoKeyPair> {
|
|
432
|
-
const pkcs8 = pemToDer(loadFromVault(`${label}-private-pem`));
|
|
482
|
+
// Below is one example that imports separately stored PKCS#8 and SPKI PEM.
|
|
483
|
+
async function loadKeyPair(label: string): Promise<CryptoKeyPair> {
|
|
484
|
+
const pkcs8 = pemToDer(loadFromVault(`${label}-private-pem`));
|
|
485
|
+
const spki = pemToDer(loadFromVault(`${label}-public-pem`));
|
|
433
486
|
const privateKey = await crypto.subtle.importKey(
|
|
434
487
|
"pkcs8",
|
|
435
488
|
pkcs8,
|
|
@@ -437,14 +490,9 @@ async function loadKeyPair(label: string): Promise<CryptoKeyPair> {
|
|
|
437
490
|
/* extractable */ false,
|
|
438
491
|
["sign"]
|
|
439
492
|
);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
delete jwk.d;
|
|
444
|
-
jwk.key_ops = ["verify"];
|
|
445
|
-
const publicKey = await crypto.subtle.importKey(
|
|
446
|
-
"jwk",
|
|
447
|
-
jwk,
|
|
493
|
+
const publicKey = await crypto.subtle.importKey(
|
|
494
|
+
"spki",
|
|
495
|
+
spki,
|
|
448
496
|
{ name: "ECDSA", namedCurve: "P-256" },
|
|
449
497
|
true,
|
|
450
498
|
["verify"]
|
package/SECURITY.md
CHANGED
|
@@ -10,16 +10,17 @@ Do not open public issues for security reports. We will respond on a best-effort
|
|
|
10
10
|
|
|
11
11
|
## Scope
|
|
12
12
|
|
|
13
|
-
EdgCA is a stateless certificate-issuance toolkit for Cloudflare Workers-compatible runtimes. The following are **in scope** for security reports:
|
|
13
|
+
EdgCA is a stateless certificate-issuance and bounded-validation toolkit for Cloudflare Workers-compatible runtimes. The following are **in scope** for security reports:
|
|
14
14
|
|
|
15
15
|
- Incorrect ASN.1 / DER encoding of issued certificates that could mislead a verifier.
|
|
16
16
|
- Cryptographic signature or KDF misuse, including incorrect use of `globalThis.crypto.subtle`.
|
|
17
17
|
- Memory-safety problems, infinite loops, or unbounded allocations triggered by malformed PEM/DER input to public functions.
|
|
18
18
|
- Public-API surface that allows a caller to produce a certificate that violates the documented invariants (e.g., `issueIntermediateCA` producing `pathLenConstraint > 0`).
|
|
19
|
+
- Incorrect acceptance or rejection by the documented direct-issuer and bounded-chain validation policies.
|
|
19
20
|
|
|
20
21
|
The following are **out of scope** (see [docs/en/NON_GOALS.md](docs/en/NON_GOALS.md) for the full list and rationale):
|
|
21
22
|
|
|
22
|
-
-
|
|
23
|
+
- Automatic PKI path building, AIA fetching, OS trust-store access, revocation (CRL/OCSP), key storage, and rotation — EdgCA does not provide them.
|
|
23
24
|
- Operational misuse of issued material (leaked private keys, logging secrets, weak storage).
|
|
24
25
|
- Caller-controlled inputs producing wrong outputs by design ("garbage in, garbage out" behavior is documented; e.g., `importCertificateAuthority` does not cryptographically validate that `issuerChainPem` actually issued `certPem`).
|
|
25
26
|
- `verifyClientCertificateIssuedBy` not authenticating the presenter — by design, this function only verifies issuance, not proof-of-possession of the private key. See the Verify section of the README for details.
|
package/dist/ca.js
CHANGED
|
@@ -185,6 +185,7 @@ function assertIssuerChainPem(chainPem) {
|
|
|
185
185
|
}
|
|
186
186
|
async function resolveKeyPair(provided) {
|
|
187
187
|
if (provided !== undefined) {
|
|
188
|
+
await assertKeyPairMatches(provided.privateKey, provided.publicKey);
|
|
188
189
|
return provided;
|
|
189
190
|
}
|
|
190
191
|
return generateKeyPair();
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export { certificateToPem, csrToPem, encodePem, pemToDer, pemToDerWithLabel, spl
|
|
|
4
4
|
export { generateKeyPair, type SupportedCurve } from "./crypto.js";
|
|
5
5
|
export { arrayBufferFromBytes, bytesEqual } from "./bytes.js";
|
|
6
6
|
export { exportPkcs12, type ExportPkcs12Input } from "./pkcs12.js";
|
|
7
|
-
export { verifyClientCertificateIssuedBy, type VerifyClientCertificateIssuedByOptions, type VerifyClientCertificateValidity } from "./verify.js";
|
|
7
|
+
export { verifyCertificateChain, verifyCertificateIssuedBy, verifyClientCertificateIssuedBy, type CertificateChainVerificationResult, type CertificateVerificationFailureReason, type CertificateVerificationPurpose, type VerifyCertificateChainOptions, type VerifyCertificateIssuedByOptions, type VerifyClientCertificateIssuedByOptions, type VerifyClientCertificateValidity } from "./verify.js";
|
|
8
8
|
export type { CertificateAuthority, CreateRootCAOptions, ImportCertificateAuthorityOptions, IssueClientCertForPublicKeyOptions, IssueClientCertOptions, IssueDocumentSigningCertOptions, IssueIntermediateCAOptions, IssuedClientCertificate, IssuedClientCertificateForPublicKey, IssuedDocumentSigningCertificate, SerialNumber, ShortSubjectAttributeType, Subject, SubjectAttribute, SubjectAttributeType } from "./types.js";
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,0BAA0B,EAC1B,eAAe,EACf,2BAA2B,EAC3B,wBAAwB,EACxB,mBAAmB,EACpB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,+BAA+B,EAC/B,8BAA8B,EAC9B,wCAAwC,EACxC,KAAK,kCAAkC,EACvC,KAAK,kCAAkC,EACvC,KAAK,oCAAoC,EACzC,KAAK,gCAAgC,EACrC,KAAK,+BAA+B,EACrC,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,iBAAiB,EACjB,cAAc,EACf,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EACL,+BAA+B,EAC/B,KAAK,sCAAsC,EAC3C,KAAK,+BAA+B,EACrC,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,iCAAiC,EACjC,kCAAkC,EAClC,sBAAsB,EACtB,+BAA+B,EAC/B,0BAA0B,EAC1B,uBAAuB,EACvB,mCAAmC,EACnC,gCAAgC,EAChC,YAAY,EACZ,yBAAyB,EACzB,OAAO,EACP,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,0BAA0B,EAC1B,eAAe,EACf,2BAA2B,EAC3B,wBAAwB,EACxB,mBAAmB,EACpB,MAAM,SAAS,CAAC;AACjB,OAAO,EACL,+BAA+B,EAC/B,8BAA8B,EAC9B,wCAAwC,EACxC,KAAK,kCAAkC,EACvC,KAAK,kCAAkC,EACvC,KAAK,oCAAoC,EACzC,KAAK,gCAAgC,EACrC,KAAK,+BAA+B,EACrC,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,iBAAiB,EACjB,cAAc,EACf,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,+BAA+B,EAC/B,KAAK,kCAAkC,EACvC,KAAK,oCAAoC,EACzC,KAAK,8BAA8B,EACnC,KAAK,6BAA6B,EAClC,KAAK,gCAAgC,EACrC,KAAK,sCAAsC,EAC3C,KAAK,+BAA+B,EACrC,MAAM,aAAa,CAAC;AACrB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,iCAAiC,EACjC,kCAAkC,EAClC,sBAAsB,EACtB,+BAA+B,EAC/B,0BAA0B,EAC1B,uBAAuB,EACvB,mCAAmC,EACnC,gCAAgC,EAChC,YAAY,EACZ,yBAAyB,EACzB,OAAO,EACP,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,4 +4,4 @@ export { certificateToPem, csrToPem, encodePem, pemToDer, pemToDerWithLabel, spl
|
|
|
4
4
|
export { generateKeyPair } from "./crypto.js";
|
|
5
5
|
export { arrayBufferFromBytes, bytesEqual } from "./bytes.js";
|
|
6
6
|
export { exportPkcs12 } from "./pkcs12.js";
|
|
7
|
-
export { verifyClientCertificateIssuedBy } from "./verify.js";
|
|
7
|
+
export { verifyCertificateChain, verifyCertificateIssuedBy, verifyClientCertificateIssuedBy } from "./verify.js";
|
package/dist/issuer.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createRootCA, importCertificateAuthority, issueClientCert, issueClientCertForPublicKey, issueDocumentSigningCert, issueIntermediateCA } from "./ca.js";
|
|
2
|
+
export type { CertificateAuthority, CreateRootCAOptions, ImportCertificateAuthorityOptions, IssueClientCertForPublicKeyOptions, IssueClientCertOptions, IssueDocumentSigningCertOptions, IssueIntermediateCAOptions, IssuedClientCertificate, IssuedClientCertificateForPublicKey, IssuedDocumentSigningCertificate, SerialNumber, ShortSubjectAttributeType, Subject, SubjectAttribute, SubjectAttributeType } from "./types.js";
|
|
3
|
+
//# sourceMappingURL=issuer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issuer.d.ts","sourceRoot":"","sources":["../src/issuer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,0BAA0B,EAC1B,eAAe,EACf,2BAA2B,EAC3B,wBAAwB,EACxB,mBAAmB,EACpB,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,oBAAoB,EACpB,mBAAmB,EACnB,iCAAiC,EACjC,kCAAkC,EAClC,sBAAsB,EACtB,+BAA+B,EAC/B,0BAA0B,EAC1B,uBAAuB,EACvB,mCAAmC,EACnC,gCAAgC,EAChC,YAAY,EACZ,yBAAyB,EACzB,OAAO,EACP,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,YAAY,CAAC"}
|
package/dist/issuer.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createRootCA, importCertificateAuthority, issueClientCert, issueClientCertForPublicKey, issueDocumentSigningCert, issueIntermediateCA } from "./ca.js";
|
package/dist/parser.d.ts
CHANGED
|
@@ -1,18 +1,41 @@
|
|
|
1
|
+
export interface ParsedKeyUsage {
|
|
2
|
+
digitalSignature: boolean;
|
|
3
|
+
contentCommitment: boolean;
|
|
4
|
+
keyCertSign: boolean;
|
|
5
|
+
cRLSign: boolean;
|
|
6
|
+
}
|
|
1
7
|
export interface ParsedCertificate {
|
|
2
8
|
der: Uint8Array;
|
|
3
9
|
tbsCertificateDer: Uint8Array;
|
|
4
10
|
signatureDer: Uint8Array;
|
|
11
|
+
signatureAlgorithmOid: string;
|
|
12
|
+
tbsSignatureAlgorithmOid: string;
|
|
13
|
+
signatureAlgorithmMatches: boolean;
|
|
5
14
|
issuerNameDer: Uint8Array;
|
|
6
15
|
subjectNameDer: Uint8Array;
|
|
7
16
|
subjectPublicKeyInfoDer: Uint8Array;
|
|
8
17
|
publicKey: CryptoKey;
|
|
18
|
+
notBeforeMs?: number;
|
|
19
|
+
notAfterMs?: number;
|
|
20
|
+
basicConstraintsPresent: boolean;
|
|
9
21
|
isCA: boolean;
|
|
10
22
|
pathLenConstraint?: number;
|
|
23
|
+
keyUsagePresent: boolean;
|
|
24
|
+
keyUsage: ParsedKeyUsage;
|
|
11
25
|
keyCertSign: boolean;
|
|
26
|
+
extendedKeyUsagePresent: boolean;
|
|
27
|
+
extendedKeyUsageOids: readonly string[];
|
|
12
28
|
subjectKeyIdentifier?: Uint8Array;
|
|
13
29
|
authorityKeyIdentifier?: Uint8Array;
|
|
30
|
+
duplicateExtensionOids: readonly string[];
|
|
31
|
+
unsupportedCriticalExtensionOids: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
export interface ParsedCertificateForVerification extends ParsedCertificate {
|
|
34
|
+
notBeforeMs: number;
|
|
35
|
+
notAfterMs: number;
|
|
14
36
|
}
|
|
15
37
|
export declare function extractCertificateSpkiDer(der: Uint8Array): Uint8Array;
|
|
16
38
|
export declare function parseCertificateDer(der: Uint8Array): Promise<ParsedCertificate>;
|
|
39
|
+
export declare function parseCertificateDerForVerification(der: Uint8Array): Promise<ParsedCertificateForVerification>;
|
|
17
40
|
export declare function assertIssuerSubjectMatches(issuer: ParsedCertificate, issued: ParsedCertificate): void;
|
|
18
41
|
//# sourceMappingURL=parser.d.ts.map
|
package/dist/parser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,UAAU,CAAC;IAChB,iBAAiB,EAAE,UAAU,CAAC;IAC9B,YAAY,EAAE,UAAU,CAAC;IACzB,aAAa,EAAE,UAAU,CAAC;IAC1B,cAAc,EAAE,UAAU,CAAC;IAC3B,uBAAuB,EAAE,UAAU,CAAC;IACpC,SAAS,EAAE,SAAS,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,oBAAoB,CAAC,EAAE,UAAU,CAAC;IAClC,sBAAsB,CAAC,EAAE,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,cAAc;IAC7B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,UAAU,CAAC;IAChB,iBAAiB,EAAE,UAAU,CAAC;IAC9B,YAAY,EAAE,UAAU,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,wBAAwB,EAAE,MAAM,CAAC;IACjC,yBAAyB,EAAE,OAAO,CAAC;IACnC,aAAa,EAAE,UAAU,CAAC;IAC1B,cAAc,EAAE,UAAU,CAAC;IAC3B,uBAAuB,EAAE,UAAU,CAAC;IACpC,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uBAAuB,EAAE,OAAO,CAAC;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAE,cAAc,CAAC;IACzB,WAAW,EAAE,OAAO,CAAC;IACrB,uBAAuB,EAAE,OAAO,CAAC;IACjC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,oBAAoB,CAAC,EAAE,UAAU,CAAC;IAClC,sBAAsB,CAAC,EAAE,UAAU,CAAC;IACpC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,gCAAgC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrD;AAED,MAAM,WAAW,gCAAiC,SAAQ,iBAAiB;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAoBD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAyBrE;AAID,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAKrF;AAKD,wBAAsB,kCAAkC,CACtD,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,gCAAgC,CAAC,CAM3C;AAkGD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAIrG"}
|
package/dist/parser.js
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { bytesEqual } from "./bytes.js";
|
|
2
|
-
import { decodeInteger, decodeOid,
|
|
2
|
+
import { decodeInteger, decodeOid, readElement, readSequenceChildren, TAG } from "./der.js";
|
|
3
3
|
import { importPublicKeySpki } from "./crypto.js";
|
|
4
4
|
import { OID } from "./oids.js";
|
|
5
|
+
const EMPTY_KEY_USAGE = {
|
|
6
|
+
digitalSignature: false,
|
|
7
|
+
contentCommitment: false,
|
|
8
|
+
keyCertSign: false,
|
|
9
|
+
cRLSign: false
|
|
10
|
+
};
|
|
11
|
+
const KNOWN_EXTENSION_OIDS = new Set([
|
|
12
|
+
OID.basicConstraints,
|
|
13
|
+
OID.keyUsage,
|
|
14
|
+
OID.extendedKeyUsage,
|
|
15
|
+
OID.subjectKeyIdentifier,
|
|
16
|
+
OID.authorityKeyIdentifier
|
|
17
|
+
]);
|
|
5
18
|
// Read just the SubjectPublicKeyInfo DER from a v3 X.509 certificate without
|
|
6
19
|
// importing the public key. Used by exportPkcs12, which needs the SPKI for
|
|
7
|
-
// localKeyId computation
|
|
8
|
-
// inner key algorithm. parseCertificateDer below additionally imports the
|
|
9
|
-
// SPKI as an ECDSA CryptoKey for the issuance/verify paths; that import is
|
|
10
|
-
// EC-only and must not be on the pkcs12 path.
|
|
20
|
+
// localKeyId computation regardless of the inner key algorithm.
|
|
11
21
|
export function extractCertificateSpkiDer(der) {
|
|
12
22
|
const certificate = readElement(der);
|
|
13
23
|
if (certificate.tag !== TAG.SEQUENCE || certificate.end !== der.length) {
|
|
@@ -18,14 +28,7 @@ export function extractCertificateSpkiDer(der) {
|
|
|
18
28
|
throw new Error("Invalid certificate structure");
|
|
19
29
|
}
|
|
20
30
|
const tbsChildren = readSequenceChildren(tbsCertificate);
|
|
21
|
-
|
|
22
|
-
if (!versionTag || versionTag.tag !== 0xa0) {
|
|
23
|
-
throw new Error("Unsupported X.509 version (only v3 is supported)");
|
|
24
|
-
}
|
|
25
|
-
const versionInner = readElement(versionTag.value);
|
|
26
|
-
if (versionInner.tag !== TAG.INTEGER || decodeInteger(versionInner.value) !== 2n) {
|
|
27
|
-
throw new Error("Unsupported X.509 version (only v3 is supported)");
|
|
28
|
-
}
|
|
31
|
+
assertV3(tbsChildren[0]);
|
|
29
32
|
// tbsCertificate (v3) layout:
|
|
30
33
|
// [0] version [0] EXPLICIT INTEGER 2
|
|
31
34
|
// [1] serialNumber
|
|
@@ -40,54 +43,96 @@ export function extractCertificateSpkiDer(der) {
|
|
|
40
43
|
}
|
|
41
44
|
return subjectPublicKeyInfo.raw;
|
|
42
45
|
}
|
|
46
|
+
// Compatibility parser used by issuance/import paths. It preserves the
|
|
47
|
+
// historical behavior for unusual-but-parseable imported certificates.
|
|
43
48
|
export async function parseCertificateDer(der) {
|
|
49
|
+
// Await here (rather than returning the inner promise directly) so runtimes
|
|
50
|
+
// with eager unhandled-rejection tracking observe this wrapper as the
|
|
51
|
+
// rejection owner for synchronous parse failures.
|
|
52
|
+
return await parseCertificateDerInternal(der, false);
|
|
53
|
+
}
|
|
54
|
+
// Strict parser used only by the public verification surface. It reads the
|
|
55
|
+
// certificate's own validity and rejects malformed X.509 structures while
|
|
56
|
+
// reporting policy failures (duplicates/unknown critical extensions) as data.
|
|
57
|
+
export async function parseCertificateDerForVerification(der) {
|
|
58
|
+
const parsed = await parseCertificateDerInternal(der, true);
|
|
59
|
+
if (parsed.notBeforeMs === undefined || parsed.notAfterMs === undefined) {
|
|
60
|
+
throw new Error("Invalid certificate validity");
|
|
61
|
+
}
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
async function parseCertificateDerInternal(der, strict) {
|
|
44
65
|
const certificate = readElement(der);
|
|
45
66
|
if (certificate.tag !== TAG.SEQUENCE || certificate.end !== der.length) {
|
|
46
67
|
throw new Error("Invalid certificate DER");
|
|
47
68
|
}
|
|
48
|
-
const
|
|
69
|
+
const certificateChildren = readSequenceChildren(certificate);
|
|
70
|
+
if (strict && certificateChildren.length !== 3) {
|
|
71
|
+
throw new Error("Invalid certificate structure");
|
|
72
|
+
}
|
|
73
|
+
const [tbsCertificate, signatureAlgorithm, signatureValue] = certificateChildren;
|
|
49
74
|
if (!tbsCertificate || !signatureAlgorithm || !signatureValue) {
|
|
50
75
|
throw new Error("Invalid certificate structure");
|
|
51
76
|
}
|
|
52
|
-
if (
|
|
77
|
+
if (tbsCertificate.tag !== TAG.SEQUENCE || signatureAlgorithm.tag !== TAG.SEQUENCE) {
|
|
78
|
+
throw new Error("Invalid certificate structure");
|
|
79
|
+
}
|
|
80
|
+
if (signatureValue.tag !== TAG.BIT_STRING || signatureValue.value.length < 1 || signatureValue.value[0] !== 0) {
|
|
53
81
|
throw new Error("Invalid certificate signature value");
|
|
54
82
|
}
|
|
55
83
|
const tbsChildren = readSequenceChildren(tbsCertificate);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const versionTag = tbsChildren[0];
|
|
60
|
-
if (!versionTag || versionTag.tag !== 0xa0) {
|
|
61
|
-
throw new Error("Unsupported X.509 version (only v3 is supported)");
|
|
62
|
-
}
|
|
63
|
-
const versionInner = readElement(versionTag.value);
|
|
64
|
-
if (versionInner.tag !== TAG.INTEGER || decodeInteger(versionInner.value) !== 2n) {
|
|
65
|
-
throw new Error("Unsupported X.509 version (only v3 is supported)");
|
|
84
|
+
assertV3(tbsChildren[0]);
|
|
85
|
+
if (strict && tbsChildren.length < 7) {
|
|
86
|
+
throw new Error("Invalid certificate TBSCertificate structure");
|
|
66
87
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
88
|
+
const tbsSignatureAlgorithm = tbsChildren[2];
|
|
89
|
+
const issuer = tbsChildren[3];
|
|
90
|
+
const validity = tbsChildren[4];
|
|
91
|
+
const subject = tbsChildren[5];
|
|
92
|
+
const subjectPublicKeyInfo = tbsChildren[6];
|
|
93
|
+
if (!tbsSignatureAlgorithm || tbsSignatureAlgorithm.tag !== TAG.SEQUENCE ||
|
|
94
|
+
!issuer || issuer.tag !== TAG.SEQUENCE ||
|
|
95
|
+
!validity || validity.tag !== TAG.SEQUENCE ||
|
|
96
|
+
!subject || subject.tag !== TAG.SEQUENCE ||
|
|
97
|
+
!subjectPublicKeyInfo || subjectPublicKeyInfo.tag !== TAG.SEQUENCE) {
|
|
75
98
|
throw new Error("Invalid certificate TBSCertificate structure");
|
|
76
99
|
}
|
|
77
|
-
const
|
|
78
|
-
const
|
|
100
|
+
const signatureAlgorithmOid = parseAlgorithmIdentifier(signatureAlgorithm, strict);
|
|
101
|
+
const tbsSignatureAlgorithmOid = parseAlgorithmIdentifier(tbsSignatureAlgorithm, strict);
|
|
102
|
+
const extensionFields = tbsChildren.filter((element) => element.tag === 0xa3);
|
|
103
|
+
if (strict && extensionFields.length > 1) {
|
|
104
|
+
throw new Error("Invalid certificate: duplicate extensions field");
|
|
105
|
+
}
|
|
106
|
+
const parsedExtensions = extensionFields[0]
|
|
107
|
+
? parseExtensions(extensionFields[0].value, strict)
|
|
108
|
+
: emptyParsedExtensions();
|
|
79
109
|
const publicKey = await importPublicKeySpki(subjectPublicKeyInfo.raw);
|
|
110
|
+
const parsedValidity = strict ? parseValidity(validity) : undefined;
|
|
80
111
|
const parsed = {
|
|
81
112
|
der,
|
|
82
113
|
tbsCertificateDer: tbsCertificate.raw,
|
|
83
114
|
signatureDer: signatureValue.value.subarray(1),
|
|
115
|
+
signatureAlgorithmOid,
|
|
116
|
+
tbsSignatureAlgorithmOid,
|
|
117
|
+
signatureAlgorithmMatches: bytesEqual(signatureAlgorithm.raw, tbsSignatureAlgorithm.raw),
|
|
84
118
|
issuerNameDer: issuer.raw,
|
|
85
119
|
subjectNameDer: subject.raw,
|
|
86
120
|
subjectPublicKeyInfoDer: subjectPublicKeyInfo.raw,
|
|
87
121
|
publicKey,
|
|
122
|
+
basicConstraintsPresent: parsedExtensions.basicConstraintsPresent,
|
|
88
123
|
isCA: parsedExtensions.isCA ?? false,
|
|
89
|
-
|
|
124
|
+
keyUsagePresent: parsedExtensions.keyUsagePresent,
|
|
125
|
+
keyUsage: parsedExtensions.keyUsage ?? { ...EMPTY_KEY_USAGE },
|
|
126
|
+
keyCertSign: parsedExtensions.keyUsage?.keyCertSign ?? false,
|
|
127
|
+
extendedKeyUsagePresent: parsedExtensions.extendedKeyUsagePresent,
|
|
128
|
+
extendedKeyUsageOids: parsedExtensions.extendedKeyUsageOids ?? [],
|
|
129
|
+
duplicateExtensionOids: parsedExtensions.duplicateExtensionOids,
|
|
130
|
+
unsupportedCriticalExtensionOids: parsedExtensions.unsupportedCriticalExtensionOids
|
|
90
131
|
};
|
|
132
|
+
if (parsedValidity !== undefined) {
|
|
133
|
+
parsed.notBeforeMs = parsedValidity.notBeforeMs;
|
|
134
|
+
parsed.notAfterMs = parsedValidity.notAfterMs;
|
|
135
|
+
}
|
|
91
136
|
if (parsedExtensions.pathLenConstraint !== undefined) {
|
|
92
137
|
parsed.pathLenConstraint = parsedExtensions.pathLenConstraint;
|
|
93
138
|
}
|
|
@@ -104,12 +149,22 @@ export function assertIssuerSubjectMatches(issuer, issued) {
|
|
|
104
149
|
throw new Error("Issued certificate issuer does not match CA subject");
|
|
105
150
|
}
|
|
106
151
|
}
|
|
107
|
-
function
|
|
152
|
+
function emptyParsedExtensions() {
|
|
153
|
+
return {
|
|
154
|
+
basicConstraintsPresent: false,
|
|
155
|
+
keyUsagePresent: false,
|
|
156
|
+
extendedKeyUsagePresent: false,
|
|
157
|
+
duplicateExtensionOids: [],
|
|
158
|
+
unsupportedCriticalExtensionOids: []
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function parseExtensions(explicitValue, strict) {
|
|
108
162
|
const outer = readElement(explicitValue);
|
|
109
|
-
if (outer.tag !== TAG.SEQUENCE) {
|
|
163
|
+
if (outer.tag !== TAG.SEQUENCE || (strict && outer.end !== explicitValue.length)) {
|
|
110
164
|
throw new Error("Invalid extensions structure");
|
|
111
165
|
}
|
|
112
|
-
const parsed =
|
|
166
|
+
const parsed = emptyParsedExtensions();
|
|
167
|
+
const seen = new Set();
|
|
113
168
|
for (const extension of readSequenceChildren(outer)) {
|
|
114
169
|
const children = readSequenceChildren(extension);
|
|
115
170
|
const oidElement = children[0];
|
|
@@ -117,25 +172,40 @@ function parseExtensions(explicitValue) {
|
|
|
117
172
|
throw new Error("Invalid extension OID");
|
|
118
173
|
}
|
|
119
174
|
let valueIndex = 1;
|
|
175
|
+
let critical = false;
|
|
120
176
|
if (children[valueIndex]?.tag === TAG.BOOLEAN) {
|
|
177
|
+
critical = parseBoolean(children[valueIndex], strict);
|
|
121
178
|
valueIndex += 1;
|
|
122
179
|
}
|
|
123
180
|
const value = children[valueIndex];
|
|
124
|
-
if (!value || value.tag !== TAG.OCTET_STRING) {
|
|
181
|
+
if (!value || value.tag !== TAG.OCTET_STRING || (strict && children.length !== valueIndex + 1)) {
|
|
125
182
|
throw new Error("Invalid extension value");
|
|
126
183
|
}
|
|
127
184
|
const extensionOid = decodeOid(oidElement.value);
|
|
185
|
+
if (seen.has(extensionOid) && !parsed.duplicateExtensionOids.includes(extensionOid)) {
|
|
186
|
+
parsed.duplicateExtensionOids.push(extensionOid);
|
|
187
|
+
}
|
|
188
|
+
seen.add(extensionOid);
|
|
189
|
+
if (critical && !KNOWN_EXTENSION_OIDS.has(extensionOid)) {
|
|
190
|
+
parsed.unsupportedCriticalExtensionOids.push(extensionOid);
|
|
191
|
+
}
|
|
128
192
|
if (extensionOid === OID.basicConstraints) {
|
|
129
|
-
|
|
193
|
+
parsed.basicConstraintsPresent = true;
|
|
194
|
+
Object.assign(parsed, parseBasicConstraints(value.value, strict));
|
|
130
195
|
}
|
|
131
196
|
else if (extensionOid === OID.keyUsage) {
|
|
132
|
-
parsed.
|
|
197
|
+
parsed.keyUsagePresent = true;
|
|
198
|
+
parsed.keyUsage = parseKeyUsage(value.value, strict);
|
|
199
|
+
}
|
|
200
|
+
else if (extensionOid === OID.extendedKeyUsage) {
|
|
201
|
+
parsed.extendedKeyUsagePresent = true;
|
|
202
|
+
parsed.extendedKeyUsageOids = parseExtendedKeyUsage(value.value, strict);
|
|
133
203
|
}
|
|
134
204
|
else if (extensionOid === OID.subjectKeyIdentifier) {
|
|
135
|
-
parsed.subjectKeyIdentifier = parseOctetString(value.value);
|
|
205
|
+
parsed.subjectKeyIdentifier = parseOctetString(value.value, strict);
|
|
136
206
|
}
|
|
137
207
|
else if (extensionOid === OID.authorityKeyIdentifier) {
|
|
138
|
-
const keyIdentifier = parseAuthorityKeyIdentifier(value.value);
|
|
208
|
+
const keyIdentifier = parseAuthorityKeyIdentifier(value.value, strict);
|
|
139
209
|
if (keyIdentifier !== undefined) {
|
|
140
210
|
parsed.authorityKeyIdentifier = keyIdentifier;
|
|
141
211
|
}
|
|
@@ -143,42 +213,187 @@ function parseExtensions(explicitValue) {
|
|
|
143
213
|
}
|
|
144
214
|
return parsed;
|
|
145
215
|
}
|
|
146
|
-
function
|
|
216
|
+
function parseAlgorithmIdentifier(element, strict) {
|
|
217
|
+
const children = readSequenceChildren(element);
|
|
218
|
+
const oidElement = children[0];
|
|
219
|
+
if (!oidElement || oidElement.tag !== TAG.OBJECT_IDENTIFIER) {
|
|
220
|
+
throw new Error("Invalid signature AlgorithmIdentifier");
|
|
221
|
+
}
|
|
222
|
+
if (strict && children.length !== 1) {
|
|
223
|
+
throw new Error("ECDSA signature AlgorithmIdentifier must not have parameters");
|
|
224
|
+
}
|
|
225
|
+
return decodeOid(oidElement.value);
|
|
226
|
+
}
|
|
227
|
+
function parseValidity(validity) {
|
|
228
|
+
const children = readSequenceChildren(validity);
|
|
229
|
+
if (children.length !== 2 || !children[0] || !children[1]) {
|
|
230
|
+
throw new Error("Invalid certificate validity");
|
|
231
|
+
}
|
|
232
|
+
const notBeforeMs = parseDerTime(children[0]);
|
|
233
|
+
const notAfterMs = parseDerTime(children[1]);
|
|
234
|
+
if (notBeforeMs > notAfterMs) {
|
|
235
|
+
throw new Error("Invalid certificate validity range");
|
|
236
|
+
}
|
|
237
|
+
return { notBeforeMs, notAfterMs };
|
|
238
|
+
}
|
|
239
|
+
function parseDerTime(element) {
|
|
240
|
+
const text = asciiString(element.value, "certificate time");
|
|
241
|
+
let year;
|
|
242
|
+
let offset;
|
|
243
|
+
if (element.tag === TAG.UTC_TIME) {
|
|
244
|
+
if (!/^\d{12}Z$/.test(text)) {
|
|
245
|
+
throw new Error("Invalid UTCTime");
|
|
246
|
+
}
|
|
247
|
+
const shortYear = Number(text.slice(0, 2));
|
|
248
|
+
year = shortYear >= 50 ? 1900 + shortYear : 2000 + shortYear;
|
|
249
|
+
offset = 2;
|
|
250
|
+
}
|
|
251
|
+
else if (element.tag === TAG.GENERALIZED_TIME) {
|
|
252
|
+
if (!/^\d{14}Z$/.test(text)) {
|
|
253
|
+
throw new Error("Invalid GeneralizedTime");
|
|
254
|
+
}
|
|
255
|
+
year = Number(text.slice(0, 4));
|
|
256
|
+
if (year < 1 || year > 9999) {
|
|
257
|
+
throw new Error("Invalid GeneralizedTime year");
|
|
258
|
+
}
|
|
259
|
+
offset = 4;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
throw new Error("Unsupported certificate time type");
|
|
263
|
+
}
|
|
264
|
+
const month = Number(text.slice(offset, offset + 2));
|
|
265
|
+
const day = Number(text.slice(offset + 2, offset + 4));
|
|
266
|
+
const hour = Number(text.slice(offset + 4, offset + 6));
|
|
267
|
+
const minute = Number(text.slice(offset + 6, offset + 8));
|
|
268
|
+
const second = Number(text.slice(offset + 8, offset + 10));
|
|
269
|
+
const date = new Date(0);
|
|
270
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
271
|
+
date.setUTCHours(hour, minute, second, 0);
|
|
272
|
+
if (month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59 || second > 59 ||
|
|
273
|
+
date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 ||
|
|
274
|
+
date.getUTCDate() !== day || date.getUTCHours() !== hour ||
|
|
275
|
+
date.getUTCMinutes() !== minute || date.getUTCSeconds() !== second) {
|
|
276
|
+
throw new Error("Invalid certificate time value");
|
|
277
|
+
}
|
|
278
|
+
return date.getTime();
|
|
279
|
+
}
|
|
280
|
+
function parseBasicConstraints(value, strict) {
|
|
147
281
|
const root = readElement(value);
|
|
282
|
+
if (root.tag !== TAG.SEQUENCE || (strict && root.end !== value.length)) {
|
|
283
|
+
throw new Error("Invalid basicConstraints extension");
|
|
284
|
+
}
|
|
148
285
|
const children = readSequenceChildren(root);
|
|
149
286
|
const result = { isCA: false };
|
|
150
|
-
|
|
151
|
-
|
|
287
|
+
let index = 0;
|
|
288
|
+
if (children[index]?.tag === TAG.BOOLEAN) {
|
|
289
|
+
result.isCA = parseBoolean(children[index], strict);
|
|
290
|
+
index += 1;
|
|
291
|
+
}
|
|
292
|
+
if (children[index]?.tag === TAG.INTEGER) {
|
|
293
|
+
const decoded = decodeInteger(children[index].value);
|
|
294
|
+
if (decoded > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
295
|
+
throw new Error("basicConstraints pathLenConstraint is too large");
|
|
296
|
+
}
|
|
297
|
+
result.pathLenConstraint = Number(decoded);
|
|
298
|
+
index += 1;
|
|
299
|
+
}
|
|
300
|
+
if (strict && index !== children.length) {
|
|
301
|
+
throw new Error("Invalid basicConstraints extension");
|
|
152
302
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
result.pathLenConstraint = Number(decodeInteger(pathLen.value));
|
|
303
|
+
if (strict && result.pathLenConstraint !== undefined && result.isCA !== true) {
|
|
304
|
+
throw new Error("basicConstraints pathLenConstraint requires CA=true");
|
|
156
305
|
}
|
|
157
306
|
return result;
|
|
158
307
|
}
|
|
159
|
-
function parseKeyUsage(value) {
|
|
308
|
+
function parseKeyUsage(value, strict) {
|
|
160
309
|
const root = readElement(value);
|
|
161
|
-
if (root.tag !== TAG.BIT_STRING || root.value.length < 2) {
|
|
310
|
+
if (root.tag !== TAG.BIT_STRING || root.value.length < 2 || (strict && root.end !== value.length)) {
|
|
162
311
|
throw new Error("Invalid keyUsage extension");
|
|
163
312
|
}
|
|
313
|
+
const unusedBits = root.value[0];
|
|
164
314
|
const bytes = root.value.subarray(1);
|
|
315
|
+
if (unusedBits > 7) {
|
|
316
|
+
throw new Error("Invalid keyUsage unused bits");
|
|
317
|
+
}
|
|
318
|
+
if (strict && unusedBits > 0 && (bytes[bytes.length - 1] & ((1 << unusedBits) - 1)) !== 0) {
|
|
319
|
+
throw new Error("Invalid keyUsage padding bits");
|
|
320
|
+
}
|
|
165
321
|
return {
|
|
166
|
-
|
|
322
|
+
digitalSignature: keyUsageBit(bytes, 0),
|
|
323
|
+
contentCommitment: keyUsageBit(bytes, 1),
|
|
324
|
+
keyCertSign: keyUsageBit(bytes, 5),
|
|
325
|
+
cRLSign: keyUsageBit(bytes, 6)
|
|
167
326
|
};
|
|
168
327
|
}
|
|
169
|
-
function
|
|
328
|
+
function keyUsageBit(bytes, bit) {
|
|
329
|
+
const byte = bytes[Math.floor(bit / 8)];
|
|
330
|
+
return byte !== undefined && (byte & (0x80 >> (bit % 8))) !== 0;
|
|
331
|
+
}
|
|
332
|
+
function parseExtendedKeyUsage(value, strict) {
|
|
170
333
|
const root = readElement(value);
|
|
171
|
-
if (root.tag !== TAG.
|
|
334
|
+
if (root.tag !== TAG.SEQUENCE || (strict && root.end !== value.length)) {
|
|
335
|
+
throw new Error("Invalid extendedKeyUsage extension");
|
|
336
|
+
}
|
|
337
|
+
const children = readSequenceChildren(root);
|
|
338
|
+
if (strict && children.length === 0) {
|
|
339
|
+
throw new Error("extendedKeyUsage must contain at least one purpose");
|
|
340
|
+
}
|
|
341
|
+
return children.map((child) => {
|
|
342
|
+
if (child.tag !== TAG.OBJECT_IDENTIFIER) {
|
|
343
|
+
throw new Error("Invalid extendedKeyUsage purpose");
|
|
344
|
+
}
|
|
345
|
+
return decodeOid(child.value);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
function parseOctetString(value, strict) {
|
|
349
|
+
const root = readElement(value);
|
|
350
|
+
if (root.tag !== TAG.OCTET_STRING || (strict && root.end !== value.length)) {
|
|
172
351
|
throw new Error("Invalid OCTET STRING extension payload");
|
|
173
352
|
}
|
|
174
353
|
return root.value;
|
|
175
354
|
}
|
|
176
|
-
function parseAuthorityKeyIdentifier(value) {
|
|
355
|
+
function parseAuthorityKeyIdentifier(value, strict) {
|
|
177
356
|
const root = readElement(value);
|
|
357
|
+
if (root.tag !== TAG.SEQUENCE || (strict && root.end !== value.length)) {
|
|
358
|
+
throw new Error("Invalid authorityKeyIdentifier extension");
|
|
359
|
+
}
|
|
360
|
+
let keyIdentifier;
|
|
178
361
|
for (const child of readSequenceChildren(root)) {
|
|
179
362
|
if (child.tag === 0x80) {
|
|
180
|
-
|
|
363
|
+
if (strict && keyIdentifier !== undefined) {
|
|
364
|
+
throw new Error("Duplicate authorityKeyIdentifier keyIdentifier");
|
|
365
|
+
}
|
|
366
|
+
keyIdentifier = child.value;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return keyIdentifier;
|
|
370
|
+
}
|
|
371
|
+
function parseBoolean(element, strict) {
|
|
372
|
+
if (element.value.length !== 1) {
|
|
373
|
+
throw new Error("Invalid BOOLEAN");
|
|
374
|
+
}
|
|
375
|
+
if (strict && element.value[0] !== 0x00 && element.value[0] !== 0xff) {
|
|
376
|
+
throw new Error("Invalid DER BOOLEAN value");
|
|
377
|
+
}
|
|
378
|
+
return element.value[0] !== 0;
|
|
379
|
+
}
|
|
380
|
+
function assertV3(versionTag) {
|
|
381
|
+
if (!versionTag || versionTag.tag !== 0xa0) {
|
|
382
|
+
throw new Error("Unsupported X.509 version (only v3 is supported)");
|
|
383
|
+
}
|
|
384
|
+
const versionInner = readElement(versionTag.value);
|
|
385
|
+
if (versionInner.tag !== TAG.INTEGER || versionInner.end !== versionTag.value.length ||
|
|
386
|
+
decodeInteger(versionInner.value) !== 2n) {
|
|
387
|
+
throw new Error("Unsupported X.509 version (only v3 is supported)");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function asciiString(bytes, name) {
|
|
391
|
+
let out = "";
|
|
392
|
+
for (const byte of bytes) {
|
|
393
|
+
if (byte > 0x7f) {
|
|
394
|
+
throw new Error(`Invalid non-ASCII ${name}`);
|
|
181
395
|
}
|
|
396
|
+
out += String.fromCharCode(byte);
|
|
182
397
|
}
|
|
183
|
-
return
|
|
398
|
+
return out;
|
|
184
399
|
}
|
package/dist/verify.d.ts
CHANGED
|
@@ -9,38 +9,43 @@ export interface VerifyClientCertificateIssuedByOptions {
|
|
|
9
9
|
certPem: string;
|
|
10
10
|
validity?: VerifyClientCertificateValidity;
|
|
11
11
|
}
|
|
12
|
+
export interface VerifyCertificateIssuedByOptions {
|
|
13
|
+
certificatePem: string;
|
|
14
|
+
issuerCertificatePem: string;
|
|
15
|
+
at?: Date | number;
|
|
16
|
+
}
|
|
17
|
+
export type CertificateVerificationPurpose = "ca" | "clientAuth" | "documentSigning";
|
|
18
|
+
export interface VerifyCertificateChainOptions {
|
|
19
|
+
certificatePem: string;
|
|
20
|
+
intermediateCertificatesPem?: readonly string[];
|
|
21
|
+
trustedRootCertificatesPem: readonly string[];
|
|
22
|
+
at?: Date | number;
|
|
23
|
+
purpose?: CertificateVerificationPurpose;
|
|
24
|
+
}
|
|
25
|
+
export type CertificateVerificationFailureReason = "not-yet-valid" | "expired" | "issuer-name-mismatch" | "key-identifier-mismatch" | "invalid-signature" | "issuer-not-ca" | "issuer-key-usage-invalid" | "path-length-exceeded" | "target-profile-invalid" | "invalid-chain-order" | "duplicate-extension" | "signature-algorithm-mismatch" | "unsupported-critical-extension" | "untrusted-root";
|
|
26
|
+
export type CertificateChainVerificationResult = {
|
|
27
|
+
valid: true;
|
|
28
|
+
trustedRootIndex: number;
|
|
29
|
+
} | {
|
|
30
|
+
valid: false;
|
|
31
|
+
reason: CertificateVerificationFailureReason;
|
|
32
|
+
certificateIndex: number;
|
|
33
|
+
};
|
|
12
34
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* Proving legitimate ownership additionally requires verifying possession
|
|
26
|
-
* of the corresponding private key (a signature made by it, verified
|
|
27
|
-
* against the certificate's public key). The TLS handshake's
|
|
28
|
-
* `CertificateVerify` message normally provides this, but the Cloudflare
|
|
29
|
-
* Workers runtime does not expose that signature to the application. On
|
|
30
|
-
* non-Enterprise plans, Cloudflare's TLS layer also does not know about
|
|
31
|
-
* your self-managed CA, so `request.cf.tlsClientAuth.certVerified` will
|
|
32
|
-
* not be `"SUCCESS"` for certificates EdgCA issued. Application code on
|
|
33
|
-
* Workers (Enterprise excluded) has no way to verify proof-of-possession.
|
|
34
|
-
*
|
|
35
|
-
* Implication: anyone who has obtained a copy of a valid certificate
|
|
36
|
-
* (logs, leaked storage, network capture, etc.) can present it and pass
|
|
37
|
-
* this check. Use this as a minimum identity-check layer, not as
|
|
38
|
-
* authentication. For real authentication, use Cloudflare Enterprise mTLS
|
|
39
|
-
* at the TLS layer, or add an application-layer challenge-response that
|
|
40
|
-
* has the client sign a server-issued nonce with its private key.
|
|
35
|
+
* Verifies one certificate/issuer link using public certificate material only.
|
|
36
|
+
* This is not proof that a presenter possesses the certificate's private key.
|
|
37
|
+
*/
|
|
38
|
+
export declare function verifyCertificateIssuedBy(options: VerifyCertificateIssuedByOptions): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Verifies a caller-ordered chain ending at one explicitly supplied trust
|
|
41
|
+
* anchor. It intentionally does not perform PKI path building or revocation.
|
|
42
|
+
*/
|
|
43
|
+
export declare function verifyCertificateChain(options: VerifyCertificateChainOptions): Promise<CertificateChainVerificationResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Legacy compatibility API. Its external validity option and direct-issuer
|
|
46
|
+
* semantics intentionally remain unchanged.
|
|
41
47
|
*
|
|
42
|
-
*
|
|
43
|
-
* `EKU clientAuth`, revocation, and chain walking.
|
|
48
|
+
* This is not mTLS verification and does not authenticate the presenter.
|
|
44
49
|
*/
|
|
45
50
|
export declare function verifyClientCertificateIssuedBy(options: VerifyClientCertificateIssuedByOptions): Promise<boolean>;
|
|
46
51
|
//# sourceMappingURL=verify.d.ts.map
|
package/dist/verify.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;IACzB,QAAQ,EAAE,IAAI,GAAG,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,sCAAsC;IACrD,EAAE,EAAE,oBAAoB,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,+BAA+B,CAAC;CAC5C;AAED,MAAM,WAAW,gCAAgC;IAC/C,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,EAAE,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,8BAA8B,GAAG,IAAI,GAAG,YAAY,GAAG,iBAAiB,CAAC;AAErF,MAAM,WAAW,6BAA6B;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,2BAA2B,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD,0BAA0B,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,EAAE,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,8BAA8B,CAAC;CAC1C;AAED,MAAM,MAAM,oCAAoC,GAC5C,eAAe,GACf,SAAS,GACT,sBAAsB,GACtB,yBAAyB,GACzB,mBAAmB,GACnB,eAAe,GACf,0BAA0B,GAC1B,sBAAsB,GACtB,wBAAwB,GACxB,qBAAqB,GACrB,qBAAqB,GACrB,8BAA8B,GAC9B,gCAAgC,GAChC,gBAAgB,CAAC;AAErB,MAAM,MAAM,kCAAkC,GAC1C;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,GACzC;IACE,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,oCAAoC,CAAC;IAC7C,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAIN;;;GAGG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,kCAAkC,CAAC,CA2F7C;AAED;;;;;GAKG;AACH,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,sCAAsC,GAC9C,OAAO,CAAC,OAAO,CAAC,CAwBlB"}
|
package/dist/verify.js
CHANGED
|
@@ -1,39 +1,113 @@
|
|
|
1
1
|
import { bytesEqual } from "./bytes.js";
|
|
2
|
-
import { keyIdentifierFromSpki, verifyDer } from "./crypto.js";
|
|
2
|
+
import { curveOf, keyIdentifierFromSpki, signatureAlgorithmOidForCurve, verifyDer } from "./crypto.js";
|
|
3
|
+
import { OID } from "./oids.js";
|
|
3
4
|
import { pemToDerWithLabel } from "./pem.js";
|
|
4
|
-
import { parseCertificateDer } from "./parser.js";
|
|
5
|
+
import { parseCertificateDer, parseCertificateDerForVerification } from "./parser.js";
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
7
|
+
* Verifies one certificate/issuer link using public certificate material only.
|
|
8
|
+
* This is not proof that a presenter possesses the certificate's private key.
|
|
9
|
+
*/
|
|
10
|
+
export async function verifyCertificateIssuedBy(options) {
|
|
11
|
+
assertObject(options, "options");
|
|
12
|
+
const at = toEpochMs(options.at ?? Date.now(), "at");
|
|
13
|
+
const certificate = await parseVerificationCertificate(options.certificatePem, "certificatePem");
|
|
14
|
+
const issuer = await parseVerificationCertificate(options.issuerCertificatePem, "issuerCertificatePem");
|
|
15
|
+
if (certificatePolicyFailure(certificate, at, 0))
|
|
16
|
+
return false;
|
|
17
|
+
if (certificatePolicyFailure(issuer, at, 1))
|
|
18
|
+
return false;
|
|
19
|
+
if (issuerConstraintFailure(issuer, 1))
|
|
20
|
+
return false;
|
|
21
|
+
return (await linkFailure(certificate, issuer, 0)) === undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Verifies a caller-ordered chain ending at one explicitly supplied trust
|
|
25
|
+
* anchor. It intentionally does not perform PKI path building or revocation.
|
|
26
|
+
*/
|
|
27
|
+
export async function verifyCertificateChain(options) {
|
|
28
|
+
assertObject(options, "options");
|
|
29
|
+
const at = toEpochMs(options.at ?? Date.now(), "at");
|
|
30
|
+
const intermediatePems = options.intermediateCertificatesPem ?? [];
|
|
31
|
+
if (!Array.isArray(intermediatePems)) {
|
|
32
|
+
throw new Error("intermediateCertificatesPem must be an array");
|
|
33
|
+
}
|
|
34
|
+
if (intermediatePems.length > 1) {
|
|
35
|
+
throw new Error("intermediateCertificatesPem supports at most one certificate");
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(options.trustedRootCertificatesPem) || options.trustedRootCertificatesPem.length === 0) {
|
|
38
|
+
throw new Error("trustedRootCertificatesPem must be a non-empty array");
|
|
39
|
+
}
|
|
40
|
+
if (options.purpose !== undefined &&
|
|
41
|
+
options.purpose !== "ca" &&
|
|
42
|
+
options.purpose !== "clientAuth" &&
|
|
43
|
+
options.purpose !== "documentSigning") {
|
|
44
|
+
throw new Error("purpose must be ca, clientAuth, or documentSigning");
|
|
45
|
+
}
|
|
46
|
+
const target = await parseVerificationCertificate(options.certificatePem, "certificatePem");
|
|
47
|
+
const intermediates = await Promise.all(intermediatePems.map((pem, index) => parseVerificationCertificate(pem, `intermediateCertificatesPem[${index}]`)));
|
|
48
|
+
const roots = await Promise.all(options.trustedRootCertificatesPem.map((pem, index) => parseVerificationCertificate(pem, `trustedRootCertificatesPem[${index}]`)));
|
|
49
|
+
const path = [target, ...intermediates];
|
|
50
|
+
for (let index = 0; index < intermediates.length; index += 1) {
|
|
51
|
+
const intermediate = intermediates[index];
|
|
52
|
+
if (bytesEqual(intermediate.subjectNameDer, intermediate.issuerNameDer)) {
|
|
53
|
+
return failure("invalid-chain-order", index + 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (let index = 0; index < path.length; index += 1) {
|
|
57
|
+
const policyFailure = certificatePolicyFailure(path[index], at, index);
|
|
58
|
+
if (policyFailure)
|
|
59
|
+
return policyFailure;
|
|
60
|
+
}
|
|
61
|
+
const profileFailure = targetProfileFailure(target, options.purpose);
|
|
62
|
+
if (profileFailure)
|
|
63
|
+
return failure(profileFailure, 0);
|
|
64
|
+
for (let childIndex = 0; childIndex < path.length - 1; childIndex += 1) {
|
|
65
|
+
const issuerIndex = childIndex + 1;
|
|
66
|
+
const issuer = path[issuerIndex];
|
|
67
|
+
const issuerFailure = issuerConstraintFailure(issuer, issuerIndex);
|
|
68
|
+
if (issuerFailure)
|
|
69
|
+
return issuerFailure;
|
|
70
|
+
const pathFailure = pathLengthFailure(issuer, path.slice(0, issuerIndex), issuerIndex);
|
|
71
|
+
if (pathFailure)
|
|
72
|
+
return pathFailure;
|
|
73
|
+
const relationshipFailure = await linkFailure(path[childIndex], issuer, childIndex);
|
|
74
|
+
if (relationshipFailure)
|
|
75
|
+
return relationshipFailure;
|
|
76
|
+
}
|
|
77
|
+
const terminalChild = path[path.length - 1];
|
|
78
|
+
const rootIndexInPath = path.length;
|
|
79
|
+
const candidateRootIndexes = [];
|
|
80
|
+
for (let index = 0; index < roots.length; index += 1) {
|
|
81
|
+
if (await issuerIdentityMatches(terminalChild, roots[index])) {
|
|
82
|
+
candidateRootIndexes.push(index);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (candidateRootIndexes.length === 0) {
|
|
86
|
+
return failure("untrusted-root", rootIndexInPath);
|
|
87
|
+
}
|
|
88
|
+
let firstCandidateFailure;
|
|
89
|
+
for (const trustedRootIndex of candidateRootIndexes) {
|
|
90
|
+
const root = roots[trustedRootIndex];
|
|
91
|
+
const checks = [
|
|
92
|
+
certificatePolicyFailure(root, at, rootIndexInPath),
|
|
93
|
+
issuerConstraintFailure(root, rootIndexInPath),
|
|
94
|
+
pathLengthFailure(root, path, rootIndexInPath),
|
|
95
|
+
await linkFailure(terminalChild, root, path.length - 1),
|
|
96
|
+
await rootIntegrityFailure(root, rootIndexInPath)
|
|
97
|
+
];
|
|
98
|
+
const candidateFailure = checks.find((check) => check !== undefined);
|
|
99
|
+
if (!candidateFailure) {
|
|
100
|
+
return { valid: true, trustedRootIndex };
|
|
101
|
+
}
|
|
102
|
+
firstCandidateFailure ??= candidateFailure;
|
|
103
|
+
}
|
|
104
|
+
return firstCandidateFailure ?? failure("untrusted-root", rootIndexInPath);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Legacy compatibility API. Its external validity option and direct-issuer
|
|
108
|
+
* semantics intentionally remain unchanged.
|
|
34
109
|
*
|
|
35
|
-
*
|
|
36
|
-
* `EKU clientAuth`, revocation, and chain walking.
|
|
110
|
+
* This is not mTLS verification and does not authenticate the presenter.
|
|
37
111
|
*/
|
|
38
112
|
export async function verifyClientCertificateIssuedBy(options) {
|
|
39
113
|
if (options.validity && !isWithinValidity(options.validity)) {
|
|
@@ -55,6 +129,116 @@ export async function verifyClientCertificateIssuedBy(options) {
|
|
|
55
129
|
}
|
|
56
130
|
return verifyDer(options.ca.publicKey, cert.signatureDer, cert.tbsCertificateDer);
|
|
57
131
|
}
|
|
132
|
+
async function parseVerificationCertificate(pem, name) {
|
|
133
|
+
if (typeof pem !== "string" || pem.length === 0) {
|
|
134
|
+
throw new Error(`${name} must be a non-empty certificate PEM string`);
|
|
135
|
+
}
|
|
136
|
+
return parseCertificateDerForVerification(pemToDerWithLabel(pem, "CERTIFICATE"));
|
|
137
|
+
}
|
|
138
|
+
function certificatePolicyFailure(certificate, at, certificateIndex) {
|
|
139
|
+
if (certificate.duplicateExtensionOids.length > 0) {
|
|
140
|
+
return failure("duplicate-extension", certificateIndex);
|
|
141
|
+
}
|
|
142
|
+
if (!certificate.signatureAlgorithmMatches) {
|
|
143
|
+
return failure("signature-algorithm-mismatch", certificateIndex);
|
|
144
|
+
}
|
|
145
|
+
if (certificate.unsupportedCriticalExtensionOids.length > 0) {
|
|
146
|
+
return failure("unsupported-critical-extension", certificateIndex);
|
|
147
|
+
}
|
|
148
|
+
if (at < certificate.notBeforeMs) {
|
|
149
|
+
return failure("not-yet-valid", certificateIndex);
|
|
150
|
+
}
|
|
151
|
+
if (at > certificate.notAfterMs) {
|
|
152
|
+
return failure("expired", certificateIndex);
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
function issuerConstraintFailure(issuer, certificateIndex) {
|
|
157
|
+
if (!issuer.basicConstraintsPresent || !issuer.isCA) {
|
|
158
|
+
return failure("issuer-not-ca", certificateIndex);
|
|
159
|
+
}
|
|
160
|
+
if (!issuer.keyUsagePresent || !issuer.keyUsage.keyCertSign) {
|
|
161
|
+
return failure("issuer-key-usage-invalid", certificateIndex);
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
function pathLengthFailure(issuer, certificatesBelow, certificateIndex) {
|
|
166
|
+
if (issuer.pathLenConstraint === undefined)
|
|
167
|
+
return undefined;
|
|
168
|
+
const caCountBelow = certificatesBelow.filter((certificate) => certificate.isCA).length;
|
|
169
|
+
return caCountBelow > issuer.pathLenConstraint
|
|
170
|
+
? failure("path-length-exceeded", certificateIndex)
|
|
171
|
+
: undefined;
|
|
172
|
+
}
|
|
173
|
+
function targetProfileFailure(target, purpose) {
|
|
174
|
+
if (purpose === undefined)
|
|
175
|
+
return undefined;
|
|
176
|
+
if (purpose === "ca") {
|
|
177
|
+
return target.basicConstraintsPresent && target.isCA &&
|
|
178
|
+
target.keyUsagePresent && target.keyUsage.keyCertSign
|
|
179
|
+
? undefined
|
|
180
|
+
: "target-profile-invalid";
|
|
181
|
+
}
|
|
182
|
+
if (!target.basicConstraintsPresent || target.isCA || !target.keyUsagePresent) {
|
|
183
|
+
return "target-profile-invalid";
|
|
184
|
+
}
|
|
185
|
+
if (!target.keyUsage.digitalSignature || !target.extendedKeyUsagePresent) {
|
|
186
|
+
return "target-profile-invalid";
|
|
187
|
+
}
|
|
188
|
+
if (purpose === "clientAuth") {
|
|
189
|
+
return target.extendedKeyUsageOids.includes(OID.clientAuth)
|
|
190
|
+
? undefined
|
|
191
|
+
: "target-profile-invalid";
|
|
192
|
+
}
|
|
193
|
+
return target.keyUsage.contentCommitment &&
|
|
194
|
+
target.extendedKeyUsageOids.includes(OID.documentSigning)
|
|
195
|
+
? undefined
|
|
196
|
+
: "target-profile-invalid";
|
|
197
|
+
}
|
|
198
|
+
async function linkFailure(child, issuer, childIndex) {
|
|
199
|
+
if (!bytesEqual(child.issuerNameDer, issuer.subjectNameDer)) {
|
|
200
|
+
return failure("issuer-name-mismatch", childIndex);
|
|
201
|
+
}
|
|
202
|
+
if (!(await keyIdentifierMatches(child, issuer))) {
|
|
203
|
+
return failure("key-identifier-mismatch", childIndex);
|
|
204
|
+
}
|
|
205
|
+
const expectedAlgorithmOid = signatureAlgorithmOidForCurve(curveOf(issuer.publicKey));
|
|
206
|
+
if (child.signatureAlgorithmOid !== expectedAlgorithmOid) {
|
|
207
|
+
return failure("signature-algorithm-mismatch", childIndex);
|
|
208
|
+
}
|
|
209
|
+
if (!(await verifyDer(issuer.publicKey, child.signatureDer, child.tbsCertificateDer))) {
|
|
210
|
+
return failure("invalid-signature", childIndex);
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
async function issuerIdentityMatches(child, issuer) {
|
|
215
|
+
return bytesEqual(child.issuerNameDer, issuer.subjectNameDer) &&
|
|
216
|
+
await keyIdentifierMatches(child, issuer);
|
|
217
|
+
}
|
|
218
|
+
async function keyIdentifierMatches(child, issuer) {
|
|
219
|
+
if (!child.authorityKeyIdentifier || !issuer.subjectKeyIdentifier)
|
|
220
|
+
return false;
|
|
221
|
+
return bytesEqual(child.authorityKeyIdentifier, issuer.subjectKeyIdentifier);
|
|
222
|
+
}
|
|
223
|
+
async function rootIntegrityFailure(root, certificateIndex) {
|
|
224
|
+
if (!bytesEqual(root.issuerNameDer, root.subjectNameDer)) {
|
|
225
|
+
return failure("issuer-name-mismatch", certificateIndex);
|
|
226
|
+
}
|
|
227
|
+
if (!(await keyIdentifierMatches(root, root))) {
|
|
228
|
+
return failure("key-identifier-mismatch", certificateIndex);
|
|
229
|
+
}
|
|
230
|
+
const expectedAlgorithmOid = signatureAlgorithmOidForCurve(curveOf(root.publicKey));
|
|
231
|
+
if (root.signatureAlgorithmOid !== expectedAlgorithmOid) {
|
|
232
|
+
return failure("signature-algorithm-mismatch", certificateIndex);
|
|
233
|
+
}
|
|
234
|
+
if (!(await verifyDer(root.publicKey, root.signatureDer, root.tbsCertificateDer))) {
|
|
235
|
+
return failure("invalid-signature", certificateIndex);
|
|
236
|
+
}
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
function failure(reason, certificateIndex) {
|
|
240
|
+
return { valid: false, reason, certificateIndex };
|
|
241
|
+
}
|
|
58
242
|
function isWithinValidity(validity) {
|
|
59
243
|
const notBefore = toEpochMs(validity.notBefore, "validity.notBefore");
|
|
60
244
|
const notAfter = toEpochMs(validity.notAfter, "validity.notAfter");
|
|
@@ -71,3 +255,8 @@ function toEpochMs(value, name) {
|
|
|
71
255
|
}
|
|
72
256
|
return ms;
|
|
73
257
|
}
|
|
258
|
+
function assertObject(value, name) {
|
|
259
|
+
if (typeof value !== "object" || value === null) {
|
|
260
|
+
throw new Error(`${name} must be an object`);
|
|
261
|
+
}
|
|
262
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noz-ele/edgca",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A small Cloudflare Workers-friendly toolkit for issuing self-managed CA
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "A small Cloudflare Workers-friendly toolkit for issuing and validating bounded self-managed CA chains.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "NOZ-ELE",
|
|
@@ -46,6 +46,14 @@
|
|
|
46
46
|
"./pkcs12": {
|
|
47
47
|
"types": "./dist/pkcs12.d.ts",
|
|
48
48
|
"import": "./dist/pkcs12.js"
|
|
49
|
+
},
|
|
50
|
+
"./issuer": {
|
|
51
|
+
"types": "./dist/issuer.d.ts",
|
|
52
|
+
"import": "./dist/issuer.js"
|
|
53
|
+
},
|
|
54
|
+
"./verify": {
|
|
55
|
+
"types": "./dist/verify.d.ts",
|
|
56
|
+
"import": "./dist/verify.js"
|
|
49
57
|
}
|
|
50
58
|
},
|
|
51
59
|
"types": "./dist/index.d.ts",
|