@optimystic/quereus-plugin-crypto 0.2.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.
@@ -0,0 +1,279 @@
1
+ /**
2
+ * SignatureValid Function for Quereus
3
+ *
4
+ * Returns true if the ECC signature is valid for the given digest and public key.
5
+ * Uses @noble/curves for portable implementation.
6
+ * Compatible with React Native and all JS environments.
7
+ */
8
+
9
+ import { secp256k1 } from '@noble/curves/secp256k1';
10
+ import { p256 } from '@noble/curves/nist';
11
+ import { ed25519 } from '@noble/curves/ed25519';
12
+ import { hexToBytes } from '@noble/curves/abstract/utils';
13
+
14
+ /**
15
+ * Supported elliptic curve types
16
+ */
17
+ export type CurveType = 'secp256k1' | 'p256' | 'ed25519';
18
+
19
+ /**
20
+ * Input types that can be Uint8Array or hex string
21
+ */
22
+ export type BytesInput = Uint8Array | string;
23
+
24
+ /**
25
+ * Options for signature verification
26
+ */
27
+ export interface VerifyOptions {
28
+ /** Elliptic curve to use (default: secp256k1) */
29
+ curve?: CurveType;
30
+ /** Signature format (default: auto-detect) */
31
+ signatureFormat?: 'compact' | 'der' | 'raw';
32
+ /** Allow malleable signatures (default: false for ECDSA, true for EdDSA) */
33
+ allowMalleableSignatures?: boolean;
34
+ }
35
+
36
+ /**
37
+ * Normalize input to Uint8Array
38
+ */
39
+ function normalizeBytes(input: BytesInput): Uint8Array {
40
+ if (input instanceof Uint8Array) {
41
+ return input;
42
+ }
43
+
44
+ if (typeof input === 'string') {
45
+ return hexToBytes(input);
46
+ }
47
+
48
+ throw new Error('Invalid input format - expected Uint8Array or hex string');
49
+ }
50
+
51
+ /**
52
+ * Auto-detect signature format based on length and curve
53
+ */
54
+ function detectSignatureFormat(signature: Uint8Array, curve: CurveType): 'compact' | 'der' | 'raw' {
55
+ const length = signature.length;
56
+
57
+ if (curve === 'ed25519') {
58
+ return 'raw'; // Ed25519 signatures are always 64 bytes
59
+ }
60
+
61
+ // For ECDSA curves (secp256k1, p256)
62
+ if (length === 64) {
63
+ return 'compact'; // r + s concatenated (32 + 32 bytes)
64
+ }
65
+
66
+ if (length >= 70 && length <= 72 && signature[0] === 0x30) {
67
+ return 'der'; // DER encoding starts with 0x30
68
+ }
69
+
70
+ // Default to compact for shorter signatures
71
+ return 'compact';
72
+ }
73
+
74
+ /**
75
+ * Parse signature based on format and curve
76
+ */
77
+ function parseSignature(signature: Uint8Array, format: 'compact' | 'der' | 'raw', curve: CurveType): any {
78
+ if (curve === 'ed25519') {
79
+ // Ed25519 signatures are always raw 64-byte format
80
+ return signature;
81
+ }
82
+
83
+ // For ECDSA curves
84
+ switch (format) {
85
+ case 'compact':
86
+ if (curve === 'secp256k1') {
87
+ return secp256k1.Signature.fromCompact(signature);
88
+ } else if (curve === 'p256') {
89
+ return p256.Signature.fromCompact(signature);
90
+ }
91
+ break;
92
+
93
+ case 'der':
94
+ if (curve === 'secp256k1') {
95
+ return secp256k1.Signature.fromDER(signature);
96
+ } else if (curve === 'p256') {
97
+ return p256.Signature.fromDER(signature);
98
+ }
99
+ break;
100
+
101
+ case 'raw':
102
+ // Treat as compact for ECDSA
103
+ if (curve === 'secp256k1') {
104
+ return secp256k1.Signature.fromCompact(signature);
105
+ } else if (curve === 'p256') {
106
+ return p256.Signature.fromCompact(signature);
107
+ }
108
+ break;
109
+ }
110
+
111
+ throw new Error(`Failed to parse signature for curve ${curve} with format ${format}`);
112
+ }
113
+
114
+ /**
115
+ * Verify if an ECC signature is valid
116
+ *
117
+ * @param {BytesInput} digest - The digest/hash that was signed
118
+ * @param {BytesInput} signature - The signature to verify
119
+ * @param {BytesInput} publicKey - The public key to verify against
120
+ * @param {VerifyOptions} [options] - Optional verification parameters
121
+ * @returns {boolean} True if the signature is valid, false otherwise
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * // Basic usage with secp256k1
126
+ * const isValid = SignatureValid(digest, signature, publicKey);
127
+ *
128
+ * // With specific curve
129
+ * const isValid = SignatureValid(digest, signature, publicKey, {
130
+ * curve: 'p256'
131
+ * });
132
+ *
133
+ * // With specific signature format
134
+ * const isValid = SignatureValid(digest, signature, publicKey, {
135
+ * curve: 'secp256k1',
136
+ * signatureFormat: 'der'
137
+ * });
138
+ *
139
+ * // Allow malleable signatures
140
+ * const isValid = SignatureValid(digest, signature, publicKey, {
141
+ * allowMalleableSignatures: true
142
+ * });
143
+ * ```
144
+ */
145
+ export function SignatureValid(
146
+ digest: BytesInput,
147
+ signature: BytesInput,
148
+ publicKey: BytesInput,
149
+ options: VerifyOptions = {}
150
+ ): boolean {
151
+ try {
152
+ const {
153
+ curve = 'secp256k1',
154
+ signatureFormat,
155
+ allowMalleableSignatures,
156
+ } = options;
157
+
158
+ const normalizedDigest = normalizeBytes(digest);
159
+ const normalizedSignature = normalizeBytes(signature);
160
+ const normalizedPublicKey = normalizeBytes(publicKey);
161
+
162
+ // Auto-detect signature format if not specified
163
+ const detectedFormat = signatureFormat || detectSignatureFormat(normalizedSignature, curve);
164
+
165
+ // Parse the signature
166
+ const parsedSignature = parseSignature(normalizedSignature, detectedFormat, curve);
167
+
168
+ // Set up verification options
169
+ const verifyOptions: any = {};
170
+
171
+ // Handle malleable signatures for ECDSA curves
172
+ if (curve !== 'ed25519' && allowMalleableSignatures !== undefined) {
173
+ verifyOptions.lowS = !allowMalleableSignatures;
174
+ }
175
+
176
+ // Verify the signature
177
+ switch (curve) {
178
+ case 'secp256k1':
179
+ return secp256k1.verify(parsedSignature, normalizedDigest, normalizedPublicKey, verifyOptions);
180
+
181
+ case 'p256':
182
+ return p256.verify(parsedSignature, normalizedDigest, normalizedPublicKey, verifyOptions);
183
+
184
+ case 'ed25519':
185
+ return ed25519.verify(parsedSignature, normalizedDigest, normalizedPublicKey);
186
+
187
+ default:
188
+ throw new Error(`Unsupported curve: ${curve}`);
189
+ }
190
+ } catch (error) {
191
+ // If any error occurs during verification, the signature is invalid
192
+ return false;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Convenience functions for specific curves
198
+ */
199
+ SignatureValid.secp256k1 = (
200
+ digest: BytesInput,
201
+ signature: BytesInput,
202
+ publicKey: BytesInput,
203
+ options: Omit<VerifyOptions, 'curve'> = {}
204
+ ): boolean => {
205
+ return SignatureValid(digest, signature, publicKey, { ...options, curve: 'secp256k1' });
206
+ };
207
+
208
+ SignatureValid.p256 = (
209
+ digest: BytesInput,
210
+ signature: BytesInput,
211
+ publicKey: BytesInput,
212
+ options: Omit<VerifyOptions, 'curve'> = {}
213
+ ): boolean => {
214
+ return SignatureValid(digest, signature, publicKey, { ...options, curve: 'p256' });
215
+ };
216
+
217
+ SignatureValid.ed25519 = (
218
+ digest: BytesInput,
219
+ signature: BytesInput,
220
+ publicKey: BytesInput,
221
+ options: Omit<VerifyOptions, 'curve'> = {}
222
+ ): boolean => {
223
+ return SignatureValid(digest, signature, publicKey, { ...options, curve: 'ed25519' });
224
+ };
225
+
226
+ /**
227
+ * Batch verify multiple signatures (more efficient for multiple verifications)
228
+ */
229
+ SignatureValid.batch = (
230
+ verifications: Array<{
231
+ digest: BytesInput;
232
+ signature: BytesInput;
233
+ publicKey: BytesInput;
234
+ options?: VerifyOptions;
235
+ }>
236
+ ): boolean[] => {
237
+ return verifications.map(({ digest, signature, publicKey, options }) =>
238
+ SignatureValid(digest, signature, publicKey, options)
239
+ );
240
+ };
241
+
242
+ /**
243
+ * Verify and return detailed information about the verification
244
+ */
245
+ SignatureValid.detailed = (
246
+ digest: BytesInput,
247
+ signature: BytesInput,
248
+ publicKey: BytesInput,
249
+ options: VerifyOptions = {}
250
+ ): {
251
+ valid: boolean;
252
+ curve: CurveType;
253
+ signatureFormat: string;
254
+ error?: string;
255
+ } => {
256
+ const curve = options.curve || 'secp256k1';
257
+
258
+ try {
259
+ const normalizedSignature = normalizeBytes(signature);
260
+ const detectedFormat = options.signatureFormat || detectSignatureFormat(normalizedSignature, curve);
261
+
262
+ const valid = SignatureValid(digest, signature, publicKey, options);
263
+
264
+ return {
265
+ valid,
266
+ curve,
267
+ signatureFormat: detectedFormat,
268
+ };
269
+ } catch (error) {
270
+ return {
271
+ valid: false,
272
+ curve,
273
+ signatureFormat: 'unknown',
274
+ error: error instanceof Error ? error.message : 'Unknown error',
275
+ };
276
+ }
277
+ };
278
+
279
+ export default SignatureValid;