@kindgi/crypto 0.0.0-bootstrap.0 → 0.1.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/LICENSE +201 -0
- package/README.md +112 -2
- package/dist/binding.d.ts +60 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +37 -0
- package/dist/binding.js.map +1 -0
- package/dist/ed25519.d.ts +64 -0
- package/dist/ed25519.d.ts.map +1 -0
- package/dist/ed25519.js +169 -0
- package/dist/ed25519.js.map +1 -0
- package/dist/encoding.d.ts +40 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +123 -0
- package/dist/encoding.js.map +1 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +27 -0
- package/dist/errors.js.map +1 -0
- package/dist/hmac.d.ts +16 -0
- package/dist/hmac.d.ts.map +1 -0
- package/dist/hmac.js +59 -0
- package/dist/hmac.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/webhook.d.ts +74 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +149 -0
- package/dist/webhook.js.map +1 -0
- package/package.json +48 -4
- package/src/binding.ts +103 -0
- package/src/ed25519.ts +228 -0
- package/src/encoding.ts +166 -0
- package/src/errors.ts +84 -0
- package/src/hmac.ts +62 -0
- package/src/index.ts +51 -0
- package/src/webhook.ts +205 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.js","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAEjC;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAIvE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,0CAA0C;AAC1C,MAAM,CAAC,MAAM,qBAAqB,GAAG,QAAQ,CAAC;AAE9C,0DAA0D;AAC1D,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,EAAE,EAAE,YAAY;IAChB,SAAS,EAAE,mBAAmB;IAC9B,SAAS,EAAE,mBAAmB;CACtB,CAAC;AAEX,gFAAgF;AAChF,MAAM,CAAC,MAAM,iCAAiC,GAAG,GAAG,CAAC;AAErD,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAC/B,MAAM,YAAY,GAAG,EAAE,CAAC;AAExB,oFAAoF;AACpF,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,CAAC;AAE3C,yEAAyE;AACzE,MAAM,UAAU,qBAAqB;IACnC,OAAO,GAAG,qBAAqB,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;AACnF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAc;IAClD,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,OAAO,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,MAAM,IAAI,wBAAwB,CAAC;AAChE,CAAC;AAiBD;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,SAAS,CAAC,sEAAsE,CAAC,CAAC;IAC9F,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC;IACjF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,8CAA8C,CAAC,CAAC;IAC9F,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7E,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;QAChC,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO,YAAY,CAAC,oDAAoD,CAAC,CAAC;QAC5F,UAAU,CAAC,IAAI,CAAC,GAAG,iBAAiB,IAAI,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACnF,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AACrD,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,cAAc,CAC5B,KAAuB;IAEvB,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACrC,IAAI,SAAS,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,SAAS,CAAC;IAC/C,OAAO;QACL,IAAI,EAAE,IAAI;QACV,KAAK,EAAE;YACL,CAAC,eAAe,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE;YAC9B,CAAC,eAAe,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC;YACpD,CAAC,eAAe,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,KAAK;SAC7C;KACF,CAAC;AACJ,CAAC;AA6BD;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAyB;IACrD,MAAM,EAAE,GAAG,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC;IACzD,MAAM,YAAY,GAAG,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IAC1E,MAAM,eAAe,GAAG,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IAC7E,IAAI,EAAE,KAAK,SAAS,IAAI,YAAY,KAAK,SAAS,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;QACpF,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IACpD,CAAC;IACD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC1F,MAAM,SAAS,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACvC,MAAM,SAAS,GAAG,KAAK,CAAC,gBAAgB,IAAI,iCAAiC,CAAC;IAC9E,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;IAChE,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,GAAG,SAAS,CAAC,GAAG,SAAS,EAAE,CAAC;QACjD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,4BAA4B,EAAE,CAAC;IAC/D,CAAC;IACD,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAEnE,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,EAAE,YAAY,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACxE,KAAK,MAAM,IAAI,IAAI,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,iBAAiB;YAAE,SAAS;QACtE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC9D,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;YAC/E,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC;QACvC,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,uBAAuB,EAAE,CAAC;AAC1D,CAAC;AAED,SAAS,aAAa,CAAC,EAAU,EAAE,SAAiB,EAAE,IAAyB;IAC7E,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,SAAS,GAAG,EAAE,OAAO,CAAC,CAAC;IAC3D,MAAM,SAAS,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5F,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,IAAI,CAAC,GAAW,EAAE,OAAe;IACxC,OAAO,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;AAC5D,CAAC;AAED,+EAA+E;AAC/E,SAAS,WAAW,CAAC,MAAc;IACjC,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,qBAAqB,CAAC;QACtD,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,MAAM,CAAC;QAC5C,CAAC,CAAC,MAAM,CAAC;IACX,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACjF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC3C,OAAO,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;AACvC,CAAC;AAED,SAAS,UAAU,CAAC,OAA8B,EAAE,IAAY;IAC9D,IAAI,OAAQ,OAA6B,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;QAC7D,MAAM,KAAK,GAAI,OAAgD,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1E,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;IAClE,CAAC;IACD,MAAM,MAAM,GAAG,OAA2E,CAAC;IAC3F,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,IAAI;YAAE,SAAS;QACzC,MAAM,KAAK,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7D,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;IACvE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kindgi/crypto",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kindgi™ cryptographic primitives. Ed25519 signatures for asymmetric use (signed audit bundles, provenance exports, deployment attestations) and HMAC-SHA256 for symmetric use (webhook payloads). Includes SigningKeyBinding interface + in-memory reference impl, PEM/base64 key format converters, and typed error interfaces. Pure Node built-ins, zero runtime deps. Deployments plug their own key store (KMS, Vault, env-var) via SigningKeyBinding.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"repository": {
|
|
7
|
-
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/kindgi/kindgi-sdk.git",
|
|
9
|
+
"directory": "packages/crypto"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/kindgi/kindgi-sdk/tree/main/packages/crypto#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/kindgi/kindgi-sdk/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@kindgi/types": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.0.0"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.10.5",
|
|
41
|
+
"standardwebhooks": "^1.1.1",
|
|
42
|
+
"typescript": "^5.7.3",
|
|
43
|
+
"vitest": "^2.1.8"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/binding.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `SigningKeyBinding` — the seam between the crypto primitives and a
|
|
6
|
+
* deployment's key store. The framework does not persist key material;
|
|
7
|
+
* it never has direct access to a KMS. Instead, callers implement this
|
|
8
|
+
* binding against whatever store they run (an env-var-backed key for
|
|
9
|
+
* local dev, a cloud KMS or secrets vault in production, a hardware
|
|
10
|
+
* token).
|
|
11
|
+
*
|
|
12
|
+
* Consumers that need to sign or verify look up a key by `SigningKeyId`
|
|
13
|
+
* through the binding. The binding returns `null` when a key is not
|
|
14
|
+
* present — callers translate that to the appropriate domain error
|
|
15
|
+
* (e.g., "signing key for tenant T is not provisioned").
|
|
16
|
+
*
|
|
17
|
+
* Not defined here: rotation, revocation, tenant-scoped key selection.
|
|
18
|
+
* Those are lifecycle concerns handled outside this package.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { SigningKeyId } from '@kindgi/types';
|
|
22
|
+
|
|
23
|
+
/** Which signature algorithm the key is for. */
|
|
24
|
+
export type SigningAlgorithm = 'ed25519' | 'hmac-sha256';
|
|
25
|
+
|
|
26
|
+
/** Metadata about a key registered with a binding. */
|
|
27
|
+
export interface SigningKeyDescriptor {
|
|
28
|
+
readonly keyId: SigningKeyId;
|
|
29
|
+
readonly algorithm: SigningAlgorithm;
|
|
30
|
+
/** ISO-8601 timestamp when the key was first registered with this binding. */
|
|
31
|
+
readonly createdAt: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A caller-provided key store. Deployments plug in KMS, env-var-backed,
|
|
36
|
+
* or in-memory-for-tests implementations. Framework code never persists
|
|
37
|
+
* or generates key material through this interface — it only reads.
|
|
38
|
+
*
|
|
39
|
+
* Contracts:
|
|
40
|
+
* - `getPublicKey` returns raw bytes. For Ed25519, 32 bytes; for
|
|
41
|
+
* HMAC-SHA256, the shared secret (the binding does not distinguish
|
|
42
|
+
* the "public" side for symmetric algorithms; verifiers hold the
|
|
43
|
+
* same secret as signers).
|
|
44
|
+
* - `getPrivateKey` returns raw bytes. For Ed25519, 32-byte seed;
|
|
45
|
+
* for HMAC-SHA256, the shared secret.
|
|
46
|
+
* - `null` return means "no key by that id" — never throw.
|
|
47
|
+
* - `listKeys` MUST NOT expose key material, only descriptors.
|
|
48
|
+
*/
|
|
49
|
+
export interface SigningKeyBinding {
|
|
50
|
+
getPublicKey(keyId: SigningKeyId): Uint8Array | null;
|
|
51
|
+
getPrivateKey(keyId: SigningKeyId): Uint8Array | null;
|
|
52
|
+
listKeys(): readonly SigningKeyDescriptor[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* In-memory implementation. Intended for tests and local development —
|
|
57
|
+
* production deployments implement `SigningKeyBinding` against their own
|
|
58
|
+
* KMS. Keys are held by strong reference; the binding does not touch
|
|
59
|
+
* disk or the network.
|
|
60
|
+
*/
|
|
61
|
+
export function createInMemorySigningKeyBinding(
|
|
62
|
+
entries: readonly {
|
|
63
|
+
readonly keyId: SigningKeyId;
|
|
64
|
+
readonly algorithm: SigningAlgorithm;
|
|
65
|
+
readonly publicKey: Uint8Array;
|
|
66
|
+
readonly privateKey: Uint8Array;
|
|
67
|
+
readonly createdAt?: string;
|
|
68
|
+
}[],
|
|
69
|
+
): SigningKeyBinding {
|
|
70
|
+
const map = new Map<
|
|
71
|
+
SigningKeyId,
|
|
72
|
+
{
|
|
73
|
+
readonly publicKey: Uint8Array;
|
|
74
|
+
readonly privateKey: Uint8Array;
|
|
75
|
+
readonly descriptor: SigningKeyDescriptor;
|
|
76
|
+
}
|
|
77
|
+
>();
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (map.has(entry.keyId)) {
|
|
80
|
+
throw new Error(`Duplicate SigningKeyId in binding: "${entry.keyId}"`);
|
|
81
|
+
}
|
|
82
|
+
map.set(entry.keyId, {
|
|
83
|
+
publicKey: entry.publicKey,
|
|
84
|
+
privateKey: entry.privateKey,
|
|
85
|
+
descriptor: {
|
|
86
|
+
keyId: entry.keyId,
|
|
87
|
+
algorithm: entry.algorithm,
|
|
88
|
+
createdAt: entry.createdAt ?? new Date().toISOString(),
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
getPublicKey(keyId) {
|
|
94
|
+
return map.get(keyId)?.publicKey ?? null;
|
|
95
|
+
},
|
|
96
|
+
getPrivateKey(keyId) {
|
|
97
|
+
return map.get(keyId)?.privateKey ?? null;
|
|
98
|
+
},
|
|
99
|
+
listKeys() {
|
|
100
|
+
return Array.from(map.values(), (v) => v.descriptor);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
package/src/ed25519.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ed25519 digital signatures. Asymmetric: the signer holds a private key,
|
|
6
|
+
* verifiers hold only the public key. Used by signed audit-bundle exports,
|
|
7
|
+
* signed provenance exports, and any consumer that needs "prove this
|
|
8
|
+
* artifact came from a party holding the private key" without trusting the
|
|
9
|
+
* verifier.
|
|
10
|
+
*
|
|
11
|
+
* All APIs operate on raw 32-byte Uint8Arrays for keys. PEM / Base64
|
|
12
|
+
* serialization is offered as helpers — the wire encoding is DER SPKI
|
|
13
|
+
* (public) and DER PKCS8 (private).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
type KeyObject,
|
|
18
|
+
createPrivateKey,
|
|
19
|
+
createPublicKey,
|
|
20
|
+
generateKeyPairSync,
|
|
21
|
+
sign as nodeSign,
|
|
22
|
+
verify as nodeVerify,
|
|
23
|
+
} from 'node:crypto';
|
|
24
|
+
|
|
25
|
+
import type { Result } from '@kindgi/types';
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
ED25519_PKCS8_LENGTH,
|
|
29
|
+
ED25519_RAW_KEY_BYTES,
|
|
30
|
+
ED25519_SPKI_LENGTH,
|
|
31
|
+
fromBase64,
|
|
32
|
+
fromPem,
|
|
33
|
+
toBase64,
|
|
34
|
+
toPem,
|
|
35
|
+
unwrapPrivateKeyPkcs8,
|
|
36
|
+
unwrapPublicKeySpki,
|
|
37
|
+
wrapPrivateKeyPkcs8,
|
|
38
|
+
wrapPublicKeySpki,
|
|
39
|
+
} from './encoding.js';
|
|
40
|
+
import { malformedKey, malformedSignature } from './errors.js';
|
|
41
|
+
import type { CryptoError, MalformedKeyError } from './errors.js';
|
|
42
|
+
|
|
43
|
+
/** Byte length of an Ed25519 signature (always 64). */
|
|
44
|
+
export const ED25519_SIGNATURE_BYTES = 64;
|
|
45
|
+
|
|
46
|
+
/** Raw 32-byte key material — the wire form Ed25519 primitives operate on. */
|
|
47
|
+
export interface Ed25519KeyPair {
|
|
48
|
+
readonly publicKey: Uint8Array;
|
|
49
|
+
readonly privateKey: Uint8Array;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate a fresh Ed25519 key pair. Intended for tests, local development,
|
|
54
|
+
* and deployments that provision keys at boot. Production key material
|
|
55
|
+
* typically lives in a KMS / HSM and is loaded through `SigningKeyBinding`.
|
|
56
|
+
*/
|
|
57
|
+
export function generateEd25519KeyPair(): Ed25519KeyPair {
|
|
58
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
59
|
+
const spki = publicKey.export({ format: 'der', type: 'spki' });
|
|
60
|
+
const pkcs8 = privateKey.export({ format: 'der', type: 'pkcs8' });
|
|
61
|
+
const pubUnwrap = unwrapPublicKeySpki(new Uint8Array(spki));
|
|
62
|
+
const privUnwrap = unwrapPrivateKeyPkcs8(new Uint8Array(pkcs8));
|
|
63
|
+
if (pubUnwrap.kind === 'err' || privUnwrap.kind === 'err') {
|
|
64
|
+
throw new Error('generateEd25519KeyPair: unexpected Node key export shape');
|
|
65
|
+
}
|
|
66
|
+
return { publicKey: pubUnwrap.value, privateKey: privUnwrap.value };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Sign a message with an Ed25519 private key. Returns the 64-byte signature.
|
|
71
|
+
* Returns `Result` because the caller-supplied key may be malformed (wrong
|
|
72
|
+
* length, wrong bytes); underlying primitive errors (system-level) throw.
|
|
73
|
+
*/
|
|
74
|
+
export function signEd25519(
|
|
75
|
+
privateKey: Uint8Array,
|
|
76
|
+
message: Uint8Array,
|
|
77
|
+
): Result<Uint8Array, MalformedKeyError> {
|
|
78
|
+
if (!(privateKey instanceof Uint8Array) || privateKey.length !== ED25519_RAW_KEY_BYTES) {
|
|
79
|
+
return malformedKey(
|
|
80
|
+
`Ed25519 private key must be ${ED25519_RAW_KEY_BYTES} raw bytes, got ${privateKey.length}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (!(message instanceof Uint8Array)) {
|
|
84
|
+
return malformedKey('Ed25519 message must be a Uint8Array');
|
|
85
|
+
}
|
|
86
|
+
let keyObject: KeyObject;
|
|
87
|
+
try {
|
|
88
|
+
keyObject = createPrivateKey({
|
|
89
|
+
key: Buffer.from(wrapPrivateKeyPkcs8(privateKey)),
|
|
90
|
+
format: 'der',
|
|
91
|
+
type: 'pkcs8',
|
|
92
|
+
});
|
|
93
|
+
} catch (cause) {
|
|
94
|
+
return malformedKey('Ed25519 private key rejected by underlying primitive', cause);
|
|
95
|
+
}
|
|
96
|
+
const signature = nodeSign(null, message, keyObject);
|
|
97
|
+
return { kind: 'ok', value: new Uint8Array(signature) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Verify an Ed25519 signature. Returns `true` on cryptographic success,
|
|
102
|
+
* `false` on cryptographic failure. Malformed key/signature bytes return
|
|
103
|
+
* a typed error — the caller distinguishes "key/sig shape is wrong"
|
|
104
|
+
* (an operational bug) from "signature does not match" (an authenticity
|
|
105
|
+
* failure). Never returns `true` for a shape-error input.
|
|
106
|
+
*/
|
|
107
|
+
export function verifyEd25519(
|
|
108
|
+
publicKey: Uint8Array,
|
|
109
|
+
message: Uint8Array,
|
|
110
|
+
signature: Uint8Array,
|
|
111
|
+
): Result<boolean, CryptoError> {
|
|
112
|
+
if (!(publicKey instanceof Uint8Array) || publicKey.length !== ED25519_RAW_KEY_BYTES) {
|
|
113
|
+
return malformedKey(
|
|
114
|
+
`Ed25519 public key must be ${ED25519_RAW_KEY_BYTES} raw bytes, got ${publicKey.length}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (!(message instanceof Uint8Array)) {
|
|
118
|
+
return malformedKey('Ed25519 message must be a Uint8Array');
|
|
119
|
+
}
|
|
120
|
+
if (!(signature instanceof Uint8Array) || signature.length !== ED25519_SIGNATURE_BYTES) {
|
|
121
|
+
return malformedSignature(
|
|
122
|
+
`Ed25519 signature must be ${ED25519_SIGNATURE_BYTES} bytes, got ${signature.length}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
let keyObject: KeyObject;
|
|
126
|
+
try {
|
|
127
|
+
keyObject = createPublicKey({
|
|
128
|
+
key: Buffer.from(wrapPublicKeySpki(publicKey)),
|
|
129
|
+
format: 'der',
|
|
130
|
+
type: 'spki',
|
|
131
|
+
});
|
|
132
|
+
} catch (cause) {
|
|
133
|
+
return malformedKey('Ed25519 public key rejected by underlying primitive', cause);
|
|
134
|
+
}
|
|
135
|
+
const ok = nodeVerify(null, message, keyObject, signature);
|
|
136
|
+
return { kind: 'ok', value: ok };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============ serialization helpers ============
|
|
140
|
+
|
|
141
|
+
const PEM_PUBLIC_LABEL = 'PUBLIC KEY';
|
|
142
|
+
const PEM_PRIVATE_LABEL = 'PRIVATE KEY';
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Serialize a raw Ed25519 public key to PEM (standard "PUBLIC KEY" label
|
|
146
|
+
* wrapping a DER SPKI envelope). Interoperable with OpenSSL and any
|
|
147
|
+
* consumer that speaks RFC 5280 SPKI.
|
|
148
|
+
*/
|
|
149
|
+
export function serializePublicKeyPem(publicKey: Uint8Array): string {
|
|
150
|
+
return toPem(wrapPublicKeySpki(publicKey), PEM_PUBLIC_LABEL);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Parse a PEM "PUBLIC KEY" (DER SPKI Ed25519) into raw 32 bytes.
|
|
155
|
+
*/
|
|
156
|
+
export function parsePublicKeyPem(pem: string): Result<Uint8Array, CryptoError> {
|
|
157
|
+
const der = fromPem(pem, PEM_PUBLIC_LABEL);
|
|
158
|
+
if (der.kind === 'err') return der;
|
|
159
|
+
if (der.value.length !== ED25519_SPKI_LENGTH) {
|
|
160
|
+
return malformedKey(
|
|
161
|
+
`PEM decoded to ${der.value.length} bytes; Ed25519 SPKI is ${ED25519_SPKI_LENGTH}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return unwrapPublicKeySpki(der.value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Serialize a raw Ed25519 private key seed to PEM (standard "PRIVATE KEY"
|
|
169
|
+
* label wrapping a DER PKCS8 envelope). WARNING: private key material —
|
|
170
|
+
* never log this, never emit it in journals, never send it over the wire
|
|
171
|
+
* to a verifier.
|
|
172
|
+
*/
|
|
173
|
+
export function serializePrivateKeyPem(privateKey: Uint8Array): string {
|
|
174
|
+
return toPem(wrapPrivateKeyPkcs8(privateKey), PEM_PRIVATE_LABEL);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Parse a PEM "PRIVATE KEY" (DER PKCS8 Ed25519) into raw 32 bytes. */
|
|
178
|
+
export function parsePrivateKeyPem(pem: string): Result<Uint8Array, CryptoError> {
|
|
179
|
+
const der = fromPem(pem, PEM_PRIVATE_LABEL);
|
|
180
|
+
if (der.kind === 'err') return der;
|
|
181
|
+
if (der.value.length !== ED25519_PKCS8_LENGTH) {
|
|
182
|
+
return malformedKey(
|
|
183
|
+
`PEM decoded to ${der.value.length} bytes; Ed25519 PKCS8 is ${ED25519_PKCS8_LENGTH}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return unwrapPrivateKeyPkcs8(der.value);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Serialize a raw Ed25519 public key to base64-encoded DER SPKI. This is
|
|
191
|
+
* the compact JSON-wire form — smaller than PEM (no header lines) and
|
|
192
|
+
* still interoperable with OpenSSL by wrapping in PEM downstream.
|
|
193
|
+
*/
|
|
194
|
+
export function serializePublicKeyBase64(publicKey: Uint8Array): string {
|
|
195
|
+
return toBase64(wrapPublicKeySpki(publicKey));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Parse a base64-encoded DER SPKI Ed25519 public key into raw 32 bytes. */
|
|
199
|
+
export function parsePublicKeyBase64(b64: string): Result<Uint8Array, CryptoError> {
|
|
200
|
+
const der = fromBase64(b64);
|
|
201
|
+
if (der.kind === 'err') return der;
|
|
202
|
+
if (der.value.length !== ED25519_SPKI_LENGTH) {
|
|
203
|
+
return malformedKey(
|
|
204
|
+
`Base64 decoded to ${der.value.length} bytes; Ed25519 SPKI is ${ED25519_SPKI_LENGTH}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return unwrapPublicKeySpki(der.value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Serialize a raw Ed25519 private key seed to base64-encoded DER PKCS8.
|
|
212
|
+
* WARNING: private key material. See `serializePrivateKeyPem` note.
|
|
213
|
+
*/
|
|
214
|
+
export function serializePrivateKeyBase64(privateKey: Uint8Array): string {
|
|
215
|
+
return toBase64(wrapPrivateKeyPkcs8(privateKey));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Parse a base64-encoded DER PKCS8 Ed25519 private key into raw 32 bytes. */
|
|
219
|
+
export function parsePrivateKeyBase64(b64: string): Result<Uint8Array, CryptoError> {
|
|
220
|
+
const der = fromBase64(b64);
|
|
221
|
+
if (der.kind === 'err') return der;
|
|
222
|
+
if (der.value.length !== ED25519_PKCS8_LENGTH) {
|
|
223
|
+
return malformedKey(
|
|
224
|
+
`Base64 decoded to ${der.value.length} bytes; Ed25519 PKCS8 is ${ED25519_PKCS8_LENGTH}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return unwrapPrivateKeyPkcs8(der.value);
|
|
228
|
+
}
|
package/src/encoding.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DER envelope + PEM wrapping helpers for Ed25519 keys.
|
|
6
|
+
*
|
|
7
|
+
* Ed25519 keys have a fixed 32-byte raw form. Interop with Node's
|
|
8
|
+
* `crypto.createPublicKey` / `createPrivateKey` requires wrapping the raw
|
|
9
|
+
* bytes in a standard envelope (SPKI for public, PKCS8 for private) —
|
|
10
|
+
* fixed-prefix DER structures whose only variable part is the 32-byte key
|
|
11
|
+
* material itself. Doing this by hand keeps the package free of ASN.1
|
|
12
|
+
* dependencies and portable across Node / Bun / Deno.
|
|
13
|
+
*
|
|
14
|
+
* The prefixes below encode the AlgorithmIdentifier for Ed25519
|
|
15
|
+
* (OID 1.3.101.112, no parameters) plus the outer SEQUENCE / OCTET STRING
|
|
16
|
+
* wrappers that SPKI (RFC 5280) and PKCS8 (RFC 5958) require. They are
|
|
17
|
+
* constant for Ed25519.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Result } from '@kindgi/types';
|
|
21
|
+
|
|
22
|
+
import { malformedKey, unsupportedFormat } from './errors.js';
|
|
23
|
+
import type { MalformedKeyError, UnsupportedFormatError } from './errors.js';
|
|
24
|
+
|
|
25
|
+
export const ED25519_RAW_KEY_BYTES = 32;
|
|
26
|
+
|
|
27
|
+
// SPKI (SubjectPublicKeyInfo) prefix for Ed25519 (12 bytes). Full envelope
|
|
28
|
+
// is 44 bytes: prefix (12) + raw public key (32).
|
|
29
|
+
const ED25519_SPKI_PREFIX = new Uint8Array([
|
|
30
|
+
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
|
|
31
|
+
]);
|
|
32
|
+
export const ED25519_SPKI_LENGTH = ED25519_SPKI_PREFIX.length + ED25519_RAW_KEY_BYTES; // 44
|
|
33
|
+
|
|
34
|
+
// PKCS8 (PrivateKeyInfo) prefix for Ed25519 (16 bytes). Full envelope is
|
|
35
|
+
// 48 bytes: prefix (16) + raw private key seed (32).
|
|
36
|
+
const ED25519_PKCS8_PREFIX = new Uint8Array([
|
|
37
|
+
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
|
|
38
|
+
]);
|
|
39
|
+
export const ED25519_PKCS8_LENGTH = ED25519_PKCS8_PREFIX.length + ED25519_RAW_KEY_BYTES; // 48
|
|
40
|
+
|
|
41
|
+
/** Wrap a raw 32-byte Ed25519 public key in a DER SPKI envelope. */
|
|
42
|
+
export function wrapPublicKeySpki(rawPublicKey: Uint8Array): Uint8Array {
|
|
43
|
+
if (rawPublicKey.length !== ED25519_RAW_KEY_BYTES) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Ed25519 raw public key must be ${ED25519_RAW_KEY_BYTES} bytes, got ${rawPublicKey.length}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const out = new Uint8Array(ED25519_SPKI_LENGTH);
|
|
49
|
+
out.set(ED25519_SPKI_PREFIX, 0);
|
|
50
|
+
out.set(rawPublicKey, ED25519_SPKI_PREFIX.length);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Wrap a raw 32-byte Ed25519 private key seed in a DER PKCS8 envelope. */
|
|
55
|
+
export function wrapPrivateKeyPkcs8(rawPrivateKey: Uint8Array): Uint8Array {
|
|
56
|
+
if (rawPrivateKey.length !== ED25519_RAW_KEY_BYTES) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Ed25519 raw private key must be ${ED25519_RAW_KEY_BYTES} bytes, got ${rawPrivateKey.length}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const out = new Uint8Array(ED25519_PKCS8_LENGTH);
|
|
62
|
+
out.set(ED25519_PKCS8_PREFIX, 0);
|
|
63
|
+
out.set(rawPrivateKey, ED25519_PKCS8_PREFIX.length);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Strip the DER SPKI envelope off an Ed25519 public key, returning the 32 raw bytes. */
|
|
68
|
+
export function unwrapPublicKeySpki(spki: Uint8Array): Result<Uint8Array, MalformedKeyError> {
|
|
69
|
+
if (spki.length !== ED25519_SPKI_LENGTH) {
|
|
70
|
+
return malformedKey(
|
|
71
|
+
`Ed25519 SPKI envelope must be ${ED25519_SPKI_LENGTH} bytes, got ${spki.length}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
for (let i = 0; i < ED25519_SPKI_PREFIX.length; i++) {
|
|
75
|
+
if (spki[i] !== ED25519_SPKI_PREFIX[i]) {
|
|
76
|
+
return malformedKey('Ed25519 SPKI prefix mismatch — not an Ed25519 public key envelope');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { kind: 'ok', value: spki.slice(ED25519_SPKI_PREFIX.length) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Strip the DER PKCS8 envelope off an Ed25519 private key, returning the 32 raw seed bytes. */
|
|
83
|
+
export function unwrapPrivateKeyPkcs8(pkcs8: Uint8Array): Result<Uint8Array, MalformedKeyError> {
|
|
84
|
+
if (pkcs8.length !== ED25519_PKCS8_LENGTH) {
|
|
85
|
+
return malformedKey(
|
|
86
|
+
`Ed25519 PKCS8 envelope must be ${ED25519_PKCS8_LENGTH} bytes, got ${pkcs8.length}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
for (let i = 0; i < ED25519_PKCS8_PREFIX.length; i++) {
|
|
90
|
+
if (pkcs8[i] !== ED25519_PKCS8_PREFIX[i]) {
|
|
91
|
+
return malformedKey('Ed25519 PKCS8 prefix mismatch — not an Ed25519 private key envelope');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { kind: 'ok', value: pkcs8.slice(ED25519_PKCS8_PREFIX.length) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============ base64 ============
|
|
98
|
+
|
|
99
|
+
/** Encode bytes as standard base64. */
|
|
100
|
+
export function toBase64(bytes: Uint8Array): string {
|
|
101
|
+
return Buffer.from(bytes).toString('base64');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Decode a base64 string into bytes. Returns a typed error on invalid input. */
|
|
105
|
+
export function fromBase64(b64: string): Result<Uint8Array, MalformedKeyError> {
|
|
106
|
+
if (typeof b64 !== 'string' || b64.length === 0) {
|
|
107
|
+
return malformedKey('base64 input must be a non-empty string');
|
|
108
|
+
}
|
|
109
|
+
// Buffer.from is lenient about non-base64 characters; validate strictly.
|
|
110
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) {
|
|
111
|
+
return malformedKey('base64 input contains characters outside the standard alphabet');
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const buf = Buffer.from(b64, 'base64');
|
|
115
|
+
// Round-trip to detect padding / silent truncation.
|
|
116
|
+
if (buf.toString('base64').replace(/=+$/u, '') !== b64.replace(/=+$/u, '')) {
|
|
117
|
+
return malformedKey('base64 input did not round-trip cleanly');
|
|
118
|
+
}
|
|
119
|
+
return { kind: 'ok', value: new Uint8Array(buf) };
|
|
120
|
+
} catch (cause) {
|
|
121
|
+
return malformedKey('base64 decode failed', cause);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============ PEM ============
|
|
126
|
+
|
|
127
|
+
/** Wrap DER bytes in a PEM envelope with the given label. */
|
|
128
|
+
export function toPem(derBytes: Uint8Array, label: string): string {
|
|
129
|
+
const b64 = toBase64(derBytes);
|
|
130
|
+
const lines: string[] = [];
|
|
131
|
+
for (let i = 0; i < b64.length; i += 64) lines.push(b64.slice(i, i + 64));
|
|
132
|
+
return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----\n`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse a PEM envelope for the given label. Rejects unexpected labels,
|
|
137
|
+
* missing header/footer, or base64 payload that does not decode cleanly.
|
|
138
|
+
*/
|
|
139
|
+
export function fromPem(
|
|
140
|
+
pem: string,
|
|
141
|
+
expectedLabel: string,
|
|
142
|
+
): Result<Uint8Array, MalformedKeyError | UnsupportedFormatError> {
|
|
143
|
+
if (typeof pem !== 'string' || pem.length === 0) {
|
|
144
|
+
return malformedKey('PEM input must be a non-empty string');
|
|
145
|
+
}
|
|
146
|
+
const trimmed = pem.trim();
|
|
147
|
+
const header = `-----BEGIN ${expectedLabel}-----`;
|
|
148
|
+
const footer = `-----END ${expectedLabel}-----`;
|
|
149
|
+
const startIdx = trimmed.indexOf(header);
|
|
150
|
+
const endIdx = trimmed.indexOf(footer);
|
|
151
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
152
|
+
// Detect a *different* PEM label (e.g., someone passed an RSA key here).
|
|
153
|
+
const otherLabelMatch = /-----BEGIN ([^-]+)-----/.exec(trimmed);
|
|
154
|
+
if (otherLabelMatch && otherLabelMatch[1] !== expectedLabel) {
|
|
155
|
+
return unsupportedFormat(
|
|
156
|
+
`PEM label mismatch — expected "${expectedLabel}", got "${otherLabelMatch[1]}"`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return malformedKey(`PEM envelope missing "${expectedLabel}" header/footer`);
|
|
160
|
+
}
|
|
161
|
+
if (endIdx < startIdx) {
|
|
162
|
+
return malformedKey('PEM footer appears before header');
|
|
163
|
+
}
|
|
164
|
+
const body = trimmed.slice(startIdx + header.length, endIdx).replace(/\s+/g, '');
|
|
165
|
+
return fromBase64(body);
|
|
166
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Typed errors returned from the crypto primitives. Cryptographic operations
|
|
6
|
+
* fall into two buckets: (a) caller-recoverable errors — malformed key
|
|
7
|
+
* material, wrong-shape input, bad PEM headers — and (b) system-level errors
|
|
8
|
+
* from the underlying primitive that a caller cannot recover from. The
|
|
9
|
+
* former return as `Result<T, CryptoError>`; the latter throw. This file
|
|
10
|
+
* declares (a).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type CryptoError = MalformedKeyError | MalformedSignatureError | UnsupportedFormatError;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Key material could not be parsed. Covers malformed PEM headers, wrong
|
|
17
|
+
* base64, DER envelopes with unexpected OIDs, and byte-length mismatches
|
|
18
|
+
* for the declared algorithm.
|
|
19
|
+
*/
|
|
20
|
+
export interface MalformedKeyError {
|
|
21
|
+
readonly code: 'malformed-key';
|
|
22
|
+
readonly message: string;
|
|
23
|
+
readonly cause?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Signature bytes are the wrong length or shape for the declared algorithm.
|
|
28
|
+
* Returned by verify when the caller-supplied signature can be rejected
|
|
29
|
+
* without invoking the underlying primitive.
|
|
30
|
+
*/
|
|
31
|
+
export interface MalformedSignatureError {
|
|
32
|
+
readonly code: 'malformed-signature';
|
|
33
|
+
readonly message: string;
|
|
34
|
+
readonly cause?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A serialization format was requested that this package does not support
|
|
39
|
+
* (e.g., a PEM label the parser does not recognize).
|
|
40
|
+
*/
|
|
41
|
+
export interface UnsupportedFormatError {
|
|
42
|
+
readonly code: 'unsupported-format';
|
|
43
|
+
readonly message: string;
|
|
44
|
+
readonly cause?: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function malformedKey(
|
|
48
|
+
message: string,
|
|
49
|
+
cause?: unknown,
|
|
50
|
+
): { readonly kind: 'err'; readonly error: MalformedKeyError } {
|
|
51
|
+
return {
|
|
52
|
+
kind: 'err',
|
|
53
|
+
error:
|
|
54
|
+
cause === undefined
|
|
55
|
+
? { code: 'malformed-key', message }
|
|
56
|
+
: { code: 'malformed-key', message, cause },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function malformedSignature(
|
|
61
|
+
message: string,
|
|
62
|
+
cause?: unknown,
|
|
63
|
+
): { readonly kind: 'err'; readonly error: MalformedSignatureError } {
|
|
64
|
+
return {
|
|
65
|
+
kind: 'err',
|
|
66
|
+
error:
|
|
67
|
+
cause === undefined
|
|
68
|
+
? { code: 'malformed-signature', message }
|
|
69
|
+
: { code: 'malformed-signature', message, cause },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function unsupportedFormat(
|
|
74
|
+
message: string,
|
|
75
|
+
cause?: unknown,
|
|
76
|
+
): { readonly kind: 'err'; readonly error: UnsupportedFormatError } {
|
|
77
|
+
return {
|
|
78
|
+
kind: 'err',
|
|
79
|
+
error:
|
|
80
|
+
cause === undefined
|
|
81
|
+
? { code: 'unsupported-format', message }
|
|
82
|
+
: { code: 'unsupported-format', message, cause },
|
|
83
|
+
};
|
|
84
|
+
}
|