@kindgi/crypto 0.1.4 → 0.1.5-rc.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 +24 -0
- package/dist/export-signing.d.ts +91 -0
- package/dist/export-signing.d.ts.map +1 -0
- package/dist/export-signing.js +283 -0
- package/dist/export-signing.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/export-signing.ts +364 -0
- package/src/index.ts +17 -0
package/README.md
CHANGED
|
@@ -105,6 +105,30 @@ if (priv === null) throw new Error('key not provisioned');
|
|
|
105
105
|
|
|
106
106
|
`createInMemorySigningKeyBinding` is for tests and local development. Production deployments implement the interface against their own key store (a cloud KMS, a secrets vault, an HSM). This package never persists key material.
|
|
107
107
|
|
|
108
|
+
## ExportSigningBinding
|
|
109
|
+
|
|
110
|
+
The key a deployment signs its exports with (audit bundles, provenance, compliance evidence). It's async, so a KMS that never hands out its private key can implement it, as well as a key file can:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { createExportSignerFromPem } from '@kindgi/crypto';
|
|
114
|
+
|
|
115
|
+
// An Ed25519 key signs `ed25519`; an EC P-256 key signs `ecdsa-p256-sha256`.
|
|
116
|
+
const made = createExportSignerFromPem(await readFile(keyPath, 'utf8'));
|
|
117
|
+
if (made.kind === 'err') throw new Error(made.error.message);
|
|
118
|
+
const signer = made.value;
|
|
119
|
+
|
|
120
|
+
signer.activeKey(); // { keyId: 'ex_…', algorithm: 'ed25519', publicKeyPem, fingerprint: 'sha256:…' }
|
|
121
|
+
const signed = await signer.sign(bytes); // the active key, or { keyId }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- **Key ids are derived from the public key** (`exportSigningKey`), so the same key keeps its id.
|
|
125
|
+
- **`listKeys()`** is every key a verifier should trust, active first.
|
|
126
|
+
- **`exportSignerFromSigningKeyBinding`** adapts a `SigningKeyBinding`'s Ed25519 keys.
|
|
127
|
+
- **Two algorithms, chosen per key:** `ed25519` (the default) and `ecdsa-p256-sha256`, for a KMS without Ed25519. An ECDSA signature is IEEE P1363 `r‖s`, 64 bytes. A KMS binding that gets DER back (Cloud KMS, AWS KMS) converts with `ecdsaDerToP1363`.
|
|
128
|
+
- **Test vectors** for both are in `@kindgi/specs` (`test-vectors/signed-export/`).
|
|
129
|
+
|
|
130
|
+
`@kindgi/api` takes one as `createApp({ exportSigning })`. To verify an export, use `verifySignedExport` from `@kindgi/client` (Web Crypto, so it runs in a browser too).
|
|
131
|
+
|
|
108
132
|
## Security notes
|
|
109
133
|
|
|
110
134
|
- **Never log private keys or shared secrets.** Journals, provenance blobs, structured logs, and wire payloads must not contain key material except by explicit caller intent.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Result } from '@kindgi/types';
|
|
2
|
+
import type { SigningKeyBinding } from './binding.js';
|
|
3
|
+
import type { CryptoError } from './errors.js';
|
|
4
|
+
/** The algorithms an export can be signed with. */
|
|
5
|
+
export declare const EXPORT_SIGNING_ALGORITHMS: readonly ["ed25519", "ecdsa-p256-sha256"];
|
|
6
|
+
export type ExportSigningAlgorithm = (typeof EXPORT_SIGNING_ALGORITHMS)[number];
|
|
7
|
+
/** A key exports are signed with, as a verifier sees it. No private material. */
|
|
8
|
+
export interface ExportSigningKey {
|
|
9
|
+
/**
|
|
10
|
+
* `ex_` and 16 base64url characters of the SHA-256 of the raw public
|
|
11
|
+
* key: Ed25519's 32 bytes, P-256's 65-byte uncompressed point.
|
|
12
|
+
*/
|
|
13
|
+
readonly keyId: string;
|
|
14
|
+
readonly algorithm: ExportSigningAlgorithm;
|
|
15
|
+
/** The public key, PEM SPKI. */
|
|
16
|
+
readonly publicKeyPem: string;
|
|
17
|
+
/** `sha256:` and the hex SHA-256 of the raw public key: to pin it, or compare by eye. */
|
|
18
|
+
readonly fingerprint: string;
|
|
19
|
+
}
|
|
20
|
+
export type ExportSigningError = {
|
|
21
|
+
/** `keyId` was asked for and isn't a key this binding signs with. */
|
|
22
|
+
readonly code: 'signing-key-not-found';
|
|
23
|
+
readonly message: string;
|
|
24
|
+
readonly keyId: string;
|
|
25
|
+
} | {
|
|
26
|
+
/** The signer failed: a KMS that refused or didn't answer, a malformed key. */
|
|
27
|
+
readonly code: 'export-signing-failed';
|
|
28
|
+
readonly message: string;
|
|
29
|
+
readonly cause?: unknown;
|
|
30
|
+
};
|
|
31
|
+
export interface ExportSignature {
|
|
32
|
+
readonly key: ExportSigningKey;
|
|
33
|
+
readonly signature: Uint8Array;
|
|
34
|
+
}
|
|
35
|
+
export interface ExportSigningBinding {
|
|
36
|
+
/** The key `sign` uses when no `keyId` is asked for. */
|
|
37
|
+
activeKey(): ExportSigningKey;
|
|
38
|
+
/** Every key a verifier should trust, the active one first. */
|
|
39
|
+
listKeys(): readonly ExportSigningKey[];
|
|
40
|
+
/**
|
|
41
|
+
* Sign `bytes` with the active key, or with `keyId` when given. A
|
|
42
|
+
* `keyId` this binding can't sign with is `signing-key-not-found`.
|
|
43
|
+
*/
|
|
44
|
+
sign(bytes: Uint8Array, options?: {
|
|
45
|
+
readonly keyId?: string;
|
|
46
|
+
}): Promise<Result<ExportSignature, ExportSigningError>>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The `ExportSigningKey` for a raw public key, under its derived id: a
|
|
50
|
+
* 32-byte Ed25519 key (the default), or a 65-byte uncompressed P-256 point
|
|
51
|
+
* for `ecdsa-p256-sha256`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function exportSigningKey(publicKey: Uint8Array, algorithm?: ExportSigningAlgorithm): ExportSigningKey;
|
|
54
|
+
/**
|
|
55
|
+
* An export signer over an EC P-256 private key held in memory (PKCS#8 or
|
|
56
|
+
* SEC1 PEM): `ecdsa-p256-sha256`, its signatures as P1363 `r‖s`. `err`
|
|
57
|
+
* when the key isn't a P-256 private key.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createEcdsaP256ExportSigner(input: {
|
|
60
|
+
readonly privateKeyPem: string;
|
|
61
|
+
}): Result<ExportSigningBinding, CryptoError>;
|
|
62
|
+
/**
|
|
63
|
+
* The export signer for a PEM private key, its algorithm read from the
|
|
64
|
+
* key: Ed25519 → `ed25519`, EC P-256 → `ecdsa-p256-sha256`. Any other key
|
|
65
|
+
* is `err`, naming what it is.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createExportSignerFromPem(privateKeyPem: string): Result<ExportSigningBinding, CryptoError>;
|
|
68
|
+
/**
|
|
69
|
+
* An ECDSA signature from DER (`SEQUENCE { INTEGER r, INTEGER s }`, what
|
|
70
|
+
* Cloud KMS and AWS KMS answer) to IEEE P1363 `r‖s`, each half `size`
|
|
71
|
+
* bytes (32 for P-256): the form exports carry and Web Crypto verifies.
|
|
72
|
+
*/
|
|
73
|
+
export declare function ecdsaDerToP1363(der: Uint8Array, size?: number): Result<Uint8Array, CryptoError>;
|
|
74
|
+
/**
|
|
75
|
+
* An export signer over an Ed25519 private key held in memory: a PEM
|
|
76
|
+
* file's contents, a base64 env value decoded, a test's or a
|
|
77
|
+
* development server's generated key. `err` when the key isn't an
|
|
78
|
+
* Ed25519 PKCS#8 key.
|
|
79
|
+
*/
|
|
80
|
+
export declare function createEd25519ExportSigner(input: {
|
|
81
|
+
readonly privateKeyPem: string;
|
|
82
|
+
} | {
|
|
83
|
+
readonly privateKey: Uint8Array;
|
|
84
|
+
}): Result<ExportSigningBinding, CryptoError>;
|
|
85
|
+
/**
|
|
86
|
+
* The `ExportSigningBinding` over a `SigningKeyBinding`'s Ed25519 keys,
|
|
87
|
+
* under the ids that binding gives them; the first is the active one.
|
|
88
|
+
* `undefined` when it holds no Ed25519 key (nothing to sign with).
|
|
89
|
+
*/
|
|
90
|
+
export declare function exportSignerFromSigningKeyBinding(binding: SigningKeyBinding): ExportSigningBinding | undefined;
|
|
91
|
+
//# sourceMappingURL=export-signing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-signing.d.ts","sourceRoot":"","sources":["../src/export-signing.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAE5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAGtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C,mDAAmD;AACnD,eAAO,MAAM,yBAAyB,2CAA4C,CAAC;AACnF,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhF,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,sBAAsB,CAAC;IAC3C,gCAAgC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,yFAAyF;IACzF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,MAAM,kBAAkB,GAC1B;IACE,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,GACD;IACE,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEN,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,gBAAgB,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;CAChC;AAED,MAAM,WAAW,oBAAoB;IACnC,wDAAwD;IACxD,SAAS,IAAI,gBAAgB,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,IAAI,SAAS,gBAAgB,EAAE,CAAC;IACxC;;;OAGG;IACH,IAAI,CACF,KAAK,EAAE,UAAU,EACjB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GACpC,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,CAAC,CAAC;CACzD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,UAAU,EACrB,SAAS,GAAE,sBAAkC,GAC5C,gBAAgB,CAUlB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE;IACjD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC,GAAG,MAAM,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAsC5C;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,aAAa,EAAE,MAAM,GACpB,MAAM,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAa3C;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,SAAK,GAAG,MAAM,CAAC,UAAU,EAAE,WAAW,CAAC,CAqC3F;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE;IAAE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAA;CAAE,GAC9E,MAAM,CAAC,oBAAoB,EAAE,WAAW,CAAC,CAkB3C;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,iBAAiB,GACzB,oBAAoB,GAAG,SAAS,CAuBlC"}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
/**
|
|
4
|
+
* `ExportSigningBinding` — the key a deployment signs its exports with:
|
|
5
|
+
* audit bundles, provenance exports and compliance evidence. Async, so
|
|
6
|
+
* a KMS that never hands out its private key (Cloud KMS
|
|
7
|
+
* `asymmetricSign`) can implement it as well as a key file can.
|
|
8
|
+
*
|
|
9
|
+
* One key signs at a time (`activeKey`); `listKeys` is every key a
|
|
10
|
+
* verifier should trust, active first. A key's id is derived from its
|
|
11
|
+
* public key (`exportSigningKey`), so the same key has the same id
|
|
12
|
+
* across restarts and deployments, with nothing to configure.
|
|
13
|
+
*
|
|
14
|
+
* Two algorithms, chosen per key (`ExportSigningKey.algorithm`):
|
|
15
|
+
* `ed25519`, the default for a key in memory or a file, and
|
|
16
|
+
* `ecdsa-p256-sha256`, for a KMS that has no Ed25519. An ECDSA signature
|
|
17
|
+
* travels as IEEE P1363 `r‖s` (64 bytes), the form Web Crypto verifies;
|
|
18
|
+
* a KMS that answers DER converts with `ecdsaDerToP1363`.
|
|
19
|
+
*
|
|
20
|
+
* Not the same seam as `SigningKeyBinding` (raw key bytes, read
|
|
21
|
+
* synchronously, for public run tokens on the request path).
|
|
22
|
+
* `exportSignerFromSigningKeyBinding` adapts one to the other.
|
|
23
|
+
*/
|
|
24
|
+
import { createHash, createPrivateKey, createPublicKey, sign as nodeSign, } from 'node:crypto';
|
|
25
|
+
import { parsePrivateKeyPem, serializePublicKeyPem, signEd25519 } from './ed25519.js';
|
|
26
|
+
import { ED25519_RAW_KEY_BYTES, unwrapPublicKeySpki, wrapPrivateKeyPkcs8 } from './encoding.js';
|
|
27
|
+
import { malformedKey } from './errors.js';
|
|
28
|
+
/** The algorithms an export can be signed with. */
|
|
29
|
+
export const EXPORT_SIGNING_ALGORITHMS = ['ed25519', 'ecdsa-p256-sha256'];
|
|
30
|
+
/**
|
|
31
|
+
* The `ExportSigningKey` for a raw public key, under its derived id: a
|
|
32
|
+
* 32-byte Ed25519 key (the default), or a 65-byte uncompressed P-256 point
|
|
33
|
+
* for `ecdsa-p256-sha256`.
|
|
34
|
+
*/
|
|
35
|
+
export function exportSigningKey(publicKey, algorithm = 'ed25519') {
|
|
36
|
+
const keyId = `ex_${sha256(publicKey).toString('base64url').slice(0, 16)}`;
|
|
37
|
+
return algorithm === 'ed25519'
|
|
38
|
+
? describeKey(keyId, publicKey)
|
|
39
|
+
: {
|
|
40
|
+
keyId,
|
|
41
|
+
algorithm,
|
|
42
|
+
publicKeyPem: p256PublicKeyPem(publicKey),
|
|
43
|
+
fingerprint: `sha256:${sha256(publicKey).toString('hex')}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* An export signer over an EC P-256 private key held in memory (PKCS#8 or
|
|
48
|
+
* SEC1 PEM): `ecdsa-p256-sha256`, its signatures as P1363 `r‖s`. `err`
|
|
49
|
+
* when the key isn't a P-256 private key.
|
|
50
|
+
*/
|
|
51
|
+
export function createEcdsaP256ExportSigner(input) {
|
|
52
|
+
let privateKey;
|
|
53
|
+
try {
|
|
54
|
+
privateKey = createPrivateKey({ key: input.privateKeyPem, format: 'pem' });
|
|
55
|
+
}
|
|
56
|
+
catch (cause) {
|
|
57
|
+
return malformedKey('the export signing key is not a readable PEM private key', cause);
|
|
58
|
+
}
|
|
59
|
+
const curve = privateKey.asymmetricKeyDetails?.namedCurve;
|
|
60
|
+
if (privateKey.asymmetricKeyType !== 'ec' || curve !== 'prime256v1') {
|
|
61
|
+
return malformedKey(`the export signing key is ${describeKeyType(privateKey)}, not an EC P-256 key`);
|
|
62
|
+
}
|
|
63
|
+
const jwk = createPublicKey(privateKey).export({ format: 'jwk' });
|
|
64
|
+
const point = Buffer.concat([
|
|
65
|
+
Buffer.from([0x04]),
|
|
66
|
+
Buffer.from(jwk.x, 'base64url'),
|
|
67
|
+
Buffer.from(jwk.y, 'base64url'),
|
|
68
|
+
]);
|
|
69
|
+
const key = exportSigningKey(new Uint8Array(point), 'ecdsa-p256-sha256');
|
|
70
|
+
return ok({
|
|
71
|
+
activeKey: () => key,
|
|
72
|
+
listKeys: () => [key],
|
|
73
|
+
async sign(bytes, options) {
|
|
74
|
+
if (options?.keyId !== undefined && options.keyId !== key.keyId) {
|
|
75
|
+
return { kind: 'err', error: keyNotFound(options.keyId, [key]) };
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const signature = nodeSign('sha256', bytes, { key: privateKey, dsaEncoding: 'ieee-p1363' });
|
|
79
|
+
return ok({ key, signature: new Uint8Array(signature) });
|
|
80
|
+
}
|
|
81
|
+
catch (cause) {
|
|
82
|
+
return {
|
|
83
|
+
kind: 'err',
|
|
84
|
+
error: { code: 'export-signing-failed', message: 'ECDSA P-256 signing failed', cause },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The export signer for a PEM private key, its algorithm read from the
|
|
92
|
+
* key: Ed25519 → `ed25519`, EC P-256 → `ecdsa-p256-sha256`. Any other key
|
|
93
|
+
* is `err`, naming what it is.
|
|
94
|
+
*/
|
|
95
|
+
export function createExportSignerFromPem(privateKeyPem) {
|
|
96
|
+
let privateKey;
|
|
97
|
+
try {
|
|
98
|
+
privateKey = createPrivateKey({ key: privateKeyPem, format: 'pem' });
|
|
99
|
+
}
|
|
100
|
+
catch (cause) {
|
|
101
|
+
return malformedKey('the export signing key is not a readable PEM private key', cause);
|
|
102
|
+
}
|
|
103
|
+
if (privateKey.asymmetricKeyType === 'ed25519')
|
|
104
|
+
return createEd25519ExportSigner({ privateKeyPem });
|
|
105
|
+
if (privateKey.asymmetricKeyType === 'ec')
|
|
106
|
+
return createEcdsaP256ExportSigner({ privateKeyPem });
|
|
107
|
+
return malformedKey(`the export signing key is ${describeKeyType(privateKey)}: exports are signed with an Ed25519 or an EC P-256 key`);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* An ECDSA signature from DER (`SEQUENCE { INTEGER r, INTEGER s }`, what
|
|
111
|
+
* Cloud KMS and AWS KMS answer) to IEEE P1363 `r‖s`, each half `size`
|
|
112
|
+
* bytes (32 for P-256): the form exports carry and Web Crypto verifies.
|
|
113
|
+
*/
|
|
114
|
+
export function ecdsaDerToP1363(der, size = 32) {
|
|
115
|
+
const bad = () => malformedKey('the ECDSA signature is not a DER SEQUENCE of two INTEGERs');
|
|
116
|
+
let at = 0;
|
|
117
|
+
const readLength = () => {
|
|
118
|
+
const first = der[at++];
|
|
119
|
+
if (first === undefined)
|
|
120
|
+
return undefined;
|
|
121
|
+
if (first < 0x80)
|
|
122
|
+
return first;
|
|
123
|
+
const bytes = first & 0x7f;
|
|
124
|
+
if (bytes === 0 || bytes > 2)
|
|
125
|
+
return undefined;
|
|
126
|
+
let length = 0;
|
|
127
|
+
for (let i = 0; i < bytes; i += 1) {
|
|
128
|
+
const b = der[at++];
|
|
129
|
+
if (b === undefined)
|
|
130
|
+
return undefined;
|
|
131
|
+
length = length * 256 + b;
|
|
132
|
+
}
|
|
133
|
+
return length;
|
|
134
|
+
};
|
|
135
|
+
const readInteger = () => {
|
|
136
|
+
if (der[at++] !== 0x02)
|
|
137
|
+
return undefined;
|
|
138
|
+
const length = readLength();
|
|
139
|
+
if (length === undefined || at + length > der.length)
|
|
140
|
+
return undefined;
|
|
141
|
+
let value = der.subarray(at, at + length);
|
|
142
|
+
at += length;
|
|
143
|
+
while (value.length > size && value[0] === 0)
|
|
144
|
+
value = value.subarray(1);
|
|
145
|
+
if (value.length > size)
|
|
146
|
+
return undefined;
|
|
147
|
+
const out = new Uint8Array(size);
|
|
148
|
+
out.set(value, size - value.length);
|
|
149
|
+
return out;
|
|
150
|
+
};
|
|
151
|
+
if (der[at++] !== 0x30 || readLength() === undefined)
|
|
152
|
+
return bad();
|
|
153
|
+
const r = readInteger();
|
|
154
|
+
const s = readInteger();
|
|
155
|
+
if (r === undefined || s === undefined || at !== der.length)
|
|
156
|
+
return bad();
|
|
157
|
+
const out = new Uint8Array(size * 2);
|
|
158
|
+
out.set(r, 0);
|
|
159
|
+
out.set(s, size);
|
|
160
|
+
return ok(out);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* An export signer over an Ed25519 private key held in memory: a PEM
|
|
164
|
+
* file's contents, a base64 env value decoded, a test's or a
|
|
165
|
+
* development server's generated key. `err` when the key isn't an
|
|
166
|
+
* Ed25519 PKCS#8 key.
|
|
167
|
+
*/
|
|
168
|
+
export function createEd25519ExportSigner(input) {
|
|
169
|
+
const seed = 'privateKeyPem' in input ? parsePrivateKeyPem(input.privateKeyPem) : ok(input.privateKey);
|
|
170
|
+
if (seed.kind === 'err')
|
|
171
|
+
return seed;
|
|
172
|
+
const publicKey = publicKeyFromSeed(seed.value);
|
|
173
|
+
if (publicKey.kind === 'err')
|
|
174
|
+
return publicKey;
|
|
175
|
+
const key = exportSigningKey(publicKey.value);
|
|
176
|
+
const privateKey = seed.value;
|
|
177
|
+
return ok({
|
|
178
|
+
activeKey: () => key,
|
|
179
|
+
listKeys: () => [key],
|
|
180
|
+
async sign(bytes, options) {
|
|
181
|
+
if (options?.keyId !== undefined && options.keyId !== key.keyId) {
|
|
182
|
+
return { kind: 'err', error: keyNotFound(options.keyId, [key]) };
|
|
183
|
+
}
|
|
184
|
+
return signedWith(key, privateKey, bytes);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The `ExportSigningBinding` over a `SigningKeyBinding`'s Ed25519 keys,
|
|
190
|
+
* under the ids that binding gives them; the first is the active one.
|
|
191
|
+
* `undefined` when it holds no Ed25519 key (nothing to sign with).
|
|
192
|
+
*/
|
|
193
|
+
export function exportSignerFromSigningKeyBinding(binding) {
|
|
194
|
+
const keys = binding
|
|
195
|
+
.listKeys()
|
|
196
|
+
.filter((d) => d.algorithm === 'ed25519')
|
|
197
|
+
.flatMap((d) => {
|
|
198
|
+
const pub = binding.getPublicKey(d.keyId);
|
|
199
|
+
return pub === null ? [] : [describeKey(d.keyId, pub)];
|
|
200
|
+
});
|
|
201
|
+
const [active] = keys;
|
|
202
|
+
if (active === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
return {
|
|
205
|
+
activeKey: () => active,
|
|
206
|
+
listKeys: () => keys,
|
|
207
|
+
async sign(bytes, options) {
|
|
208
|
+
const key = options?.keyId === undefined ? active : keys.find((k) => k.keyId === options.keyId);
|
|
209
|
+
const privateKey = key === undefined ? null : binding.getPrivateKey(key.keyId);
|
|
210
|
+
if (key === undefined || privateKey === null) {
|
|
211
|
+
return { kind: 'err', error: keyNotFound(options?.keyId ?? active.keyId, keys) };
|
|
212
|
+
}
|
|
213
|
+
return signedWith(key, privateKey, bytes);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function signedWith(key, privateKey, bytes) {
|
|
218
|
+
const signed = signEd25519(privateKey, bytes);
|
|
219
|
+
if (signed.kind === 'err') {
|
|
220
|
+
return {
|
|
221
|
+
kind: 'err',
|
|
222
|
+
error: { code: 'export-signing-failed', message: signed.error.message, cause: signed.error },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return ok({ key, signature: signed.value });
|
|
226
|
+
}
|
|
227
|
+
function keyNotFound(keyId, keys) {
|
|
228
|
+
return {
|
|
229
|
+
code: 'signing-key-not-found',
|
|
230
|
+
message: `This deployment doesn't sign exports with key "${keyId}". It signs with ${keys.map((k) => `"${k.keyId}"`).join(', ')}; leave signingKeyId out to use the active one.`,
|
|
231
|
+
keyId,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function describeKey(keyId, publicKey) {
|
|
235
|
+
return {
|
|
236
|
+
keyId,
|
|
237
|
+
algorithm: 'ed25519',
|
|
238
|
+
publicKeyPem: serializePublicKeyPem(publicKey),
|
|
239
|
+
fingerprint: `sha256:${sha256(publicKey).toString('hex')}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function publicKeyFromSeed(seed) {
|
|
243
|
+
if (seed.length !== ED25519_RAW_KEY_BYTES) {
|
|
244
|
+
return malformedKey(`Ed25519 private key must be ${ED25519_RAW_KEY_BYTES} raw bytes, got ${seed.length}`);
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const priv = createPrivateKey({
|
|
248
|
+
key: Buffer.from(wrapPrivateKeyPkcs8(seed)),
|
|
249
|
+
format: 'der',
|
|
250
|
+
type: 'pkcs8',
|
|
251
|
+
});
|
|
252
|
+
const spki = createPublicKey(priv).export({ format: 'der', type: 'spki' });
|
|
253
|
+
return unwrapPublicKeySpki(new Uint8Array(spki));
|
|
254
|
+
}
|
|
255
|
+
catch (cause) {
|
|
256
|
+
return malformedKey('Ed25519 private key rejected by underlying primitive', cause);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** A 65-byte uncompressed P-256 point as a PEM SPKI public key. */
|
|
260
|
+
function p256PublicKeyPem(point) {
|
|
261
|
+
const key = createPublicKey({
|
|
262
|
+
key: {
|
|
263
|
+
kty: 'EC',
|
|
264
|
+
crv: 'P-256',
|
|
265
|
+
x: Buffer.from(point.subarray(1, 33)).toString('base64url'),
|
|
266
|
+
y: Buffer.from(point.subarray(33, 65)).toString('base64url'),
|
|
267
|
+
},
|
|
268
|
+
format: 'jwk',
|
|
269
|
+
});
|
|
270
|
+
return key.export({ format: 'pem', type: 'spki' }).toString();
|
|
271
|
+
}
|
|
272
|
+
function describeKeyType(key) {
|
|
273
|
+
const type = key.asymmetricKeyType ?? 'an unknown kind of key';
|
|
274
|
+
const curve = key.asymmetricKeyDetails?.namedCurve;
|
|
275
|
+
return curve !== undefined ? `an ${type} key on ${curve}` : `an ${type} key`;
|
|
276
|
+
}
|
|
277
|
+
function sha256(bytes) {
|
|
278
|
+
return createHash('sha256').update(bytes).digest();
|
|
279
|
+
}
|
|
280
|
+
function ok(value) {
|
|
281
|
+
return { kind: 'ok', value };
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=export-signing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-signing.js","sourceRoot":"","sources":["../src/export-signing.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export { hmacSha256Sign, hmacSha256Verify } from './hmac.js';
|
|
|
4
4
|
export { DEFAULT_WEBHOOK_TOLERANCE_SECONDS, generateWebhookSecret, isStrongWebhookSecret, signWebhook, verifyWebhook, WEBHOOK_HEADERS, WEBHOOK_SECRET_MIN_BYTES, WEBHOOK_SECRET_PREFIX, webhookHeaders, } from './webhook.js';
|
|
5
5
|
export type { SignWebhookInput, VerifyWebhookFailure, VerifyWebhookInput, VerifyWebhookResult, WebhookRequestHeaders, } from './webhook.js';
|
|
6
6
|
export { createInMemorySigningKeyBinding } from './binding.js';
|
|
7
|
+
export { EXPORT_SIGNING_ALGORITHMS, createEcdsaP256ExportSigner, createEd25519ExportSigner, createExportSignerFromPem, ecdsaDerToP1363, exportSignerFromSigningKeyBinding, exportSigningKey, } from './export-signing.js';
|
|
8
|
+
export type { ExportSignature, ExportSigningAlgorithm, ExportSigningBinding, ExportSigningError, ExportSigningKey, } from './export-signing.js';
|
|
7
9
|
export type { SigningAlgorithm, SigningKeyBinding, SigningKeyDescriptor } from './binding.js';
|
|
8
10
|
export type { CryptoError, MalformedKeyError, MalformedSignatureError, UnsupportedFormatError, } from './errors.js';
|
|
9
11
|
export { ED25519_RAW_KEY_BYTES } from './encoding.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,EACzB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,WAAW,EACX,aAAa,GACd,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7D,OAAO,EACL,iCAAiC,EACjC,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,eAAe,EACf,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,GACf,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,+BAA+B,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,EACzB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,WAAW,EACX,aAAa,GACd,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7D,OAAO,EACL,iCAAiC,EACjC,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,eAAe,EACf,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,GACf,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,+BAA+B,EAAE,MAAM,cAAc,CAAC;AAE/D,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,yBAAyB,EACzB,yBAAyB,EACzB,eAAe,EACf,iCAAiC,EACjC,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,eAAe,EACf,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAE9F,YAAY,EACV,WAAW,EACX,iBAAiB,EACjB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,5 +4,6 @@ export { ED25519_SIGNATURE_BYTES, generateEd25519KeyPair, parsePrivateKeyBase64,
|
|
|
4
4
|
export { hmacSha256Sign, hmacSha256Verify } from './hmac.js';
|
|
5
5
|
export { DEFAULT_WEBHOOK_TOLERANCE_SECONDS, generateWebhookSecret, isStrongWebhookSecret, signWebhook, verifyWebhook, WEBHOOK_HEADERS, WEBHOOK_SECRET_MIN_BYTES, WEBHOOK_SECRET_PREFIX, webhookHeaders, } from './webhook.js';
|
|
6
6
|
export { createInMemorySigningKeyBinding } from './binding.js';
|
|
7
|
+
export { EXPORT_SIGNING_ALGORITHMS, createEcdsaP256ExportSigner, createEd25519ExportSigner, createExportSignerFromPem, ecdsaDerToP1363, exportSignerFromSigningKeyBinding, exportSigningKey, } from './export-signing.js';
|
|
7
8
|
export { ED25519_RAW_KEY_BYTES } from './encoding.js';
|
|
8
9
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAEjC,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,EACzB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,WAAW,EACX,aAAa,GACd,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7D,OAAO,EACL,iCAAiC,EACjC,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,eAAe,EACf,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,GACf,MAAM,cAAc,CAAC;AAStB,OAAO,EAAE,+BAA+B,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAEjC,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,yBAAyB,EACzB,sBAAsB,EACtB,wBAAwB,EACxB,qBAAqB,EACrB,WAAW,EACX,aAAa,GACd,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7D,OAAO,EACL,iCAAiC,EACjC,qBAAqB,EACrB,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,eAAe,EACf,wBAAwB,EACxB,qBAAqB,EACrB,cAAc,GACf,MAAM,cAAc,CAAC;AAStB,OAAO,EAAE,+BAA+B,EAAE,MAAM,cAAc,CAAC;AAE/D,OAAO,EACL,yBAAyB,EACzB,2BAA2B,EAC3B,yBAAyB,EACzB,yBAAyB,EACzB,eAAe,EACf,iCAAiC,EACjC,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAiB7B,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kindgi/crypto",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5-rc.0",
|
|
4
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
6
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@kindgi/types": "0.1.
|
|
31
|
+
"@kindgi/types": "0.1.5-rc.0"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
|
34
34
|
"node": ">=22.12.0"
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `ExportSigningBinding` — the key a deployment signs its exports with:
|
|
6
|
+
* audit bundles, provenance exports and compliance evidence. Async, so
|
|
7
|
+
* a KMS that never hands out its private key (Cloud KMS
|
|
8
|
+
* `asymmetricSign`) can implement it as well as a key file can.
|
|
9
|
+
*
|
|
10
|
+
* One key signs at a time (`activeKey`); `listKeys` is every key a
|
|
11
|
+
* verifier should trust, active first. A key's id is derived from its
|
|
12
|
+
* public key (`exportSigningKey`), so the same key has the same id
|
|
13
|
+
* across restarts and deployments, with nothing to configure.
|
|
14
|
+
*
|
|
15
|
+
* Two algorithms, chosen per key (`ExportSigningKey.algorithm`):
|
|
16
|
+
* `ed25519`, the default for a key in memory or a file, and
|
|
17
|
+
* `ecdsa-p256-sha256`, for a KMS that has no Ed25519. An ECDSA signature
|
|
18
|
+
* travels as IEEE P1363 `r‖s` (64 bytes), the form Web Crypto verifies;
|
|
19
|
+
* a KMS that answers DER converts with `ecdsaDerToP1363`.
|
|
20
|
+
*
|
|
21
|
+
* Not the same seam as `SigningKeyBinding` (raw key bytes, read
|
|
22
|
+
* synchronously, for public run tokens on the request path).
|
|
23
|
+
* `exportSignerFromSigningKeyBinding` adapts one to the other.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
type KeyObject,
|
|
28
|
+
createHash,
|
|
29
|
+
createPrivateKey,
|
|
30
|
+
createPublicKey,
|
|
31
|
+
sign as nodeSign,
|
|
32
|
+
} from 'node:crypto';
|
|
33
|
+
|
|
34
|
+
import type { Result } from '@kindgi/types';
|
|
35
|
+
|
|
36
|
+
import type { SigningKeyBinding } from './binding.js';
|
|
37
|
+
import { parsePrivateKeyPem, serializePublicKeyPem, signEd25519 } from './ed25519.js';
|
|
38
|
+
import { ED25519_RAW_KEY_BYTES, unwrapPublicKeySpki, wrapPrivateKeyPkcs8 } from './encoding.js';
|
|
39
|
+
import type { CryptoError } from './errors.js';
|
|
40
|
+
import { malformedKey } from './errors.js';
|
|
41
|
+
|
|
42
|
+
/** The algorithms an export can be signed with. */
|
|
43
|
+
export const EXPORT_SIGNING_ALGORITHMS = ['ed25519', 'ecdsa-p256-sha256'] as const;
|
|
44
|
+
export type ExportSigningAlgorithm = (typeof EXPORT_SIGNING_ALGORITHMS)[number];
|
|
45
|
+
|
|
46
|
+
/** A key exports are signed with, as a verifier sees it. No private material. */
|
|
47
|
+
export interface ExportSigningKey {
|
|
48
|
+
/**
|
|
49
|
+
* `ex_` and 16 base64url characters of the SHA-256 of the raw public
|
|
50
|
+
* key: Ed25519's 32 bytes, P-256's 65-byte uncompressed point.
|
|
51
|
+
*/
|
|
52
|
+
readonly keyId: string;
|
|
53
|
+
readonly algorithm: ExportSigningAlgorithm;
|
|
54
|
+
/** The public key, PEM SPKI. */
|
|
55
|
+
readonly publicKeyPem: string;
|
|
56
|
+
/** `sha256:` and the hex SHA-256 of the raw public key: to pin it, or compare by eye. */
|
|
57
|
+
readonly fingerprint: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type ExportSigningError =
|
|
61
|
+
| {
|
|
62
|
+
/** `keyId` was asked for and isn't a key this binding signs with. */
|
|
63
|
+
readonly code: 'signing-key-not-found';
|
|
64
|
+
readonly message: string;
|
|
65
|
+
readonly keyId: string;
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
/** The signer failed: a KMS that refused or didn't answer, a malformed key. */
|
|
69
|
+
readonly code: 'export-signing-failed';
|
|
70
|
+
readonly message: string;
|
|
71
|
+
readonly cause?: unknown;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export interface ExportSignature {
|
|
75
|
+
readonly key: ExportSigningKey;
|
|
76
|
+
readonly signature: Uint8Array;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ExportSigningBinding {
|
|
80
|
+
/** The key `sign` uses when no `keyId` is asked for. */
|
|
81
|
+
activeKey(): ExportSigningKey;
|
|
82
|
+
/** Every key a verifier should trust, the active one first. */
|
|
83
|
+
listKeys(): readonly ExportSigningKey[];
|
|
84
|
+
/**
|
|
85
|
+
* Sign `bytes` with the active key, or with `keyId` when given. A
|
|
86
|
+
* `keyId` this binding can't sign with is `signing-key-not-found`.
|
|
87
|
+
*/
|
|
88
|
+
sign(
|
|
89
|
+
bytes: Uint8Array,
|
|
90
|
+
options?: { readonly keyId?: string },
|
|
91
|
+
): Promise<Result<ExportSignature, ExportSigningError>>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The `ExportSigningKey` for a raw public key, under its derived id: a
|
|
96
|
+
* 32-byte Ed25519 key (the default), or a 65-byte uncompressed P-256 point
|
|
97
|
+
* for `ecdsa-p256-sha256`.
|
|
98
|
+
*/
|
|
99
|
+
export function exportSigningKey(
|
|
100
|
+
publicKey: Uint8Array,
|
|
101
|
+
algorithm: ExportSigningAlgorithm = 'ed25519',
|
|
102
|
+
): ExportSigningKey {
|
|
103
|
+
const keyId = `ex_${sha256(publicKey).toString('base64url').slice(0, 16)}`;
|
|
104
|
+
return algorithm === 'ed25519'
|
|
105
|
+
? describeKey(keyId, publicKey)
|
|
106
|
+
: {
|
|
107
|
+
keyId,
|
|
108
|
+
algorithm,
|
|
109
|
+
publicKeyPem: p256PublicKeyPem(publicKey),
|
|
110
|
+
fingerprint: `sha256:${sha256(publicKey).toString('hex')}`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* An export signer over an EC P-256 private key held in memory (PKCS#8 or
|
|
116
|
+
* SEC1 PEM): `ecdsa-p256-sha256`, its signatures as P1363 `r‖s`. `err`
|
|
117
|
+
* when the key isn't a P-256 private key.
|
|
118
|
+
*/
|
|
119
|
+
export function createEcdsaP256ExportSigner(input: {
|
|
120
|
+
readonly privateKeyPem: string;
|
|
121
|
+
}): Result<ExportSigningBinding, CryptoError> {
|
|
122
|
+
let privateKey: KeyObject;
|
|
123
|
+
try {
|
|
124
|
+
privateKey = createPrivateKey({ key: input.privateKeyPem, format: 'pem' });
|
|
125
|
+
} catch (cause) {
|
|
126
|
+
return malformedKey('the export signing key is not a readable PEM private key', cause);
|
|
127
|
+
}
|
|
128
|
+
const curve = privateKey.asymmetricKeyDetails?.namedCurve;
|
|
129
|
+
if (privateKey.asymmetricKeyType !== 'ec' || curve !== 'prime256v1') {
|
|
130
|
+
return malformedKey(
|
|
131
|
+
`the export signing key is ${describeKeyType(privateKey)}, not an EC P-256 key`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const jwk = createPublicKey(privateKey).export({ format: 'jwk' });
|
|
135
|
+
const point = Buffer.concat([
|
|
136
|
+
Buffer.from([0x04]),
|
|
137
|
+
Buffer.from(jwk.x as string, 'base64url'),
|
|
138
|
+
Buffer.from(jwk.y as string, 'base64url'),
|
|
139
|
+
]);
|
|
140
|
+
const key = exportSigningKey(new Uint8Array(point), 'ecdsa-p256-sha256');
|
|
141
|
+
return ok({
|
|
142
|
+
activeKey: () => key,
|
|
143
|
+
listKeys: () => [key],
|
|
144
|
+
async sign(bytes, options) {
|
|
145
|
+
if (options?.keyId !== undefined && options.keyId !== key.keyId) {
|
|
146
|
+
return { kind: 'err', error: keyNotFound(options.keyId, [key]) };
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const signature = nodeSign('sha256', bytes, { key: privateKey, dsaEncoding: 'ieee-p1363' });
|
|
150
|
+
return ok({ key, signature: new Uint8Array(signature) });
|
|
151
|
+
} catch (cause) {
|
|
152
|
+
return {
|
|
153
|
+
kind: 'err',
|
|
154
|
+
error: { code: 'export-signing-failed', message: 'ECDSA P-256 signing failed', cause },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The export signer for a PEM private key, its algorithm read from the
|
|
163
|
+
* key: Ed25519 → `ed25519`, EC P-256 → `ecdsa-p256-sha256`. Any other key
|
|
164
|
+
* is `err`, naming what it is.
|
|
165
|
+
*/
|
|
166
|
+
export function createExportSignerFromPem(
|
|
167
|
+
privateKeyPem: string,
|
|
168
|
+
): Result<ExportSigningBinding, CryptoError> {
|
|
169
|
+
let privateKey: KeyObject;
|
|
170
|
+
try {
|
|
171
|
+
privateKey = createPrivateKey({ key: privateKeyPem, format: 'pem' });
|
|
172
|
+
} catch (cause) {
|
|
173
|
+
return malformedKey('the export signing key is not a readable PEM private key', cause);
|
|
174
|
+
}
|
|
175
|
+
if (privateKey.asymmetricKeyType === 'ed25519')
|
|
176
|
+
return createEd25519ExportSigner({ privateKeyPem });
|
|
177
|
+
if (privateKey.asymmetricKeyType === 'ec') return createEcdsaP256ExportSigner({ privateKeyPem });
|
|
178
|
+
return malformedKey(
|
|
179
|
+
`the export signing key is ${describeKeyType(privateKey)}: exports are signed with an Ed25519 or an EC P-256 key`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* An ECDSA signature from DER (`SEQUENCE { INTEGER r, INTEGER s }`, what
|
|
185
|
+
* Cloud KMS and AWS KMS answer) to IEEE P1363 `r‖s`, each half `size`
|
|
186
|
+
* bytes (32 for P-256): the form exports carry and Web Crypto verifies.
|
|
187
|
+
*/
|
|
188
|
+
export function ecdsaDerToP1363(der: Uint8Array, size = 32): Result<Uint8Array, CryptoError> {
|
|
189
|
+
const bad = () => malformedKey('the ECDSA signature is not a DER SEQUENCE of two INTEGERs');
|
|
190
|
+
let at = 0;
|
|
191
|
+
const readLength = (): number | undefined => {
|
|
192
|
+
const first = der[at++];
|
|
193
|
+
if (first === undefined) return undefined;
|
|
194
|
+
if (first < 0x80) return first;
|
|
195
|
+
const bytes = first & 0x7f;
|
|
196
|
+
if (bytes === 0 || bytes > 2) return undefined;
|
|
197
|
+
let length = 0;
|
|
198
|
+
for (let i = 0; i < bytes; i += 1) {
|
|
199
|
+
const b = der[at++];
|
|
200
|
+
if (b === undefined) return undefined;
|
|
201
|
+
length = length * 256 + b;
|
|
202
|
+
}
|
|
203
|
+
return length;
|
|
204
|
+
};
|
|
205
|
+
const readInteger = (): Uint8Array | undefined => {
|
|
206
|
+
if (der[at++] !== 0x02) return undefined;
|
|
207
|
+
const length = readLength();
|
|
208
|
+
if (length === undefined || at + length > der.length) return undefined;
|
|
209
|
+
let value = der.subarray(at, at + length);
|
|
210
|
+
at += length;
|
|
211
|
+
while (value.length > size && value[0] === 0) value = value.subarray(1);
|
|
212
|
+
if (value.length > size) return undefined;
|
|
213
|
+
const out = new Uint8Array(size);
|
|
214
|
+
out.set(value, size - value.length);
|
|
215
|
+
return out;
|
|
216
|
+
};
|
|
217
|
+
if (der[at++] !== 0x30 || readLength() === undefined) return bad();
|
|
218
|
+
const r = readInteger();
|
|
219
|
+
const s = readInteger();
|
|
220
|
+
if (r === undefined || s === undefined || at !== der.length) return bad();
|
|
221
|
+
const out = new Uint8Array(size * 2);
|
|
222
|
+
out.set(r, 0);
|
|
223
|
+
out.set(s, size);
|
|
224
|
+
return ok(out);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* An export signer over an Ed25519 private key held in memory: a PEM
|
|
229
|
+
* file's contents, a base64 env value decoded, a test's or a
|
|
230
|
+
* development server's generated key. `err` when the key isn't an
|
|
231
|
+
* Ed25519 PKCS#8 key.
|
|
232
|
+
*/
|
|
233
|
+
export function createEd25519ExportSigner(
|
|
234
|
+
input: { readonly privateKeyPem: string } | { readonly privateKey: Uint8Array },
|
|
235
|
+
): Result<ExportSigningBinding, CryptoError> {
|
|
236
|
+
const seed =
|
|
237
|
+
'privateKeyPem' in input ? parsePrivateKeyPem(input.privateKeyPem) : ok(input.privateKey);
|
|
238
|
+
if (seed.kind === 'err') return seed;
|
|
239
|
+
const publicKey = publicKeyFromSeed(seed.value);
|
|
240
|
+
if (publicKey.kind === 'err') return publicKey;
|
|
241
|
+
const key = exportSigningKey(publicKey.value);
|
|
242
|
+
const privateKey = seed.value;
|
|
243
|
+
return ok({
|
|
244
|
+
activeKey: () => key,
|
|
245
|
+
listKeys: () => [key],
|
|
246
|
+
async sign(bytes, options) {
|
|
247
|
+
if (options?.keyId !== undefined && options.keyId !== key.keyId) {
|
|
248
|
+
return { kind: 'err', error: keyNotFound(options.keyId, [key]) };
|
|
249
|
+
}
|
|
250
|
+
return signedWith(key, privateKey, bytes);
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The `ExportSigningBinding` over a `SigningKeyBinding`'s Ed25519 keys,
|
|
257
|
+
* under the ids that binding gives them; the first is the active one.
|
|
258
|
+
* `undefined` when it holds no Ed25519 key (nothing to sign with).
|
|
259
|
+
*/
|
|
260
|
+
export function exportSignerFromSigningKeyBinding(
|
|
261
|
+
binding: SigningKeyBinding,
|
|
262
|
+
): ExportSigningBinding | undefined {
|
|
263
|
+
const keys = binding
|
|
264
|
+
.listKeys()
|
|
265
|
+
.filter((d) => d.algorithm === 'ed25519')
|
|
266
|
+
.flatMap((d) => {
|
|
267
|
+
const pub = binding.getPublicKey(d.keyId);
|
|
268
|
+
return pub === null ? [] : [describeKey(d.keyId as unknown as string, pub)];
|
|
269
|
+
});
|
|
270
|
+
const [active] = keys;
|
|
271
|
+
if (active === undefined) return undefined;
|
|
272
|
+
return {
|
|
273
|
+
activeKey: () => active,
|
|
274
|
+
listKeys: () => keys,
|
|
275
|
+
async sign(bytes, options) {
|
|
276
|
+
const key =
|
|
277
|
+
options?.keyId === undefined ? active : keys.find((k) => k.keyId === options.keyId);
|
|
278
|
+
const privateKey = key === undefined ? null : binding.getPrivateKey(key.keyId as never);
|
|
279
|
+
if (key === undefined || privateKey === null) {
|
|
280
|
+
return { kind: 'err', error: keyNotFound(options?.keyId ?? active.keyId, keys) };
|
|
281
|
+
}
|
|
282
|
+
return signedWith(key, privateKey, bytes);
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function signedWith(
|
|
288
|
+
key: ExportSigningKey,
|
|
289
|
+
privateKey: Uint8Array,
|
|
290
|
+
bytes: Uint8Array,
|
|
291
|
+
): Result<ExportSignature, ExportSigningError> {
|
|
292
|
+
const signed = signEd25519(privateKey, bytes);
|
|
293
|
+
if (signed.kind === 'err') {
|
|
294
|
+
return {
|
|
295
|
+
kind: 'err',
|
|
296
|
+
error: { code: 'export-signing-failed', message: signed.error.message, cause: signed.error },
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return ok({ key, signature: signed.value });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function keyNotFound(keyId: string, keys: readonly ExportSigningKey[]): ExportSigningError {
|
|
303
|
+
return {
|
|
304
|
+
code: 'signing-key-not-found',
|
|
305
|
+
message: `This deployment doesn't sign exports with key "${keyId}". It signs with ${keys.map((k) => `"${k.keyId}"`).join(', ')}; leave signingKeyId out to use the active one.`,
|
|
306
|
+
keyId,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function describeKey(keyId: string, publicKey: Uint8Array): ExportSigningKey {
|
|
311
|
+
return {
|
|
312
|
+
keyId,
|
|
313
|
+
algorithm: 'ed25519',
|
|
314
|
+
publicKeyPem: serializePublicKeyPem(publicKey),
|
|
315
|
+
fingerprint: `sha256:${sha256(publicKey).toString('hex')}`,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function publicKeyFromSeed(seed: Uint8Array): Result<Uint8Array, CryptoError> {
|
|
320
|
+
if (seed.length !== ED25519_RAW_KEY_BYTES) {
|
|
321
|
+
return malformedKey(
|
|
322
|
+
`Ed25519 private key must be ${ED25519_RAW_KEY_BYTES} raw bytes, got ${seed.length}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const priv = createPrivateKey({
|
|
327
|
+
key: Buffer.from(wrapPrivateKeyPkcs8(seed)),
|
|
328
|
+
format: 'der',
|
|
329
|
+
type: 'pkcs8',
|
|
330
|
+
});
|
|
331
|
+
const spki = createPublicKey(priv).export({ format: 'der', type: 'spki' });
|
|
332
|
+
return unwrapPublicKeySpki(new Uint8Array(spki));
|
|
333
|
+
} catch (cause) {
|
|
334
|
+
return malformedKey('Ed25519 private key rejected by underlying primitive', cause);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** A 65-byte uncompressed P-256 point as a PEM SPKI public key. */
|
|
339
|
+
function p256PublicKeyPem(point: Uint8Array): string {
|
|
340
|
+
const key = createPublicKey({
|
|
341
|
+
key: {
|
|
342
|
+
kty: 'EC',
|
|
343
|
+
crv: 'P-256',
|
|
344
|
+
x: Buffer.from(point.subarray(1, 33)).toString('base64url'),
|
|
345
|
+
y: Buffer.from(point.subarray(33, 65)).toString('base64url'),
|
|
346
|
+
},
|
|
347
|
+
format: 'jwk',
|
|
348
|
+
});
|
|
349
|
+
return key.export({ format: 'pem', type: 'spki' }).toString();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function describeKeyType(key: KeyObject): string {
|
|
353
|
+
const type = key.asymmetricKeyType ?? 'an unknown kind of key';
|
|
354
|
+
const curve = key.asymmetricKeyDetails?.namedCurve;
|
|
355
|
+
return curve !== undefined ? `an ${type} key on ${curve}` : `an ${type} key`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function sha256(bytes: Uint8Array): Buffer {
|
|
359
|
+
return createHash('sha256').update(bytes).digest();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function ok<T>(value: T): { readonly kind: 'ok'; readonly value: T } {
|
|
363
|
+
return { kind: 'ok', value };
|
|
364
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,23 @@ export type {
|
|
|
39
39
|
} from './webhook.js';
|
|
40
40
|
|
|
41
41
|
export { createInMemorySigningKeyBinding } from './binding.js';
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
EXPORT_SIGNING_ALGORITHMS,
|
|
45
|
+
createEcdsaP256ExportSigner,
|
|
46
|
+
createEd25519ExportSigner,
|
|
47
|
+
createExportSignerFromPem,
|
|
48
|
+
ecdsaDerToP1363,
|
|
49
|
+
exportSignerFromSigningKeyBinding,
|
|
50
|
+
exportSigningKey,
|
|
51
|
+
} from './export-signing.js';
|
|
52
|
+
export type {
|
|
53
|
+
ExportSignature,
|
|
54
|
+
ExportSigningAlgorithm,
|
|
55
|
+
ExportSigningBinding,
|
|
56
|
+
ExportSigningError,
|
|
57
|
+
ExportSigningKey,
|
|
58
|
+
} from './export-signing.js';
|
|
42
59
|
export type { SigningAlgorithm, SigningKeyBinding, SigningKeyDescriptor } from './binding.js';
|
|
43
60
|
|
|
44
61
|
export type {
|