@optimystic/quereus-plugin-crypto 0.13.5 → 0.16.2

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/src/sd.ts ADDED
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Salted-leaf SET COMMITMENT for per-attribute selective disclosure.
3
+ *
4
+ * An authority commits to a whole set of attributes as a single root value (which
5
+ * it signs / persists), then later reveals only a chosen *subset* to a recipient —
6
+ * with a proof that the revealed values are genuinely the committed ones — without
7
+ * leaking the values of the withheld attributes. A flat `digest(whole set)` cannot
8
+ * do this (verifying one field needs the whole pre-image, so it is all-or-nothing);
9
+ * this construction supports *partial opening*.
10
+ *
11
+ * ## Construction (flat salted-leaf set commitment, NOT a Merkle tree)
12
+ *
13
+ * Each disclosable attribute is a salted leaf, and the commitment (root) is the
14
+ * digest of all leaf digests in canonical order:
15
+ *
16
+ * ```
17
+ * leafDigest = digest([SD_LEAF_DOMAIN_V1, name, value, salt]) // raw digest bytes
18
+ * root = digest([SD_SET_DOMAIN_V1, sortedLeaf_0, sortedLeaf_1, ...])
19
+ * ```
20
+ *
21
+ * Both layers compose on the existing canonical {@link encodeFields} framing
22
+ * (injective, type-tagged, length-prefixed, replicable) — the same layering the CID
23
+ * work uses — so a *generic* salted-set primitive is simultaneously reusable and
24
+ * fully DB-enforceable. This is the same shape the IETF SD-JWT standard settled on
25
+ * (flat salted hashes, not a tree); we are NOT wire-compatible with SD-JWT (we reuse
26
+ * Optimystic's own `encodeFields` framing for cross-peer replicability) — SD-JWT is
27
+ * cited only as conceptual precedent that the smaller construction is the right one.
28
+ *
29
+ * Voter selective-disclosure field sets are small (a handful to a few dozen fields),
30
+ * so a tree's only advantage — O(log n) proof size — is marginal, while a tree drags
31
+ * in real footguns we would have to hand-roll and pin (arity, odd-node handling /
32
+ * the CVE-2012-2459 duplicate-leaf forgery class, leaf-vs-internal domain separation,
33
+ * and a separate audit-path proof format). A flat construction avoids all of them.
34
+ *
35
+ * ## Why these specific choices
36
+ *
37
+ * - **`name` is hashed into the leaf** so a disclosed `(value, salt)` proof cannot be
38
+ * replayed against a different attribute slot (e.g. presenting an `over18=true`
39
+ * proof as the `citizen` field). The binding is free given `encodeFields` framing.
40
+ * - **`salt` is per-leaf and mandatory** — low-entropy attributes (DOB, booleans, ZIP)
41
+ * are brute-forceable from a bare hash, and independent salts also defeat cross-
42
+ * registrant equality correlation. Salts come from `random_bytes` (≥128 bits).
43
+ * - **Canonical order is by raw leaf-digest bytes (lexicographic), and this is FORCED,
44
+ * not a preference.** In a disclosure the verifier learns the *names* of only the
45
+ * disclosed leaves; the withheld leaves arrive as opaque digests with no name. So the
46
+ * verifier can re-derive the root only if the ordering key is something it holds for
47
+ * *every* leaf — the leaf digest itself. Sorting by name would be unverifiable for
48
+ * hidden leaves. Do NOT "tidy" this into a name sort.
49
+ * - Sort is over **raw digest bytes**, never over encoded strings — an encoding-
50
+ * dependent ordering would break cross-peer agreement. Output encoding applies only
51
+ * to the final root.
52
+ *
53
+ * Because leaf and root reuse `encodeFields`, a future `DIGEST_FORMAT_V1` bump changes
54
+ * `setCommit` output too; this coupling is intentional (one canonical framing).
55
+ */
56
+
57
+ import { fromString as uint8ArrayFromString, toString as uint8ArrayToString } from 'uint8arrays';
58
+ import {
59
+ encodeFields,
60
+ resolveHasher,
61
+ resolveOutputEncoder,
62
+ type DigestField,
63
+ type DigestHasher,
64
+ type OutputEncoder,
65
+ } from './crypto.js';
66
+
67
+ /**
68
+ * Fixed domain-separation constants — the leading string field of each layer's
69
+ * {@link encodeFields} tuple. They are pinned EXACTLY like `DIGEST_FORMAT_V1`:
70
+ *
71
+ * - the two strings MUST be distinct, so a leaf hash can never equal a root hash;
72
+ * - neither may change without a deliberate, breaking version bump — which would
73
+ * change every committed root and every signature taken over it.
74
+ *
75
+ * Do not "tidy" or shorten these.
76
+ */
77
+ const SD_LEAF_DOMAIN_V1 = 'optimystic/sd-leaf/v1';
78
+ const SD_SET_DOMAIN_V1 = 'optimystic/sd-set/v1';
79
+
80
+ /** Hidden leaf digests travel as base64url text — the plugin's canonical text encoding. */
81
+ const HIDDEN_ENCODING = 'base64url';
82
+
83
+ /** One disclosable attribute. `value` spans the SQL value space ({@link DigestField}). */
84
+ export interface SaltedLeaf {
85
+ readonly name: string;
86
+ readonly value: DigestField;
87
+ /** base64url text (e.g. from `random_bytes`) or raw bytes. Mandatory, non-empty. */
88
+ readonly salt: string | Uint8Array;
89
+ }
90
+
91
+ /** A disclosure payload sent to a recipient. */
92
+ export interface SetDisclosure {
93
+ /** The opened `(name, value, salt)` triples. */
94
+ readonly disclosed: readonly SaltedLeaf[];
95
+ /** Opaque leaf digests (base64url) of the withheld leaves — no name, no value, no salt. */
96
+ readonly hidden: readonly string[];
97
+ }
98
+
99
+ // --- internal helpers --- //
100
+
101
+ /** Lexicographic compare of two byte arrays (the canonical leaf ordering key). */
102
+ function compareBytes(a: Uint8Array, b: Uint8Array): number {
103
+ const len = Math.min(a.length, b.length);
104
+ for (let i = 0; i < len; i++) {
105
+ const d = a[i]! - b[i]!;
106
+ if (d !== 0) return d;
107
+ }
108
+ return a.length - b.length;
109
+ }
110
+
111
+ /** Constant-shape byte equality (length first, then content). */
112
+ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
113
+ if (a.length !== b.length) return false;
114
+ for (let i = 0; i < a.length; i++) {
115
+ if (a[i] !== b[i]) return false;
116
+ }
117
+ return true;
118
+ }
119
+
120
+ /**
121
+ * Normalize a leaf's salt to raw bytes — a base64url string (the form `random_bytes`
122
+ * returns) decodes to bytes, raw bytes pass through — so the two representations of the
123
+ * same salt commit identically. THROWS on a missing or empty salt (unsalted leaves are
124
+ * brute-forceable, an invalid state we make impossible).
125
+ */
126
+ function requireSaltBytes(leaf: SaltedLeaf): Uint8Array {
127
+ const { salt } = leaf;
128
+ if (salt == null) {
129
+ throw new Error(`set commitment: leaf '${leaf.name}' is missing a salt (an unsalted leaf is brute-forceable)`);
130
+ }
131
+ const bytes = salt instanceof Uint8Array ? salt : uint8ArrayFromString(salt, HIDDEN_ENCODING);
132
+ if (bytes.length === 0) {
133
+ throw new Error(`set commitment: leaf '${leaf.name}' has an empty salt (an unsalted leaf is brute-forceable)`);
134
+ }
135
+ return bytes;
136
+ }
137
+
138
+ /**
139
+ * THROW on a duplicate `name`. Two leaves with the same name would let a holder
140
+ * selectively present whichever value suits them; the authority side (which holds all
141
+ * names) is the only place uniqueness can be enforced — the verifier never sees the
142
+ * hidden names — so the primitive must fail-fast.
143
+ */
144
+ function assertUniqueNames(leaves: readonly SaltedLeaf[]): void {
145
+ const seen = new Set<string>();
146
+ for (const leaf of leaves) {
147
+ if (seen.has(leaf.name)) {
148
+ throw new Error(`set commitment: duplicate leaf name '${leaf.name}'`);
149
+ }
150
+ seen.add(leaf.name);
151
+ }
152
+ }
153
+
154
+ // --- public API --- //
155
+
156
+ /**
157
+ * Raw leaf digest bytes for one salted leaf: `digest([SD_LEAF_DOMAIN_V1, name,
158
+ * value, salt])`. Domain-separated (can never equal a root) and name-bound (a
159
+ * `(value, salt)` proof cannot be replayed under another attribute name). THROWS on
160
+ * a missing/empty salt.
161
+ */
162
+ export function leafDigest(leaf: SaltedLeaf, hasher: DigestHasher): Uint8Array {
163
+ const saltBytes = requireSaltBytes(leaf);
164
+ return hasher(encodeFields([SD_LEAF_DOMAIN_V1, leaf.name, leaf.value, saltBytes]));
165
+ }
166
+
167
+ /**
168
+ * Commit to a SET of salted leaves → a single root (the signed/persisted value).
169
+ * Sorts leaves by raw leaf-digest bytes, then digests them under `SD_SET_DOMAIN_V1`.
170
+ * Like `digest`, this emits a BARE digest — apply `cid()` on top for the self-
171
+ * describing column representation (`cid(set_commit(...))`).
172
+ *
173
+ * The empty set is well-defined (the digest of `[SD_SET_DOMAIN_V1]`), not an error.
174
+ * THROWS on a duplicate `name` or a missing/empty `salt` (invalid states made
175
+ * impossible). Resolve `hasher`/`encode` once and reuse — no per-call branching.
176
+ */
177
+ export function setCommit(
178
+ leaves: readonly SaltedLeaf[],
179
+ hasher: DigestHasher = resolveHasher('sha256'),
180
+ encode: OutputEncoder = resolveOutputEncoder('base64url'),
181
+ ): string | Uint8Array {
182
+ assertUniqueNames(leaves);
183
+ const leafDigests = leaves.map((leaf) => leafDigest(leaf, hasher));
184
+ leafDigests.sort(compareBytes);
185
+ return encode(hasher(encodeFields([SD_SET_DOMAIN_V1, ...leafDigests])));
186
+ }
187
+
188
+ /**
189
+ * Split a leaf set into the revealed `(name, value, salt)` triples plus the opaque
190
+ * leaf digests (base64url) of the rest. Withheld `value`/`salt` never appear in the
191
+ * output. Names in `revealNames` that match no leaf are simply not disclosed.
192
+ * THROWS on a duplicate `name` or a missing/empty salt of a withheld leaf.
193
+ */
194
+ export function setDisclose(
195
+ leaves: readonly SaltedLeaf[],
196
+ revealNames: readonly string[],
197
+ hasher: DigestHasher = resolveHasher('sha256'),
198
+ ): SetDisclosure {
199
+ assertUniqueNames(leaves);
200
+ const reveal = new Set(revealNames);
201
+ const disclosed: SaltedLeaf[] = [];
202
+ const hidden: string[] = [];
203
+ for (const leaf of leaves) {
204
+ if (reveal.has(leaf.name)) {
205
+ disclosed.push(leaf);
206
+ } else {
207
+ hidden.push(uint8ArrayToString(leafDigest(leaf, hasher), HIDDEN_ENCODING));
208
+ }
209
+ }
210
+ return { disclosed, hidden };
211
+ }
212
+
213
+ /**
214
+ * Verify a disclosure against a signed root. Recomputes the disclosed leaves'
215
+ * digests, unions them with the supplied hidden digests, sorts by bytes, recomputes
216
+ * the root, and compares to `root`. This reconstructs the ENTIRE root, so it proves
217
+ * the disclosed leaves belong to *exactly* this committed set — the holder cannot
218
+ * add, drop, or swap a leaf (the leaf count is bound too).
219
+ *
220
+ * `encode` is how the signed `root` is rendered (so the recomputed root is encoded
221
+ * the same way before comparison); for a `Uint8Array` root the raw bytes are compared
222
+ * directly. Returns `false` on mismatch or malformed input — mirroring `verify`'s
223
+ * forgiving contract rather than throwing.
224
+ */
225
+ export function setVerify(
226
+ root: string | Uint8Array,
227
+ disclosure: SetDisclosure,
228
+ hasher: DigestHasher = resolveHasher('sha256'),
229
+ encode: OutputEncoder = resolveOutputEncoder('base64url'),
230
+ ): boolean {
231
+ try {
232
+ const { disclosed, hidden } = disclosure;
233
+ const digests: Uint8Array[] = [];
234
+ for (const leaf of disclosed) {
235
+ digests.push(leafDigest(leaf, hasher));
236
+ }
237
+ for (const h of hidden) {
238
+ digests.push(uint8ArrayFromString(h, HIDDEN_ENCODING));
239
+ }
240
+ digests.sort(compareBytes);
241
+ const recomputed = hasher(encodeFields([SD_SET_DOMAIN_V1, ...digests]));
242
+ if (root instanceof Uint8Array) {
243
+ return bytesEqual(recomputed, root);
244
+ }
245
+ const encoded = encode(recomputed);
246
+ return typeof encoded === 'string' && encoded === root;
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
package/src/digest.ts DELETED
@@ -1,173 +0,0 @@
1
- /**
2
- * Digest Function for Quereus
3
- *
4
- * Computes the hash of all arguments combined.
5
- * Uses SHA-256 from @noble/hashes for portable implementation.
6
- * Compatible with React Native and all JS environments.
7
- */
8
-
9
- import { sha256 } from '@noble/hashes/sha2.js';
10
- import { sha512 } from '@noble/hashes/sha2.js';
11
- import { blake3 } from '@noble/hashes/blake3.js';
12
- import { concatBytes, utf8ToBytes } from '@noble/hashes/utils.js';
13
-
14
- /**
15
- * Hash algorithm options
16
- */
17
- export type HashAlgorithm = 'sha256' | 'sha512' | 'blake3';
18
-
19
- /**
20
- * Input type for digest function - can be string, Uint8Array, or number
21
- */
22
- export type DigestInput = string | Uint8Array | number | boolean | null | undefined;
23
-
24
- /**
25
- * Options for the digest function
26
- */
27
- export interface DigestOptions {
28
- /** Hash algorithm to use (default: sha256) */
29
- algorithm?: HashAlgorithm;
30
- /** Output format (default: uint8array) */
31
- output?: 'uint8array' | 'hex';
32
- }
33
-
34
- /**
35
- * Convert various input types to Uint8Array for hashing
36
- */
37
- function inputToBytes(input: DigestInput): Uint8Array {
38
- if (input === null || input === undefined) {
39
- return new Uint8Array(0);
40
- }
41
-
42
- if (typeof input === 'string') {
43
- return utf8ToBytes(input);
44
- }
45
-
46
- if (input instanceof Uint8Array) {
47
- return input;
48
- }
49
-
50
- if (typeof input === 'number') {
51
- // Convert number to 8-byte big-endian representation
52
- const buffer = new ArrayBuffer(8);
53
- const view = new DataView(buffer);
54
- view.setFloat64(0, input, false); // big-endian
55
- return new Uint8Array(buffer);
56
- }
57
-
58
- if (typeof input === 'boolean') {
59
- return new Uint8Array([input ? 1 : 0]);
60
- }
61
-
62
- // Fallback: convert to string then to bytes
63
- return utf8ToBytes(String(input));
64
- }
65
-
66
- /**
67
- * Get hash function based on algorithm
68
- */
69
- function getHashFunction(algorithm: HashAlgorithm): (data: Uint8Array) => Uint8Array {
70
- switch (algorithm) {
71
- case 'sha256':
72
- return sha256;
73
- case 'sha512':
74
- return sha512;
75
- case 'blake3':
76
- return blake3;
77
- default:
78
- throw new Error(`Unsupported hash algorithm: ${algorithm}`);
79
- }
80
- }
81
-
82
- /**
83
- * Computes the hash of all arguments
84
- *
85
- * @param {...DigestInput} args - Variable number of arguments to hash
86
- * @returns {Uint8Array} The computed hash as a Uint8Array
87
- *
88
- * @example
89
- * ```typescript
90
- * // Hash a string
91
- * const hash1 = Digest('hello world');
92
- *
93
- * // Hash multiple arguments
94
- * const hash2 = Digest('user:', 123, 'session');
95
- *
96
- * // Hash with specific algorithm
97
- * const hash3 = Digest.withOptions({ algorithm: 'sha512' })('data1', 'data2');
98
- *
99
- * // Get hex output
100
- * const hexHash = Digest.withOptions({ output: 'hex' })('hello');
101
- * ```
102
- */
103
- export function Digest(...args: DigestInput[]): Uint8Array {
104
- return DigestWithOptions({ algorithm: 'sha256', output: 'uint8array' }, ...args) as Uint8Array;
105
- }
106
-
107
- /**
108
- * Digest function with custom options
109
- */
110
- export function DigestWithOptions(options: DigestOptions, ...args: DigestInput[]): Uint8Array | string {
111
- const algorithm = options.algorithm || 'sha256';
112
- const output = options.output || 'uint8array';
113
-
114
- // Convert all arguments to bytes and concatenate
115
- const byteArrays = args.map(inputToBytes);
116
- const combined = concatBytes(...byteArrays);
117
-
118
- // Hash the combined data
119
- const hashFunction = getHashFunction(algorithm);
120
- const hash = hashFunction(combined);
121
-
122
- // Return in requested format
123
- if (output === 'hex') {
124
- return Array.from(hash)
125
- .map(b => b.toString(16).padStart(2, '0'))
126
- .join('');
127
- }
128
-
129
- return hash;
130
- }
131
-
132
- /**
133
- * Create a digest function with preset options
134
- */
135
- Digest.withOptions = (options: DigestOptions) => {
136
- return (...args: DigestInput[]) => DigestWithOptions(options, ...args);
137
- };
138
-
139
- /**
140
- * Convenience functions for specific algorithms
141
- */
142
- Digest.sha256 = (...args: DigestInput[]): Uint8Array => {
143
- return DigestWithOptions({ algorithm: 'sha256' }, ...args) as Uint8Array;
144
- };
145
-
146
- Digest.sha512 = (...args: DigestInput[]): Uint8Array => {
147
- return DigestWithOptions({ algorithm: 'sha512' }, ...args) as Uint8Array;
148
- };
149
-
150
- Digest.blake3 = (...args: DigestInput[]): Uint8Array => {
151
- return DigestWithOptions({ algorithm: 'blake3' }, ...args) as Uint8Array;
152
- };
153
-
154
- /**
155
- * Hex output variants
156
- */
157
- Digest.hex = (...args: DigestInput[]): string => {
158
- return DigestWithOptions({ algorithm: 'sha256', output: 'hex' }, ...args) as string;
159
- };
160
-
161
- Digest.sha256Hex = (...args: DigestInput[]): string => {
162
- return DigestWithOptions({ algorithm: 'sha256', output: 'hex' }, ...args) as string;
163
- };
164
-
165
- Digest.sha512Hex = (...args: DigestInput[]): string => {
166
- return DigestWithOptions({ algorithm: 'sha512', output: 'hex' }, ...args) as string;
167
- };
168
-
169
- Digest.blake3Hex = (...args: DigestInput[]): string => {
170
- return DigestWithOptions({ algorithm: 'blake3', output: 'hex' }, ...args) as string;
171
- };
172
-
173
- export default Digest;
package/src/sign.ts DELETED
@@ -1,235 +0,0 @@
1
- /**
2
- * Sign Function for Quereus
3
- *
4
- * Returns the signature for the given payload using an ECC private key.
5
- * Uses secp256k1 from @noble/curves for portable implementation.
6
- * Compatible with React Native and all JS environments.
7
- */
8
-
9
- import { secp256k1 } from '@noble/curves/secp256k1.js';
10
- import { p256 } from '@noble/curves/nist.js';
11
- import { ed25519 } from '@noble/curves/ed25519.js';
12
- import { bytesToHex, hexToBytes } from '@noble/curves/utils.js';
13
-
14
- /**
15
- * Supported elliptic curve types
16
- */
17
- export type CurveType = 'secp256k1' | 'p256' | 'ed25519';
18
-
19
- /**
20
- * Private key input - can be Uint8Array, hex string, or bigint
21
- */
22
- export type PrivateKeyInput = Uint8Array | string | bigint;
23
-
24
- /**
25
- * Digest input - can be Uint8Array or hex string
26
- */
27
- export type DigestInput = Uint8Array | string;
28
-
29
- /**
30
- * Signature output format options
31
- */
32
- export type SignatureFormat = 'uint8array' | 'hex' | 'compact' | 'der';
33
-
34
- /**
35
- * Options for the Sign function
36
- */
37
- export interface SignOptions {
38
- /** Elliptic curve to use (default: secp256k1) */
39
- curve?: CurveType;
40
- /** Output format for signature (default: uint8array) */
41
- format?: SignatureFormat;
42
- /** Additional entropy for signatures (hedged signatures) */
43
- extraEntropy?: boolean | Uint8Array;
44
- /** Use low-S canonical signatures (default: true) */
45
- lowS?: boolean;
46
- }
47
-
48
- /**
49
- * Normalize private key input to Uint8Array
50
- */
51
- function normalizePrivateKey(privateKey: PrivateKeyInput): Uint8Array {
52
- if (privateKey instanceof Uint8Array) {
53
- return privateKey;
54
- }
55
-
56
- if (typeof privateKey === 'string') {
57
- // Assume hex string
58
- return hexToBytes(privateKey);
59
- }
60
-
61
- if (typeof privateKey === 'bigint') {
62
- // Convert bigint to 32-byte array (for secp256k1/p256)
63
- const hex = privateKey.toString(16).padStart(64, '0');
64
- return hexToBytes(hex);
65
- }
66
-
67
- throw new Error('Invalid private key format');
68
- }
69
-
70
- /**
71
- * Normalize digest input to Uint8Array
72
- */
73
- function normalizeDigest(digest: DigestInput): Uint8Array {
74
- if (digest instanceof Uint8Array) {
75
- return digest;
76
- }
77
-
78
- if (typeof digest === 'string') {
79
- return hexToBytes(digest);
80
- }
81
-
82
- throw new Error('Invalid digest format');
83
- }
84
-
85
- /**
86
- * Format signature based on requested format.
87
- * In @noble/curves v2.0.1, sign() returns Uint8Array directly.
88
- */
89
- function formatSignature(signature: Uint8Array, format: SignatureFormat, curve: CurveType): Uint8Array | string {
90
- switch (format) {
91
- case 'uint8array':
92
- case 'compact':
93
- return signature;
94
-
95
- case 'hex':
96
- return bytesToHex(signature);
97
-
98
- case 'der':
99
- if (curve === 'ed25519') {
100
- throw new Error('DER format not supported for ed25519');
101
- }
102
- // DER format requires the sign() call to request it via format option
103
- // For now, return compact format as fallback
104
- return signature;
105
-
106
- default:
107
- throw new Error(`Unsupported signature format: ${format}`);
108
- }
109
- }
110
-
111
- /**
112
- * Sign a digest using the specified private key and curve
113
- *
114
- * @param {DigestInput} digest - The digest/hash to sign
115
- * @param {PrivateKeyInput} privateKey - The private key to use for signing
116
- * @param {SignOptions} [options] - Optional signing parameters
117
- * @returns {Uint8Array | string} The signature in the requested format
118
- *
119
- * @example
120
- * ```typescript
121
- * // Basic usage with secp256k1
122
- * const digest = new Uint8Array(32).fill(1); // Your hash here
123
- * const privateKey = 'a'.repeat(64); // Your private key hex
124
- * const signature = Sign(digest, privateKey);
125
- *
126
- * // With specific curve and format
127
- * const sig = Sign(digest, privateKey, {
128
- * curve: 'p256',
129
- * format: 'hex'
130
- * });
131
- *
132
- * // With hedged signatures for extra security
133
- * const hedgedSig = Sign(digest, privateKey, {
134
- * extraEntropy: true
135
- * });
136
- * ```
137
- */
138
- export function Sign(
139
- digest: DigestInput,
140
- privateKey: PrivateKeyInput,
141
- options: SignOptions = {}
142
- ): Uint8Array | string {
143
- const {
144
- curve = 'secp256k1',
145
- format = 'uint8array',
146
- extraEntropy = false,
147
- lowS = true,
148
- } = options;
149
-
150
- const normalizedDigest = normalizeDigest(digest);
151
- const normalizedPrivateKey = normalizePrivateKey(privateKey);
152
-
153
- let signature: any;
154
-
155
- switch (curve) {
156
- case 'secp256k1': {
157
- const signOptions: any = { lowS };
158
- if (extraEntropy) {
159
- signOptions.extraEntropy = extraEntropy;
160
- }
161
- signature = secp256k1.sign(normalizedDigest, normalizedPrivateKey, signOptions);
162
- break;
163
- }
164
-
165
- case 'p256': {
166
- const signOptions: any = { lowS };
167
- if (extraEntropy) {
168
- signOptions.extraEntropy = extraEntropy;
169
- }
170
- signature = p256.sign(normalizedDigest, normalizedPrivateKey, signOptions);
171
- break;
172
- }
173
-
174
- case 'ed25519': {
175
- signature = ed25519.sign(normalizedDigest, normalizedPrivateKey);
176
- break;
177
- }
178
-
179
- default:
180
- throw new Error(`Unsupported curve: ${curve}`);
181
- }
182
-
183
- return formatSignature(signature, format, curve);
184
- }
185
-
186
- /**
187
- * Convenience functions for specific curves
188
- */
189
- Sign.secp256k1 = (digest: DigestInput, privateKey: PrivateKeyInput, options: Omit<SignOptions, 'curve'> = {}) => {
190
- return Sign(digest, privateKey, { ...options, curve: 'secp256k1' });
191
- };
192
-
193
- Sign.p256 = (digest: DigestInput, privateKey: PrivateKeyInput, options: Omit<SignOptions, 'curve'> = {}) => {
194
- return Sign(digest, privateKey, { ...options, curve: 'p256' });
195
- };
196
-
197
- Sign.ed25519 = (digest: DigestInput, privateKey: PrivateKeyInput, options: Omit<SignOptions, 'curve'> = {}) => {
198
- return Sign(digest, privateKey, { ...options, curve: 'ed25519' });
199
- };
200
-
201
- /**
202
- * Generate a random private key for the specified curve
203
- */
204
- Sign.generatePrivateKey = (curve: CurveType = 'secp256k1'): Uint8Array => {
205
- switch (curve) {
206
- case 'secp256k1':
207
- return secp256k1.utils.randomSecretKey();
208
- case 'p256':
209
- return p256.utils.randomSecretKey();
210
- case 'ed25519':
211
- return ed25519.utils.randomSecretKey();
212
- default:
213
- throw new Error(`Unsupported curve: ${curve}`);
214
- }
215
- };
216
-
217
- /**
218
- * Get the public key for a given private key and curve
219
- */
220
- Sign.getPublicKey = (privateKey: PrivateKeyInput, curve: CurveType = 'secp256k1'): Uint8Array => {
221
- const normalizedPrivateKey = normalizePrivateKey(privateKey);
222
-
223
- switch (curve) {
224
- case 'secp256k1':
225
- return secp256k1.getPublicKey(normalizedPrivateKey);
226
- case 'p256':
227
- return p256.getPublicKey(normalizedPrivateKey);
228
- case 'ed25519':
229
- return ed25519.getPublicKey(normalizedPrivateKey);
230
- default:
231
- throw new Error(`Unsupported curve: ${curve}`);
232
- }
233
- };
234
-
235
- export default Sign;