Skip to content

Commit 3b178c6

Browse files
brancoderdavidgvfKeitoTadashiVmMad
authored
feat(sdk): add normalizePasskeyPublicKey utility (#104)
# Description of change resolves #63 port of iotaledger/iota#11258 ## Links to any relevant issues Port of iotaledger/iota#10411 by @davidgvf. Applied manually on a fresh branch to resolve import conflicts introduced by iotaledger/iota#10445 (removal of deprecated `fromB64`/`toB64` aliases). No functional changes from the original — only the import names were updated to `fromBase64`/`toBase64`. --------- Co-authored-by: davidgvf <davidgvf@users.noreply.github.com> Co-authored-by: Keito <64607484+KeitoTadashi@users.noreply.github.com> Co-authored-by: JCNoguera <88061365+VmMad@users.noreply.github.com>
1 parent 9faf218 commit 3b178c6

4 files changed

Lines changed: 85 additions & 9 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@iota/iota-sdk': minor
3+
---
4+
5+
Add `normalizePasskeyPublicKey` utility and update `PasskeyPublicKey` to accept DER SPKI (91-byte), uncompressed (65-byte), and raw XY (64-byte) passkey public key formats in addition to the standard compressed (33-byte) format.

sdk/typescript/src/keypairs/passkey/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
export { PasskeyKeypair, BrowserPasskeyProvider, findCommonPublicKey } from './keypair.js';
66
export type { PasskeyProvider, BrowserPasswordProviderOptions } from './keypair.js';
7-
export { PasskeyPublicKey } from './publickey.js';
7+
export { PasskeyPublicKey, normalizePasskeyPublicKey } from './publickey.js';

sdk/typescript/src/keypairs/passkey/publickey.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,16 @@ export class PasskeyPublicKey extends PublicKey {
7575
constructor(value: PublicKeyInitData) {
7676
super();
7777

78+
let bytes: Uint8Array;
7879
if (typeof value === 'string') {
79-
this.data = fromBase64(value);
80+
bytes = fromBase64(value);
8081
} else if (value instanceof Uint8Array) {
81-
this.data = value;
82+
bytes = value;
8283
} else {
83-
this.data = Uint8Array.from(value);
84+
bytes = Uint8Array.from(value);
8485
}
8586

86-
if (this.data.length !== PASSKEY_PUBLIC_KEY_SIZE) {
87-
throw new Error(
88-
`Invalid public key input. Expected ${PASSKEY_PUBLIC_KEY_SIZE} bytes, got ${this.data.length}`,
89-
);
90-
}
87+
this.data = normalizePasskeyPublicKey(bytes);
9188
}
9289

9390
/**
@@ -170,6 +167,35 @@ export function parseDerSPKI(derBytes: Uint8Array): Uint8Array {
170167
return derBytes.slice(SECP256R1_SPKI_HEADER.length);
171168
}
172169

170+
/**
171+
* Normalizes a WebAuthn public key to the canonical 33-byte compressed secp256r1 format.
172+
* Accepts: 33-byte compressed, 65-byte uncompressed (0x04||x||y), 64-byte raw (x||y), 91-byte DER SPKI.
173+
*/
174+
export function normalizePasskeyPublicKey(input: Uint8Array): Uint8Array {
175+
if (input.length === PASSKEY_PUBLIC_KEY_SIZE) {
176+
secp256r1.ProjectivePoint.fromHex(input); // throws if not a valid curve point
177+
return input;
178+
}
179+
180+
if (input.length === SECP256R1_SPKI_HEADER.length + PASSKEY_UNCOMPRESSED_PUBLIC_KEY_SIZE) {
181+
const uncompressed65 = parseDerSPKI(input);
182+
return secp256r1.ProjectivePoint.fromHex(uncompressed65).toRawBytes(true);
183+
}
184+
185+
if (input.length === PASSKEY_UNCOMPRESSED_PUBLIC_KEY_SIZE && input[0] === 0x04) {
186+
return secp256r1.ProjectivePoint.fromHex(input).toRawBytes(true);
187+
}
188+
189+
if (input.length === PASSKEY_UNCOMPRESSED_PUBLIC_KEY_SIZE - 1) {
190+
const uncompressed65 = new Uint8Array(PASSKEY_UNCOMPRESSED_PUBLIC_KEY_SIZE);
191+
uncompressed65[0] = 0x04;
192+
uncompressed65.set(input, 1);
193+
return secp256r1.ProjectivePoint.fromHex(uncompressed65).toRawBytes(true);
194+
}
195+
196+
throw new Error(`Unsupported passkey public key length: ${input.length}`);
197+
}
198+
173199
/**
174200
* Parse signature from bytes or base64 string into the following fields.
175201
*/

sdk/typescript/test/unit/cryptography/passkey.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,48 @@ describe('passkey signer E2E testing', () => {
365365
expect(signer2.getPublicKey().toIotaAddress()).toEqual(address);
366366
});
367367
});
368+
369+
describe('PasskeyPublicKey normalization', () => {
370+
const compressed = fromBase64('A25OtZSBXLMdIJOgZApGPtgjYcsJmK4ve0+52QRnI4vG');
371+
372+
it('should handle compressed public key (33 bytes)', () => {
373+
const pk = new PasskeyPublicKey(compressed);
374+
expect(pk.toRawBytes()).toEqual(compressed);
375+
expect(pk.toRawBytes().length).toBe(33);
376+
});
377+
378+
it('should handle uncompressed public key (65 bytes)', () => {
379+
const sk = secp256r1.utils.randomPrivateKey();
380+
const pkUncompressed = secp256r1.getPublicKey(sk, false);
381+
const pkCompressed = secp256r1.getPublicKey(sk, true);
382+
const pk = new PasskeyPublicKey(pkUncompressed);
383+
expect(pk.toRawBytes()).toEqual(pkCompressed);
384+
expect(pk.toRawBytes().length).toBe(33);
385+
});
386+
387+
it('should handle raw public key (64 bytes)', () => {
388+
const sk = secp256r1.utils.randomPrivateKey();
389+
const pkUncompressed = secp256r1.getPublicKey(sk, false);
390+
const pkRaw = pkUncompressed.slice(1);
391+
const pkCompressed = secp256r1.getPublicKey(sk, true);
392+
const pk = new PasskeyPublicKey(pkRaw);
393+
expect(pk.toRawBytes()).toEqual(pkCompressed);
394+
expect(pk.toRawBytes().length).toBe(33);
395+
});
396+
397+
it('should handle DER SPKI public key (91 bytes)', () => {
398+
const sk = secp256r1.utils.randomPrivateKey();
399+
const pkUncompressed = secp256r1.getPublicKey(sk, false);
400+
const pkCompressed = secp256r1.getPublicKey(sk, true);
401+
const der = new Uint8Array([...SECP256R1_SPKI_HEADER, ...pkUncompressed]);
402+
const pk = new PasskeyPublicKey(der);
403+
expect(pk.toRawBytes()).toEqual(pkCompressed);
404+
expect(pk.toRawBytes().length).toBe(33);
405+
});
406+
407+
it('should throw error for invalid length', () => {
408+
expect(() => new PasskeyPublicKey(new Uint8Array(32))).toThrow(
409+
'Unsupported passkey public key length: 32',
410+
);
411+
});
412+
});

0 commit comments

Comments
 (0)