gdc-common-utils-ts 2.0.10 → 2.0.11

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.
@@ -0,0 +1,60 @@
1
+ import type { ClassicPublicJwk } from '../interfaces/Cryptography.types';
2
+ /**
3
+ * Legacy JOSE signature algorithms still commonly used by BFF, controller, and
4
+ * test fixtures before a runtime switches to post-quantum signers.
5
+ *
6
+ * Notes:
7
+ * - `ES384` maps to the NIST P-384 curve
8
+ * - `ES256K` maps to the secp256k1 curve often used by wallet-oriented flows
9
+ *
10
+ * Post-quantum deterministic key generation remains available through
11
+ * `ICryptography.generateKeyPairMlDsa(...)` and
12
+ * `ICryptography.generateKeyPairMlKem(...)`.
13
+ */
14
+ export type DeterministicEcJwkAlgorithm = 'ES256K' | 'ES384';
15
+ /**
16
+ * Deterministic EC signing material derived from a stable textual seed.
17
+ *
18
+ * This helper exists for:
19
+ * - reproducible tests
20
+ * - local/live demos
21
+ * - external-signing examples where auditors must regenerate the same public
22
+ * JWK without importing PEM files or depending on random key generation
23
+ *
24
+ * Important:
25
+ * - the derived private key is deterministic for the same
26
+ * `seed + purpose + alg`
27
+ * - ECDSA signatures generated later may still differ across crypto providers,
28
+ * so callers should normally assert signature verification rather than the
29
+ * full compact token bytes
30
+ */
31
+ export type DeterministicEcJwkPair = {
32
+ publicJwk: ClassicPublicJwk & {
33
+ use: 'sig';
34
+ kid: string;
35
+ };
36
+ privateJwk: ClassicPublicJwk & {
37
+ use: 'sig';
38
+ kid: string;
39
+ d: string;
40
+ };
41
+ alg: DeterministicEcJwkAlgorithm;
42
+ seed: string;
43
+ purpose: string;
44
+ };
45
+ /**
46
+ * Derives one reproducible EC JWK pair from a stable seed string.
47
+ *
48
+ * @param input.seed Stable textual seed chosen by the caller.
49
+ * @param input.purpose Logical namespace such as `virtual-bff`, `controller`,
50
+ * or `demo-host`.
51
+ * @param input.alg JOSE algorithm/curve family. Defaults to `ES384`.
52
+ *
53
+ * @see RFC 7517 JSON Web Key (JWK)
54
+ * @see RFC 7518 JSON Web Algorithms (JWA)
55
+ */
56
+ export declare function deriveDeterministicEcJwkPair(input: {
57
+ seed: string;
58
+ purpose: string;
59
+ alg?: DeterministicEcJwkAlgorithm;
60
+ }): DeterministicEcJwkPair;
@@ -0,0 +1,97 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { createECDH, createHash } from 'crypto';
3
+ const EC_CURVE_BY_ALG = {
4
+ ES256K: {
5
+ alg: 'ES256K',
6
+ nodeCurve: 'secp256k1',
7
+ jwkCrv: 'secp256k1',
8
+ coordinateBytes: 32,
9
+ },
10
+ ES384: {
11
+ alg: 'ES384',
12
+ nodeCurve: 'secp384r1',
13
+ jwkCrv: 'P-384',
14
+ coordinateBytes: 48,
15
+ },
16
+ };
17
+ function sha256Base64Url(input) {
18
+ return createHash('sha256').update(input, 'utf8').digest('base64url');
19
+ }
20
+ /**
21
+ * Expands an arbitrary text seed into enough bytes for one EC private scalar.
22
+ *
23
+ * Each block includes both a deterministic `attempt` and `counter`, so callers
24
+ * can keep retrying reproducible candidates until the target curve accepts one.
25
+ */
26
+ function expandSeedBytes(seed, purpose, size, attempt) {
27
+ const chunks = [];
28
+ let counter = 0;
29
+ while (Buffer.concat(chunks).length < size) {
30
+ chunks.push(createHash('sha512')
31
+ .update(seed, 'utf8')
32
+ .update(':', 'utf8')
33
+ .update(purpose, 'utf8')
34
+ .update(':', 'utf8')
35
+ .update(String(attempt), 'utf8')
36
+ .update(':', 'utf8')
37
+ .update(String(counter), 'utf8')
38
+ .digest());
39
+ counter += 1;
40
+ }
41
+ return Buffer.concat(chunks).subarray(0, size);
42
+ }
43
+ /**
44
+ * Derives one reproducible EC JWK pair from a stable seed string.
45
+ *
46
+ * @param input.seed Stable textual seed chosen by the caller.
47
+ * @param input.purpose Logical namespace such as `virtual-bff`, `controller`,
48
+ * or `demo-host`.
49
+ * @param input.alg JOSE algorithm/curve family. Defaults to `ES384`.
50
+ *
51
+ * @see RFC 7517 JSON Web Key (JWK)
52
+ * @see RFC 7518 JSON Web Algorithms (JWA)
53
+ */
54
+ export function deriveDeterministicEcJwkPair(input) {
55
+ const alg = input.alg ?? 'ES384';
56
+ const curve = EC_CURVE_BY_ALG[alg];
57
+ for (let attempt = 0; attempt < 256; attempt += 1) {
58
+ const candidate = expandSeedBytes(input.seed, input.purpose, curve.coordinateBytes, attempt);
59
+ try {
60
+ const ecdh = createECDH(curve.nodeCurve);
61
+ ecdh.setPrivateKey(candidate);
62
+ const privateKey = ecdh.getPrivateKey();
63
+ const publicKey = ecdh.getPublicKey(undefined, 'uncompressed');
64
+ const x = publicKey.subarray(1, 1 + curve.coordinateBytes);
65
+ const y = publicKey.subarray(1 + curve.coordinateBytes, 1 + (curve.coordinateBytes * 2));
66
+ const kid = `${input.purpose}-${alg.toLowerCase()}-${sha256Base64Url(`${input.seed}:${input.purpose}:${alg}`).slice(0, 16)}`;
67
+ return {
68
+ alg,
69
+ seed: input.seed,
70
+ purpose: input.purpose,
71
+ publicJwk: {
72
+ kty: 'EC',
73
+ crv: curve.jwkCrv,
74
+ alg,
75
+ use: 'sig',
76
+ kid,
77
+ x: x.toString('base64url'),
78
+ y: y.toString('base64url'),
79
+ },
80
+ privateJwk: {
81
+ kty: 'EC',
82
+ crv: curve.jwkCrv,
83
+ alg,
84
+ use: 'sig',
85
+ kid,
86
+ x: x.toString('base64url'),
87
+ y: y.toString('base64url'),
88
+ d: privateKey.toString('base64url'),
89
+ },
90
+ };
91
+ }
92
+ catch {
93
+ // Retry the next deterministic candidate until the curve accepts it.
94
+ }
95
+ }
96
+ throw new Error(`Could not derive a valid ${alg} key from deterministic seed '${input.seed}'.`);
97
+ }
@@ -19,6 +19,7 @@ export * from './did-resolution';
19
19
  export * from './dataspace-discovery';
20
20
  export * from './dataspace-discovery-defaults';
21
21
  export * from './dataspace-protocol';
22
+ export * from './deterministic-jwk';
22
23
  export * from './employee';
23
24
  export * from './evidence-blockchain-references';
24
25
  export * from './didcomm';
@@ -63,6 +64,7 @@ export * from './invoice-bundle';
63
64
  export * from './ips-bundle-claims';
64
65
  export * from './interoperable-resource-operation';
65
66
  export * from './jwt';
67
+ export * from './jwt-signer';
66
68
  export * from './jwk-thumbprint';
67
69
  export * from './legal-organization-onboarding';
68
70
  export * from './legal-organization-verification-transaction';
@@ -19,6 +19,7 @@ export * from './did-resolution.js';
19
19
  export * from './dataspace-discovery.js';
20
20
  export * from './dataspace-discovery-defaults.js';
21
21
  export * from './dataspace-protocol.js';
22
+ export * from './deterministic-jwk.js';
22
23
  export * from './employee.js';
23
24
  export * from './evidence-blockchain-references.js';
24
25
  export * from './didcomm.js';
@@ -63,6 +64,7 @@ export * from './invoice-bundle.js';
63
64
  export * from './ips-bundle-claims.js';
64
65
  export * from './interoperable-resource-operation.js';
65
66
  export * from './jwt.js';
67
+ export * from './jwt-signer.js';
66
68
  export * from './jwk-thumbprint.js';
67
69
  export * from './legal-organization-onboarding.js';
68
70
  export * from './legal-organization-verification-transaction.js';
@@ -0,0 +1,80 @@
1
+ import { type JoseSignatureAlgorithm } from '../constants/cryptography';
2
+ import type { ICryptography } from '../interfaces/ICryptography';
3
+ import type { PublicJwk, ClassicPublicJwk } from '../interfaces/Cryptography.types';
4
+ export type JwtSignerMode = 'deterministic' | 'random';
5
+ export type JwtSignerMaterial = {
6
+ kind: 'classic-ec';
7
+ publicJwk: ClassicPublicJwk & {
8
+ kid: string;
9
+ use: 'sig';
10
+ };
11
+ privateJwk: ClassicPublicJwk & {
12
+ kid: string;
13
+ use: 'sig';
14
+ d: string;
15
+ };
16
+ } | {
17
+ kind: 'ml-dsa';
18
+ publicJwk: PublicJwk & {
19
+ kid: string;
20
+ use?: string;
21
+ };
22
+ secretKeyBytes: Uint8Array;
23
+ };
24
+ export type PreparedJwtSigningInput = {
25
+ protectedHeader: Record<string, unknown>;
26
+ encodedHeader: string;
27
+ encodedPayload: string;
28
+ signingInput: string;
29
+ signingBytes: Uint8Array;
30
+ };
31
+ export interface JwtSigner {
32
+ getMode(): JwtSignerMode;
33
+ getAlgorithm(): JoseSignatureAlgorithm;
34
+ getPurpose(): string;
35
+ getPublicJwk(): PublicJwk;
36
+ getPrivateMaterial(): JWKLikePrivateMaterial;
37
+ getKid(): string;
38
+ getThumbprint(): string;
39
+ getThumbprintUri(): string;
40
+ prepareJwt(input: {
41
+ payload: Record<string, unknown>;
42
+ header?: Record<string, unknown>;
43
+ typ?: string;
44
+ kid?: string;
45
+ }): PreparedJwtSigningInput;
46
+ buildCompact(signatureBase64Url: string, prepared: Pick<PreparedJwtSigningInput, 'encodedHeader' | 'encodedPayload'>): string;
47
+ buildUnsignedJwt(payload: Record<string, unknown>, options?: Readonly<{
48
+ nowSeconds?: number;
49
+ ttlSeconds?: number;
50
+ nonce?: string;
51
+ }>): string;
52
+ }
53
+ export type JWKLikePrivateMaterial = (ClassicPublicJwk & {
54
+ kid: string;
55
+ use: 'sig';
56
+ d: string;
57
+ }) | Uint8Array;
58
+ export type CreateJwtSignerOptions = {
59
+ alg: JoseSignatureAlgorithm;
60
+ purpose: string;
61
+ seed?: string | Uint8Array;
62
+ cryptography?: Pick<ICryptography, 'generateKeyPairMlDsa'>;
63
+ };
64
+ /**
65
+ * Creates one high-level JWT signer façade for docs, tests, BFF helpers, and
66
+ * controller/wallet flows.
67
+ *
68
+ * Contract:
69
+ * - if `seed` is provided, the signer runs in deterministic mode
70
+ * - if `seed` is omitted, the signer generates fresh random material
71
+ *
72
+ * This helper intentionally exposes a small integrator-facing surface:
73
+ * - get the public JWK / kid / thumbprint
74
+ * - prepare the exact `header.payload` signing input
75
+ * - assemble the final compact JWT afterwards
76
+ *
77
+ * It does not hide the real external-signing step. That step still belongs to
78
+ * a KMS, HSM, wallet, or trusted issuer.
79
+ */
80
+ export declare function createJwtSigner(options: CreateJwtSignerOptions): Promise<JwtSigner>;
@@ -0,0 +1,134 @@
1
+ // Copyright 2026 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ import { randomBytes } from 'crypto';
3
+ import { ClassicalJoseSignatureAlgorithms, JwkKeyUses } from '../constants/cryptography.js';
4
+ import { Content } from './content.js';
5
+ import { buildJwtCompact, buildUnsignedJwt, prepareJwtBytesForSignature, prepareJwtForSignature, } from './jwt.js';
6
+ import { computeRfc7638JwkThumbprint, toJwkThumbprintSha256Urn, } from './jwk-thumbprint.js';
7
+ import { deriveDeterministicEcJwkPair, } from './deterministic-jwk.js';
8
+ function isClassicEcAlg(alg) {
9
+ return alg === ClassicalJoseSignatureAlgorithms.Es384 || alg === ClassicalJoseSignatureAlgorithms.Es256K;
10
+ }
11
+ function resolveMode(seed) {
12
+ return seed === undefined ? 'random' : 'deterministic';
13
+ }
14
+ function resolveSeedString(seed) {
15
+ if (typeof seed === 'string')
16
+ return seed;
17
+ if (seed instanceof Uint8Array)
18
+ return Content.bytesToRawBase64UrlSafe(seed);
19
+ return randomBytes(32).toString('base64url');
20
+ }
21
+ /**
22
+ * Creates one high-level JWT signer façade for docs, tests, BFF helpers, and
23
+ * controller/wallet flows.
24
+ *
25
+ * Contract:
26
+ * - if `seed` is provided, the signer runs in deterministic mode
27
+ * - if `seed` is omitted, the signer generates fresh random material
28
+ *
29
+ * This helper intentionally exposes a small integrator-facing surface:
30
+ * - get the public JWK / kid / thumbprint
31
+ * - prepare the exact `header.payload` signing input
32
+ * - assemble the final compact JWT afterwards
33
+ *
34
+ * It does not hide the real external-signing step. That step still belongs to
35
+ * a KMS, HSM, wallet, or trusted issuer.
36
+ */
37
+ export async function createJwtSigner(options) {
38
+ const mode = resolveMode(options.seed);
39
+ const seedText = resolveSeedString(options.seed);
40
+ const alg = options.alg;
41
+ let material;
42
+ if (isClassicEcAlg(alg)) {
43
+ const pair = deriveDeterministicEcJwkPair({
44
+ seed: seedText,
45
+ purpose: options.purpose,
46
+ alg,
47
+ });
48
+ material = {
49
+ kind: 'classic-ec',
50
+ publicJwk: pair.publicJwk,
51
+ privateJwk: pair.privateJwk,
52
+ };
53
+ }
54
+ else {
55
+ if (!options.cryptography) {
56
+ throw new Error(`createJwtSigner requires options.cryptography for post-quantum algorithm '${alg}'.`);
57
+ }
58
+ const seedBytes = options.seed === undefined
59
+ ? undefined
60
+ : options.seed instanceof Uint8Array
61
+ ? options.seed
62
+ : Content.stringToBytesUTF8(options.seed);
63
+ const generated = await options.cryptography.generateKeyPairMlDsa(seedBytes, alg);
64
+ material = {
65
+ kind: 'ml-dsa',
66
+ publicJwk: {
67
+ ...generated.publicJWKey,
68
+ use: JwkKeyUses.Signature,
69
+ },
70
+ secretKeyBytes: generated.secretKeyBytes,
71
+ };
72
+ }
73
+ const sourcePublicJwk = material.publicJwk;
74
+ const thumbprintJwk = sourcePublicJwk;
75
+ const thumbprint = computeRfc7638JwkThumbprint(thumbprintJwk);
76
+ const thumbprintUri = toJwkThumbprintSha256Urn(thumbprintJwk);
77
+ const keyId = thumbprintUri;
78
+ const publicJwk = {
79
+ ...sourcePublicJwk,
80
+ kid: keyId,
81
+ };
82
+ const privateMaterial = material.kind === 'classic-ec'
83
+ ? {
84
+ ...material.privateJwk,
85
+ kid: keyId,
86
+ }
87
+ : material.secretKeyBytes;
88
+ return {
89
+ getMode() {
90
+ return mode;
91
+ },
92
+ getAlgorithm() {
93
+ return alg;
94
+ },
95
+ getPurpose() {
96
+ return options.purpose;
97
+ },
98
+ getPublicJwk() {
99
+ return publicJwk;
100
+ },
101
+ getPrivateMaterial() {
102
+ return privateMaterial;
103
+ },
104
+ getKid() {
105
+ return keyId;
106
+ },
107
+ getThumbprint() {
108
+ return thumbprint;
109
+ },
110
+ getThumbprintUri() {
111
+ return thumbprintUri;
112
+ },
113
+ prepareJwt(input) {
114
+ const protectedHeader = {
115
+ alg,
116
+ typ: input.typ ?? 'JWT',
117
+ ...(input.header || {}),
118
+ kid: keyId,
119
+ };
120
+ const prepared = prepareJwtForSignature(protectedHeader, input.payload);
121
+ return {
122
+ protectedHeader,
123
+ ...prepared,
124
+ signingBytes: prepareJwtBytesForSignature(protectedHeader, input.payload),
125
+ };
126
+ },
127
+ buildCompact(signatureBase64Url, prepared) {
128
+ return buildJwtCompact(prepared.encodedHeader, prepared.encodedPayload, signatureBase64Url);
129
+ },
130
+ buildUnsignedJwt(payload, optionsForUnsigned) {
131
+ return buildUnsignedJwt(payload, optionsForUnsigned);
132
+ },
133
+ };
134
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.0.10",
3
+ "version": "2.0.11",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },