@labacacia/nps-sdk 1.0.0-alpha.3 → 1.0.0-alpha.4
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/CHANGELOG.cn.md +53 -0
- package/CHANGELOG.md +62 -0
- package/README.cn.md +8 -2
- package/README.md +8 -2
- package/dist/core/anchor-cache.js +104 -0
- package/dist/core/anchor-cache.js.map +1 -0
- package/dist/core/cache.js +80 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/canonical-json.js +44 -0
- package/dist/core/canonical-json.js.map +1 -0
- package/dist/core/codec.js +119 -0
- package/dist/core/codec.js.map +1 -0
- package/dist/core/codecs/index.js +6 -0
- package/dist/core/codecs/index.js.map +1 -0
- package/dist/core/codecs/ncp-codec.js +93 -0
- package/dist/core/codecs/ncp-codec.js.map +1 -0
- package/dist/core/codecs/tier1-json-codec.js +28 -0
- package/dist/core/codecs/tier1-json-codec.js.map +1 -0
- package/dist/core/codecs/tier2-msgpack-codec.js +26 -0
- package/dist/core/codecs/tier2-msgpack-codec.js.map +1 -0
- package/dist/core/crypto-provider.js +10 -0
- package/dist/core/crypto-provider.js.map +1 -0
- package/dist/core/exceptions.js +52 -0
- package/dist/core/exceptions.js.map +1 -0
- package/dist/core/frame-header.js +185 -0
- package/dist/core/frame-header.js.map +1 -0
- package/dist/core/frame-registry.js +63 -0
- package/dist/core/frame-registry.js.map +1 -0
- package/dist/core/frames.js +154 -0
- package/dist/core/frames.js.map +1 -0
- package/dist/core/index.js +21 -405
- package/dist/core/index.js.map +1 -1
- package/dist/core/registry.js +17 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/status-codes.js +38 -0
- package/dist/core/status-codes.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/ncp/frames/anchor-frame.js +54 -0
- package/dist/ncp/frames/anchor-frame.js.map +1 -0
- package/dist/ncp/frames/caps-frame.js +29 -0
- package/dist/ncp/frames/caps-frame.js.map +1 -0
- package/dist/ncp/frames/diff-frame.js +37 -0
- package/dist/ncp/frames/diff-frame.js.map +1 -0
- package/dist/ncp/frames/error-frame.js +13 -0
- package/dist/ncp/frames/error-frame.js.map +1 -0
- package/dist/ncp/frames/hello-frame.js +25 -0
- package/dist/ncp/frames/hello-frame.js.map +1 -0
- package/dist/ncp/frames/stream-frame.js +18 -0
- package/dist/ncp/frames/stream-frame.js.map +1 -0
- package/dist/ncp/frames.js +192 -0
- package/dist/ncp/frames.js.map +1 -0
- package/dist/ncp/handshake.js +80 -0
- package/dist/ncp/handshake.js.map +1 -0
- package/dist/ncp/index.d.ts +1 -0
- package/dist/ncp/index.d.ts.map +1 -1
- package/dist/ncp/index.js +13 -368
- package/dist/ncp/index.js.map +1 -1
- package/dist/ncp/ncp-error-codes.d.ts +1 -0
- package/dist/ncp/ncp-error-codes.d.ts.map +1 -1
- package/dist/ncp/ncp-error-codes.js +34 -0
- package/dist/ncp/ncp-error-codes.js.map +1 -0
- package/dist/ncp/ncp-patch-format.js +13 -0
- package/dist/ncp/ncp-patch-format.js.map +1 -0
- package/dist/ncp/preamble.d.ts +47 -0
- package/dist/ncp/preamble.d.ts.map +1 -0
- package/dist/ncp/preamble.js +74 -0
- package/dist/ncp/preamble.js.map +1 -0
- package/dist/ncp/registry.js +13 -0
- package/dist/ncp/registry.js.map +1 -0
- package/dist/ncp/stream-manager.js +163 -0
- package/dist/ncp/stream-manager.js.map +1 -0
- package/dist/ndp/frames.js +87 -0
- package/dist/ndp/frames.js.map +1 -0
- package/dist/ndp/index.js +6 -223
- package/dist/ndp/index.js.map +1 -1
- package/dist/ndp/ndp-registry.js +79 -0
- package/dist/ndp/ndp-registry.js.map +1 -0
- package/dist/ndp/registry.js +10 -0
- package/dist/ndp/registry.js.map +1 -0
- package/dist/ndp/validator.js +48 -0
- package/dist/ndp/validator.js.map +1 -0
- package/dist/nip/acme/client.d.ts +31 -0
- package/dist/nip/acme/client.d.ts.map +1 -0
- package/dist/nip/acme/client.js +136 -0
- package/dist/nip/acme/client.js.map +1 -0
- package/dist/nip/acme/index.d.ts +6 -0
- package/dist/nip/acme/index.d.ts.map +1 -0
- package/dist/nip/acme/index.js +8 -0
- package/dist/nip/acme/index.js.map +1 -0
- package/dist/nip/acme/jws.d.ts +31 -0
- package/dist/nip/acme/jws.d.ts.map +1 -0
- package/dist/nip/acme/jws.js +76 -0
- package/dist/nip/acme/jws.js.map +1 -0
- package/dist/nip/acme/messages.d.ts +71 -0
- package/dist/nip/acme/messages.d.ts.map +1 -0
- package/dist/nip/acme/messages.js +4 -0
- package/dist/nip/acme/messages.js.map +1 -0
- package/dist/nip/acme/server.d.ts +41 -0
- package/dist/nip/acme/server.d.ts.map +1 -0
- package/dist/nip/acme/server.js +458 -0
- package/dist/nip/acme/server.js.map +1 -0
- package/dist/nip/acme/wire.d.ts +19 -0
- package/dist/nip/acme/wire.d.ts.map +1 -0
- package/dist/nip/acme/wire.js +21 -0
- package/dist/nip/acme/wire.js.map +1 -0
- package/dist/nip/assurance-level.d.ts +14 -0
- package/dist/nip/assurance-level.d.ts.map +1 -0
- package/dist/nip/assurance-level.js +33 -0
- package/dist/nip/assurance-level.js.map +1 -0
- package/dist/nip/cert-format.d.ts +5 -0
- package/dist/nip/cert-format.d.ts.map +1 -0
- package/dist/nip/cert-format.js +6 -0
- package/dist/nip/cert-format.js.map +1 -0
- package/dist/nip/error-codes.d.ts +23 -0
- package/dist/nip/error-codes.d.ts.map +1 -0
- package/dist/nip/error-codes.js +30 -0
- package/dist/nip/error-codes.js.map +1 -0
- package/dist/nip/frames.d.ts +10 -1
- package/dist/nip/frames.d.ts.map +1 -1
- package/dist/nip/frames.js +106 -0
- package/dist/nip/frames.js.map +1 -0
- package/dist/nip/identity.js +94 -0
- package/dist/nip/identity.js.map +1 -0
- package/dist/nip/index.d.ts +6 -0
- package/dist/nip/index.d.ts.map +1 -1
- package/dist/nip/index.js +12 -187
- package/dist/nip/index.js.map +1 -1
- package/dist/nip/registry.js +10 -0
- package/dist/nip/registry.js.map +1 -0
- package/dist/nip/verifier.d.ts +23 -0
- package/dist/nip/verifier.d.ts.map +1 -0
- package/dist/nip/verifier.js +90 -0
- package/dist/nip/verifier.js.map +1 -0
- package/dist/nip/x509/builder.d.ts +35 -0
- package/dist/nip/x509/builder.d.ts.map +1 -0
- package/dist/nip/x509/builder.js +59 -0
- package/dist/nip/x509/builder.js.map +1 -0
- package/dist/nip/x509/index.d.ts +4 -0
- package/dist/nip/x509/index.d.ts.map +1 -0
- package/dist/nip/x509/index.js +6 -0
- package/dist/nip/x509/index.js.map +1 -0
- package/dist/nip/x509/oids.d.ts +17 -0
- package/dist/nip/x509/oids.d.ts.map +1 -0
- package/dist/nip/x509/oids.js +23 -0
- package/dist/nip/x509/oids.js.map +1 -0
- package/dist/nip/x509/verifier.d.ts +26 -0
- package/dist/nip/x509/verifier.d.ts.map +1 -0
- package/dist/nip/x509/verifier.js +171 -0
- package/dist/nip/x509/verifier.js.map +1 -0
- package/dist/nop/client.js +90 -0
- package/dist/nop/client.js.map +1 -0
- package/dist/nop/frames.js +148 -0
- package/dist/nop/frames.js.map +1 -0
- package/dist/nop/index.js +6 -789
- package/dist/nop/index.js.map +1 -1
- package/dist/nop/models.js +50 -0
- package/dist/nop/models.js.map +1 -0
- package/dist/nop/nop-types.js +44 -0
- package/dist/nop/nop-types.js.map +1 -0
- package/dist/nop/registry.js +11 -0
- package/dist/nop/registry.js.map +1 -0
- package/dist/nwp/client.js +101 -0
- package/dist/nwp/client.js.map +1 -0
- package/dist/nwp/frames.js +81 -0
- package/dist/nwp/frames.js.map +1 -0
- package/dist/nwp/index.js +5 -693
- package/dist/nwp/index.js.map +1 -1
- package/dist/nwp/registry.js +9 -0
- package/dist/nwp/registry.js.map +1 -0
- package/dist/setup.js +29 -0
- package/dist/setup.js.map +1 -0
- package/package.json +2 -1
- package/src/index.ts +1 -1
- package/src/ncp/index.ts +1 -0
- package/src/ncp/ncp-error-codes.ts +2 -0
- package/src/ncp/preamble.ts +79 -0
- package/src/nip/acme/client.ts +185 -0
- package/src/nip/acme/index.ts +8 -0
- package/src/nip/acme/jws.ts +109 -0
- package/src/nip/acme/messages.ts +85 -0
- package/src/nip/acme/server.ts +480 -0
- package/src/nip/acme/wire.ts +24 -0
- package/src/nip/assurance-level.ts +35 -0
- package/src/nip/cert-format.ts +9 -0
- package/src/nip/error-codes.ts +36 -0
- package/src/nip/frames.ts +35 -3
- package/src/nip/index.ts +8 -0
- package/src/nip/verifier.ts +122 -0
- package/src/nip/x509/builder.ts +91 -0
- package/src/nip/x509/index.ts +6 -0
- package/src/nip/x509/oids.ts +28 -0
- package/src/nip/x509/verifier.ts +214 -0
- package/tests/_rfc0002-keys.ts +57 -0
- package/tests/ncp/preamble.test.ts +93 -0
- package/tests/nip-acme-agent01.test.ts +192 -0
- package/tests/nip-x509.test.ts +280 -0
- package/dist/core/index.cjs +0 -452
- package/dist/core/index.cjs.map +0 -1
- package/dist/index.cjs +0 -8
- package/dist/index.cjs.map +0 -1
- package/dist/ncp/index.cjs +0 -388
- package/dist/ncp/index.cjs.map +0 -1
- package/dist/ndp/index.cjs +0 -252
- package/dist/ndp/index.cjs.map +0 -1
- package/dist/nip/index.cjs +0 -214
- package/dist/nip/index.cjs.map +0 -1
- package/dist/nop/index.cjs +0 -823
- package/dist/nop/index.cjs.map +0 -1
- package/dist/nwp/index.cjs +0 -720
- package/dist/nwp/index.cjs.map +0 -1
package/src/nip/frames.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { EncodingTier, FrameType } from "../core/frames.js";
|
|
5
5
|
import type { NpsFrame } from "../core/codec.js";
|
|
6
|
+
import { AssuranceLevel } from "./assurance-level.js";
|
|
6
7
|
|
|
7
8
|
export interface IdentMetadata {
|
|
8
9
|
issuer: string;
|
|
@@ -12,35 +13,66 @@ export interface IdentMetadata {
|
|
|
12
13
|
scopes?: readonly string[];
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
export interface IdentFrameOptions {
|
|
17
|
+
assuranceLevel?: AssuranceLevel | null; // RFC-0003
|
|
18
|
+
certFormat?: string | null; // RFC-0002 — null treated as "v1-proprietary"
|
|
19
|
+
certChain?: readonly string[] | null; // RFC-0002 — base64url(DER), [leaf, intermediates..., root]
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
export class IdentFrame implements NpsFrame {
|
|
16
23
|
readonly frameType = FrameType.IDENT;
|
|
17
24
|
readonly preferredTier = EncodingTier.MSGPACK;
|
|
18
25
|
|
|
26
|
+
readonly assuranceLevel: AssuranceLevel | null;
|
|
27
|
+
readonly certFormat: string | null;
|
|
28
|
+
readonly certChain: readonly string[] | null;
|
|
29
|
+
|
|
19
30
|
constructor(
|
|
20
31
|
public readonly nid: string,
|
|
21
32
|
public readonly pubKey: string,
|
|
22
33
|
public readonly metadata: IdentMetadata,
|
|
23
34
|
public readonly signature: string,
|
|
24
|
-
|
|
35
|
+
options: IdentFrameOptions = {},
|
|
36
|
+
) {
|
|
37
|
+
this.assuranceLevel = options.assuranceLevel ?? null;
|
|
38
|
+
this.certFormat = options.certFormat ?? null;
|
|
39
|
+
this.certChain = options.certChain ?? null;
|
|
40
|
+
}
|
|
25
41
|
|
|
26
42
|
unsignedDict(): Record<string, unknown> {
|
|
27
|
-
|
|
43
|
+
const out: Record<string, unknown> = {
|
|
28
44
|
nid: this.nid,
|
|
29
45
|
pub_key: this.pubKey,
|
|
30
46
|
metadata: this.metadata,
|
|
31
47
|
};
|
|
48
|
+
if (this.assuranceLevel !== null) out["assurance_level"] = this.assuranceLevel.wire;
|
|
49
|
+
// cert_format / cert_chain deliberately excluded from the signed payload —
|
|
50
|
+
// the v1 Ed25519 signature covers only (nid, pub_key, metadata, [assurance_level]).
|
|
51
|
+
return out;
|
|
32
52
|
}
|
|
33
53
|
|
|
34
54
|
toDict(): Record<string, unknown> {
|
|
35
|
-
|
|
55
|
+
const out: Record<string, unknown> = { ...this.unsignedDict(), signature: this.signature };
|
|
56
|
+
if (this.certFormat !== null) out["cert_format"] = this.certFormat;
|
|
57
|
+
if (this.certChain !== null) out["cert_chain"] = [...this.certChain];
|
|
58
|
+
return out;
|
|
36
59
|
}
|
|
37
60
|
|
|
38
61
|
static fromDict(data: Record<string, unknown>): IdentFrame {
|
|
62
|
+
const lvl = data["assurance_level"];
|
|
63
|
+
const assuranceLevel = typeof lvl === "string" ? AssuranceLevel.fromWire(lvl) : null;
|
|
64
|
+
const chainRaw = data["cert_chain"];
|
|
65
|
+
const certChain = Array.isArray(chainRaw) ? (chainRaw as string[]) : null;
|
|
39
66
|
return new IdentFrame(
|
|
40
67
|
data["nid"] as string,
|
|
41
68
|
data["pub_key"] as string,
|
|
42
69
|
data["metadata"] as IdentMetadata,
|
|
43
70
|
data["signature"] as string,
|
|
71
|
+
{
|
|
72
|
+
assuranceLevel,
|
|
73
|
+
certFormat: (data["cert_format"] as string | undefined) ?? null,
|
|
74
|
+
certChain,
|
|
75
|
+
},
|
|
44
76
|
);
|
|
45
77
|
}
|
|
46
78
|
}
|
package/src/nip/index.ts
CHANGED
|
@@ -4,3 +4,11 @@
|
|
|
4
4
|
export * from "./frames.js";
|
|
5
5
|
export * from "./identity.js";
|
|
6
6
|
export { registerNipFrames } from "./registry.js";
|
|
7
|
+
|
|
8
|
+
// RFC-0002 / RFC-0003 — X.509 + ACME + dual-trust verifier
|
|
9
|
+
export * from "./assurance-level.js";
|
|
10
|
+
export * from "./cert-format.js";
|
|
11
|
+
export * from "./error-codes.js";
|
|
12
|
+
export * from "./verifier.js";
|
|
13
|
+
export * as x509 from "./x509/index.js";
|
|
14
|
+
export * as acme from "./acme/index.js";
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* NipIdentVerifier — Phase 1 dual-trust IdentFrame verifier per NPS-RFC-0002 §8.1.
|
|
6
|
+
*
|
|
7
|
+
* Steps:
|
|
8
|
+
* 1. v1 Ed25519 signature check against the issuer's CA public key.
|
|
9
|
+
* 2. Optional minimum assurance level check.
|
|
10
|
+
* 3b. X.509 chain validation (only if `cert_format === "v2-x509"` AND
|
|
11
|
+
* `trustedX509Roots` is configured).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as ed25519 from "@noble/ed25519";
|
|
15
|
+
import { sha512 } from "@noble/hashes/sha512";
|
|
16
|
+
import type { X509Certificate } from "@peculiar/x509";
|
|
17
|
+
|
|
18
|
+
import { AssuranceLevel } from "./assurance-level.js";
|
|
19
|
+
import * as cf from "./cert-format.js";
|
|
20
|
+
import * as ec from "./error-codes.js";
|
|
21
|
+
import type { IdentFrame } from "./frames.js";
|
|
22
|
+
import { verify as verifyX509 } from "./x509/verifier.js";
|
|
23
|
+
|
|
24
|
+
// noble/ed25519 needs sha512 wired up.
|
|
25
|
+
ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m));
|
|
26
|
+
|
|
27
|
+
export interface NipVerifierOptions {
|
|
28
|
+
/** Map of issuer NID → CA public key string (`ed25519:<hex>`). */
|
|
29
|
+
trustedCaPublicKeys?: Readonly<Record<string, string>>;
|
|
30
|
+
/** X.509 trust anchors. Empty/undefined makes Step 3b reject v2 frames. */
|
|
31
|
+
trustedX509Roots?: readonly X509Certificate[];
|
|
32
|
+
/** Minimum required assurance level (NPS-RFC-0003). */
|
|
33
|
+
minAssuranceLevel?: AssuranceLevel;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NipIdentVerifyResult {
|
|
37
|
+
valid: boolean;
|
|
38
|
+
stepFailed: number; // 0 = none, 1 = sig, 2 = assurance, 3 = X.509
|
|
39
|
+
errorCode?: string;
|
|
40
|
+
message?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ok(): NipIdentVerifyResult { return { valid: true, stepFailed: 0 }; }
|
|
44
|
+
|
|
45
|
+
function fail(stepFailed: number, errorCode: string, message: string): NipIdentVerifyResult {
|
|
46
|
+
return { valid: false, stepFailed, errorCode, message };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class NipIdentVerifier {
|
|
50
|
+
constructor(public readonly options: NipVerifierOptions) {}
|
|
51
|
+
|
|
52
|
+
async verify(frame: IdentFrame, issuerNid: string): Promise<NipIdentVerifyResult> {
|
|
53
|
+
// Step 1: v1 Ed25519 signature check ────────────────────────────────
|
|
54
|
+
const caPubKeyStr = this.options.trustedCaPublicKeys?.[issuerNid];
|
|
55
|
+
if (caPubKeyStr === undefined) {
|
|
56
|
+
return fail(1, ec.CERT_UNTRUSTED_ISSUER,
|
|
57
|
+
`no trusted CA public key for issuer: ${issuerNid}`);
|
|
58
|
+
}
|
|
59
|
+
if (!frame.signature?.startsWith("ed25519:")) {
|
|
60
|
+
return fail(1, ec.CERT_SIGNATURE_INVALID, "missing or malformed signature");
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const caPubBytes = parsePubKeyString(caPubKeyStr);
|
|
64
|
+
const sigBytes = Buffer.from(frame.signature.slice("ed25519:".length), "base64");
|
|
65
|
+
const canonical = canonicalJson(frame.unsignedDict());
|
|
66
|
+
const msg = new TextEncoder().encode(canonical);
|
|
67
|
+
if (!ed25519.verify(sigBytes, msg, caPubBytes)) {
|
|
68
|
+
return fail(1, ec.CERT_SIGNATURE_INVALID,
|
|
69
|
+
"v1 Ed25519 signature did not verify against issuer CA key");
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return fail(1, ec.CERT_SIGNATURE_INVALID,
|
|
73
|
+
`v1 signature verification error: ${(e as Error).message}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Step 2: minimum assurance level ───────────────────────────────────
|
|
77
|
+
const minLevel = this.options.minAssuranceLevel;
|
|
78
|
+
if (minLevel !== undefined) {
|
|
79
|
+
const got = frame.assuranceLevel ?? AssuranceLevel.ANONYMOUS;
|
|
80
|
+
if (!got.meetsOrExceeds(minLevel)) {
|
|
81
|
+
return fail(2, ec.ASSURANCE_MISMATCH,
|
|
82
|
+
`assurance_level (${got.wire}) below required minimum (${minLevel.wire})`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Step 3b: X.509 chain check (only if both opt-ins present) ──────────
|
|
87
|
+
const trustedRoots = this.options.trustedX509Roots ?? [];
|
|
88
|
+
const hasV2Trust = trustedRoots.length > 0;
|
|
89
|
+
const isV2Frame = frame.certFormat === cf.V2_X509;
|
|
90
|
+
if (hasV2Trust && isV2Frame) {
|
|
91
|
+
const x509Result = await verifyX509({
|
|
92
|
+
certChainBase64UrlDer: frame.certChain ?? [],
|
|
93
|
+
assertedNid: frame.nid,
|
|
94
|
+
assertedAssuranceLevel: frame.assuranceLevel,
|
|
95
|
+
trustedRootCerts: trustedRoots,
|
|
96
|
+
});
|
|
97
|
+
if (!x509Result.valid) {
|
|
98
|
+
return fail(3,
|
|
99
|
+
x509Result.errorCode ?? ec.CERT_FORMAT_INVALID,
|
|
100
|
+
x509Result.message ?? "X.509 chain validation failed");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return ok();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Canonical JSON matching NipIdentity.sign — top-level keys filtered/ordered
|
|
110
|
+
* via `Object.keys(payload).sort()` as JSON.stringify replacer.
|
|
111
|
+
*/
|
|
112
|
+
function canonicalJson(payload: Record<string, unknown>): string {
|
|
113
|
+
return JSON.stringify(payload, Object.keys(payload).sort());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Parse `ed25519:<hex>` into a 32-byte Uint8Array public key. */
|
|
117
|
+
function parsePubKeyString(s: string): Uint8Array {
|
|
118
|
+
if (!s.startsWith("ed25519:")) {
|
|
119
|
+
throw new Error(`Unsupported public key format: ${s}`);
|
|
120
|
+
}
|
|
121
|
+
return new Uint8Array(Buffer.from(s.slice("ed25519:".length), "hex"));
|
|
122
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Issues NPS X.509 NID certificates per NPS-RFC-0002 §4.
|
|
6
|
+
*
|
|
7
|
+
* Backed by @peculiar/x509 + Web Crypto Ed25519 (Node 22+).
|
|
8
|
+
*
|
|
9
|
+
* Two factory functions:
|
|
10
|
+
* - {@link issueLeaf} — leaf cert with critical NPS EKU + SAN URI = NID + assurance-level extension.
|
|
11
|
+
* - {@link issueRoot} — self-signed root for testing / private-CA use.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as x509 from "@peculiar/x509";
|
|
15
|
+
|
|
16
|
+
import { AssuranceLevel } from "../assurance-level.js";
|
|
17
|
+
import { EKU_AGENT_IDENTITY, EKU_NODE_IDENTITY, NID_ASSURANCE_LEVEL } from "./oids.js";
|
|
18
|
+
|
|
19
|
+
// Initialize @peculiar/x509 cryptoProvider once on first import. Web Crypto
|
|
20
|
+
// (globalThis.crypto) supports Ed25519 in Node 18+.
|
|
21
|
+
x509.cryptoProvider.set(globalThis.crypto);
|
|
22
|
+
|
|
23
|
+
export type LeafRole = "agent" | "node";
|
|
24
|
+
|
|
25
|
+
export interface IssueLeafOptions {
|
|
26
|
+
subjectNid: string;
|
|
27
|
+
subjectPublicKey: CryptoKey; // Ed25519 public key (Web Crypto)
|
|
28
|
+
caKeys: CryptoKeyPair; // CA's keypair (we need privateKey to sign)
|
|
29
|
+
issuerNid: string;
|
|
30
|
+
role: LeafRole;
|
|
31
|
+
assuranceLevel: AssuranceLevel;
|
|
32
|
+
notBefore: Date;
|
|
33
|
+
notAfter: Date;
|
|
34
|
+
serialNumber: string; // hex string, no "0x" prefix
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface IssueRootOptions {
|
|
38
|
+
caNid: string;
|
|
39
|
+
caKeys: CryptoKeyPair;
|
|
40
|
+
notBefore: Date;
|
|
41
|
+
notAfter: Date;
|
|
42
|
+
serialNumber: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Issue a leaf NPS NID certificate (RFC-0002 §4.1). */
|
|
46
|
+
export async function issueLeaf(opts: IssueLeafOptions): Promise<x509.X509Certificate> {
|
|
47
|
+
const ekuOid = opts.role === "node" ? EKU_NODE_IDENTITY : EKU_AGENT_IDENTITY;
|
|
48
|
+
|
|
49
|
+
// ASN.1 ENUMERATED encoding of assurance level: tag=0x0A, len=0x01, value=<rank>.
|
|
50
|
+
const assuranceDer = new Uint8Array([0x0A, 0x01, opts.assuranceLevel.rank]);
|
|
51
|
+
|
|
52
|
+
return x509.X509CertificateGenerator.create({
|
|
53
|
+
serialNumber: opts.serialNumber,
|
|
54
|
+
issuer: `CN=${escapeDn(opts.issuerNid)}`,
|
|
55
|
+
subject: `CN=${escapeDn(opts.subjectNid)}`,
|
|
56
|
+
notBefore: opts.notBefore,
|
|
57
|
+
notAfter: opts.notAfter,
|
|
58
|
+
publicKey: opts.subjectPublicKey,
|
|
59
|
+
signingAlgorithm: { name: "Ed25519" },
|
|
60
|
+
signingKey: opts.caKeys.privateKey,
|
|
61
|
+
extensions: [
|
|
62
|
+
new x509.BasicConstraintsExtension(false, undefined, true),
|
|
63
|
+
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true),
|
|
64
|
+
new x509.ExtendedKeyUsageExtension([ekuOid], true),
|
|
65
|
+
new x509.SubjectAlternativeNameExtension([{ type: "url", value: opts.subjectNid }], false),
|
|
66
|
+
new x509.Extension(NID_ASSURANCE_LEVEL, false, assuranceDer),
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Issue a self-signed CA root cert (testing / private CA). */
|
|
72
|
+
export async function issueRoot(opts: IssueRootOptions): Promise<x509.X509Certificate> {
|
|
73
|
+
return x509.X509CertificateGenerator.createSelfSigned({
|
|
74
|
+
serialNumber: opts.serialNumber,
|
|
75
|
+
name: `CN=${escapeDn(opts.caNid)}`,
|
|
76
|
+
notBefore: opts.notBefore,
|
|
77
|
+
notAfter: opts.notAfter,
|
|
78
|
+
signingAlgorithm: { name: "Ed25519" },
|
|
79
|
+
keys: opts.caKeys,
|
|
80
|
+
extensions: [
|
|
81
|
+
new x509.BasicConstraintsExtension(true, undefined, true),
|
|
82
|
+
new x509.KeyUsagesExtension(
|
|
83
|
+
x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function escapeDn(value: string): string {
|
|
89
|
+
// Escape characters that have special meaning in RFC 4514 DN syntax.
|
|
90
|
+
return value.replace(/([",+;<>\\])/g, "\\$1");
|
|
91
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* OID constants for NPS X.509 certificates per NPS-RFC-0002 §4.
|
|
6
|
+
*
|
|
7
|
+
* The 1.3.6.1.4.1.99999 arc is provisional pending IANA Private Enterprise
|
|
8
|
+
* Number assignment (RFC-0002 §10 OQ-2). All implementations MUST update
|
|
9
|
+
* these constants when the official PEN is granted.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const LAB_ACACIA_PEN_ARC = "1.3.6.1.4.1.99999";
|
|
13
|
+
export const EKU_ARC = `${LAB_ACACIA_PEN_ARC}.1`;
|
|
14
|
+
export const EXTENSION_ARC = `${LAB_ACACIA_PEN_ARC}.2`;
|
|
15
|
+
|
|
16
|
+
// ── EKUs (NPS-RFC-0002 §4.1) ─────────────────────────────────────────────────
|
|
17
|
+
export const EKU_AGENT_IDENTITY = `${EKU_ARC}.1`;
|
|
18
|
+
export const EKU_NODE_IDENTITY = `${EKU_ARC}.2`;
|
|
19
|
+
export const EKU_CA_INTERMEDIATE_AGENT = `${EKU_ARC}.3`;
|
|
20
|
+
|
|
21
|
+
// ── Custom extensions ────────────────────────────────────────────────────────
|
|
22
|
+
export const NID_ASSURANCE_LEVEL = `${EXTENSION_ARC}.1`;
|
|
23
|
+
|
|
24
|
+
// ── Ed25519 algorithm OID per RFC 8410 ───────────────────────────────────────
|
|
25
|
+
export const ED25519 = "1.3.101.112";
|
|
26
|
+
|
|
27
|
+
// ── Standard X.509 OIDs we reference ─────────────────────────────────────────
|
|
28
|
+
export const OID_EXTENDED_KEY_USAGE = "2.5.29.37";
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Verifies NPS X.509 NID certificate chains per NPS-RFC-0002 §4.
|
|
6
|
+
*
|
|
7
|
+
* Stages (RFC §4.6):
|
|
8
|
+
* 1. Decode chain (base64url DER → @peculiar/x509 X509Certificate).
|
|
9
|
+
* 2. Leaf EKU check — critical, contains agent-identity OR node-identity OID.
|
|
10
|
+
* 3. Subject CN / SAN URI match against asserted NID.
|
|
11
|
+
* 4. Assurance-level extension match against asserted level (if both present).
|
|
12
|
+
* 5. Chain signature verification — leaf → intermediates → trusted root.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as x509 from "@peculiar/x509";
|
|
16
|
+
|
|
17
|
+
import { AssuranceLevel } from "../assurance-level.js";
|
|
18
|
+
import * as ec from "../error-codes.js";
|
|
19
|
+
import {
|
|
20
|
+
EKU_AGENT_IDENTITY,
|
|
21
|
+
EKU_NODE_IDENTITY,
|
|
22
|
+
NID_ASSURANCE_LEVEL,
|
|
23
|
+
OID_EXTENDED_KEY_USAGE,
|
|
24
|
+
} from "./oids.js";
|
|
25
|
+
|
|
26
|
+
x509.cryptoProvider.set(globalThis.crypto);
|
|
27
|
+
|
|
28
|
+
export interface NipX509VerifyResult {
|
|
29
|
+
valid: boolean;
|
|
30
|
+
errorCode?: string;
|
|
31
|
+
message?: string;
|
|
32
|
+
leaf?: x509.X509Certificate;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ok(leaf: x509.X509Certificate): NipX509VerifyResult {
|
|
36
|
+
return { valid: true, leaf };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fail(errorCode: string, message: string): NipX509VerifyResult {
|
|
40
|
+
return { valid: false, errorCode, message };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface VerifyOptions {
|
|
44
|
+
certChainBase64UrlDer: readonly string[];
|
|
45
|
+
assertedNid: string;
|
|
46
|
+
assertedAssuranceLevel: AssuranceLevel | null;
|
|
47
|
+
trustedRootCerts: readonly x509.X509Certificate[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function verify(opts: VerifyOptions): Promise<NipX509VerifyResult> {
|
|
51
|
+
// Stage 1: decode chain ─────────────────────────────────────────────────
|
|
52
|
+
if (!opts.certChainBase64UrlDer.length) {
|
|
53
|
+
return fail(ec.CERT_FORMAT_INVALID, "cert_chain is empty");
|
|
54
|
+
}
|
|
55
|
+
let chain: x509.X509Certificate[];
|
|
56
|
+
try {
|
|
57
|
+
chain = opts.certChainBase64UrlDer.map((s) => new x509.X509Certificate(b64uDecode(s).buffer as ArrayBuffer));
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return fail(ec.CERT_FORMAT_INVALID, `DER decode failed: ${(e as Error).message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const leaf = chain[0];
|
|
63
|
+
|
|
64
|
+
// Stage 2: EKU check ────────────────────────────────────────────────────
|
|
65
|
+
const ekuResult = checkLeafEku(leaf);
|
|
66
|
+
if (!ekuResult.valid) return ekuResult;
|
|
67
|
+
|
|
68
|
+
// Stage 3: subject CN / SAN URI match ──────────────────────────────────
|
|
69
|
+
const subjectResult = checkSubjectNid(leaf, opts.assertedNid);
|
|
70
|
+
if (!subjectResult.valid) return subjectResult;
|
|
71
|
+
|
|
72
|
+
// Stage 4: assurance-level extension ───────────────────────────────────
|
|
73
|
+
const assuranceResult = checkAssuranceLevel(leaf, opts.assertedAssuranceLevel);
|
|
74
|
+
if (!assuranceResult.valid) return assuranceResult;
|
|
75
|
+
|
|
76
|
+
// Stage 5: chain signature verification ────────────────────────────────
|
|
77
|
+
const chainResult = await checkChainSignature(chain, opts.trustedRootCerts);
|
|
78
|
+
if (!chainResult.valid) return chainResult;
|
|
79
|
+
|
|
80
|
+
return ok(leaf);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Stage helpers ──────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function checkLeafEku(leaf: x509.X509Certificate): NipX509VerifyResult {
|
|
86
|
+
const ekuExt = leaf.extensions.find(
|
|
87
|
+
(e) => e.type === OID_EXTENDED_KEY_USAGE,
|
|
88
|
+
) as x509.ExtendedKeyUsageExtension | undefined;
|
|
89
|
+
if (!ekuExt) {
|
|
90
|
+
return fail(ec.CERT_EKU_MISSING, "leaf has no ExtendedKeyUsage extension");
|
|
91
|
+
}
|
|
92
|
+
if (!ekuExt.critical) {
|
|
93
|
+
return fail(ec.CERT_EKU_MISSING, "ExtendedKeyUsage extension is not marked critical");
|
|
94
|
+
}
|
|
95
|
+
const usages = ekuExt.usages as readonly string[];
|
|
96
|
+
if (!usages.includes(EKU_AGENT_IDENTITY) && !usages.includes(EKU_NODE_IDENTITY)) {
|
|
97
|
+
return fail(ec.CERT_EKU_MISSING,
|
|
98
|
+
"ExtendedKeyUsage does not contain agent-identity or node-identity OID");
|
|
99
|
+
}
|
|
100
|
+
return ok(leaf);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function checkSubjectNid(leaf: x509.X509Certificate, assertedNid: string): NipX509VerifyResult {
|
|
104
|
+
// @peculiar/x509 parses the subject DN; iterate to find CN.
|
|
105
|
+
const cn = extractCn(leaf.subject);
|
|
106
|
+
if (cn !== assertedNid) {
|
|
107
|
+
return fail(ec.CERT_SUBJECT_NID_MISMATCH,
|
|
108
|
+
`leaf subject CN (${cn ?? "<missing>"}) does not match asserted NID (${assertedNid})`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const sanExt = leaf.getExtension(x509.SubjectAlternativeNameExtension);
|
|
112
|
+
if (!sanExt) {
|
|
113
|
+
return fail(ec.CERT_SUBJECT_NID_MISMATCH, "leaf has no Subject Alternative Name extension");
|
|
114
|
+
}
|
|
115
|
+
// SubjectAlternativeNameExtension exposes general-name objects with `type: "url"` for URIs.
|
|
116
|
+
const uris = sanExt.names
|
|
117
|
+
.toJSON()
|
|
118
|
+
.filter((n: { type: string }) => n.type === "url")
|
|
119
|
+
.map((n: { value: string }) => n.value);
|
|
120
|
+
if (!uris.includes(assertedNid)) {
|
|
121
|
+
return fail(ec.CERT_SUBJECT_NID_MISMATCH, "no SAN URI matches asserted NID");
|
|
122
|
+
}
|
|
123
|
+
return ok(leaf);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function checkAssuranceLevel(
|
|
127
|
+
leaf: x509.X509Certificate, asserted: AssuranceLevel | null,
|
|
128
|
+
): NipX509VerifyResult {
|
|
129
|
+
if (asserted === null) return ok(leaf);
|
|
130
|
+
const ext = leaf.extensions.find((e) => e.type === NID_ASSURANCE_LEVEL);
|
|
131
|
+
if (!ext) {
|
|
132
|
+
// Optional in v0.1 — pass silently.
|
|
133
|
+
return ok(leaf);
|
|
134
|
+
}
|
|
135
|
+
const der = new Uint8Array(ext.value);
|
|
136
|
+
// Decode ASN.1 ENUMERATED: tag=0x0A, len=0x01, content=<rank>.
|
|
137
|
+
if (der.length !== 3 || der[0] !== 0x0A || der[1] !== 0x01) {
|
|
138
|
+
return fail(ec.CERT_FORMAT_INVALID,
|
|
139
|
+
`malformed assurance-level extension: ${Buffer.from(der).toString("hex")}`);
|
|
140
|
+
}
|
|
141
|
+
const rank = der[2];
|
|
142
|
+
let certLevel: AssuranceLevel;
|
|
143
|
+
try {
|
|
144
|
+
certLevel = AssuranceLevel.fromRank(rank);
|
|
145
|
+
} catch {
|
|
146
|
+
return fail(ec.ASSURANCE_UNKNOWN,
|
|
147
|
+
`assurance-level extension contains unknown value: ${rank}`);
|
|
148
|
+
}
|
|
149
|
+
if (certLevel !== asserted) {
|
|
150
|
+
return fail(ec.ASSURANCE_MISMATCH,
|
|
151
|
+
`cert assurance-level (${certLevel.wire}) does not match asserted (${asserted.wire})`);
|
|
152
|
+
}
|
|
153
|
+
return ok(leaf);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function checkChainSignature(
|
|
157
|
+
chain: readonly x509.X509Certificate[],
|
|
158
|
+
trustedRoots: readonly x509.X509Certificate[],
|
|
159
|
+
): Promise<NipX509VerifyResult> {
|
|
160
|
+
if (!trustedRoots.length) {
|
|
161
|
+
return fail(ec.CERT_FORMAT_INVALID, "no trusted X.509 roots configured");
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
// Walk leaf → intermediates: each must be signed by its successor.
|
|
165
|
+
for (let i = 0; i < chain.length - 1; i++) {
|
|
166
|
+
const okStep = await chain[i].verify({ publicKey: await chain[i + 1].publicKey.export(), signatureOnly: true });
|
|
167
|
+
if (!okStep) {
|
|
168
|
+
return fail(ec.CERT_FORMAT_INVALID, `chain link ${i} signature did not verify`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// The last cert in the chain MUST chain to a trusted root.
|
|
172
|
+
const last = chain[chain.length - 1];
|
|
173
|
+
for (const root of trustedRoots) {
|
|
174
|
+
if (Buffer.from(last.rawData).equals(Buffer.from(root.rawData))) {
|
|
175
|
+
return ok(chain[0]);
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const okStep = await last.verify({ publicKey: await root.publicKey.export(), signatureOnly: true });
|
|
179
|
+
if (okStep) return ok(chain[0]);
|
|
180
|
+
} catch {
|
|
181
|
+
/* try next root */
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return fail(ec.CERT_FORMAT_INVALID, "chain does not anchor to any trusted root");
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return fail(ec.CERT_FORMAT_INVALID,
|
|
187
|
+
`chain signature verification error: ${(e as Error).message}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
function extractCn(dn: string): string | null {
|
|
194
|
+
// @peculiar/x509 returns DN strings in RFC 4514 format ("CN=...,O=...").
|
|
195
|
+
for (const rdn of dn.split(",")) {
|
|
196
|
+
const trimmed = rdn.trim();
|
|
197
|
+
if (trimmed.startsWith("CN=")) {
|
|
198
|
+
let value = trimmed.slice(3);
|
|
199
|
+
// Strip surrounding quotes if any.
|
|
200
|
+
if (value.startsWith("\"") && value.endsWith("\"")) {
|
|
201
|
+
value = value.slice(1, -1);
|
|
202
|
+
}
|
|
203
|
+
// Unescape.
|
|
204
|
+
return value.replace(/\\([",+;<>\\])/g, "$1");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function b64uDecode(s: string): Uint8Array {
|
|
211
|
+
const padded = s + "=".repeat((4 - (s.length % 4)) % 4);
|
|
212
|
+
const std = padded.replace(/-/g, "+").replace(/_/g, "/");
|
|
213
|
+
return new Uint8Array(Buffer.from(std, "base64"));
|
|
214
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright 2026 INNO LOTUS PTY LTD
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Shared key-generation helper for RFC-0002 tests. Underscore prefix keeps
|
|
5
|
+
// vitest's `*.test.ts` discovery pattern from picking this up as a suite.
|
|
6
|
+
|
|
7
|
+
import * as ed25519 from "@noble/ed25519";
|
|
8
|
+
import { sha512 } from "@noble/hashes/sha512";
|
|
9
|
+
|
|
10
|
+
ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m));
|
|
11
|
+
|
|
12
|
+
// PKCS8 / SPKI prefixes for Ed25519 are fixed-length and well-defined
|
|
13
|
+
// (RFC 8410). Concatenating them with raw key bytes lets us shuttle a noble
|
|
14
|
+
// keypair through Web Crypto's importKey for use with @peculiar/x509.
|
|
15
|
+
const PKCS8_PREFIX = new Uint8Array([
|
|
16
|
+
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
|
|
17
|
+
0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
|
|
18
|
+
]);
|
|
19
|
+
const SPKI_PREFIX = new Uint8Array([
|
|
20
|
+
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65,
|
|
21
|
+
0x70, 0x03, 0x21, 0x00,
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export interface DualKeyPair {
|
|
25
|
+
privRaw: Uint8Array; // 32 bytes — for noble signing
|
|
26
|
+
pubRaw: Uint8Array; // 32 bytes — for IdentFrame.pub_key + JWK
|
|
27
|
+
webCrypto: CryptoKeyPair; // for @peculiar/x509 signing / CSR generation
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function generateDualKeyPair(): Promise<DualKeyPair> {
|
|
31
|
+
const privRaw = ed25519.utils.randomPrivateKey();
|
|
32
|
+
const pubRaw = ed25519.getPublicKey(privRaw);
|
|
33
|
+
|
|
34
|
+
const pkcs8 = concat(PKCS8_PREFIX, privRaw);
|
|
35
|
+
const spki = concat(SPKI_PREFIX, pubRaw);
|
|
36
|
+
|
|
37
|
+
const subtle = globalThis.crypto.subtle;
|
|
38
|
+
const privateKey = await subtle.importKey(
|
|
39
|
+
"pkcs8", pkcs8.buffer as ArrayBuffer, { name: "Ed25519" }, true, ["sign"]);
|
|
40
|
+
const publicKey = await subtle.importKey(
|
|
41
|
+
"spki", spki.buffer as ArrayBuffer, { name: "Ed25519" }, true, ["verify"]);
|
|
42
|
+
|
|
43
|
+
return { privRaw, pubRaw, webCrypto: { privateKey, publicKey } };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function randomHexSerial(): string {
|
|
47
|
+
const buf = new Uint8Array(20);
|
|
48
|
+
globalThis.crypto.getRandomValues(buf);
|
|
49
|
+
return Buffer.from(buf).toString("hex");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
53
|
+
const out = new Uint8Array(a.length + b.length);
|
|
54
|
+
out.set(a, 0);
|
|
55
|
+
out.set(b, a.length);
|
|
56
|
+
return out;
|
|
57
|
+
}
|