Skip to content

Commit 1b4fa3d

Browse files
committed
Support wa key signature verification and recovery
1 parent a22e282 commit 1b4fa3d

File tree

6 files changed

+188
-3
lines changed

6 files changed

+188
-3
lines changed

src/chain/public-key.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ export class PublicKey implements ABISerializableObject {
6868
this.data = data
6969
}
7070

71+
/**
72+
* Returns the core 33-byte compressed public key data as a Uint8Array.
73+
* This is suitable for cryptographic operations like verification.
74+
*/
75+
getCompressedKeyBytes(): Uint8Array {
76+
return this.data.array.subarray(0, 33)
77+
}
78+
7179
equals(other: PublicKeyType) {
7280
const otherKey = PublicKey.from(other)
7381
return this.type === otherKey.type && this.data.equals(otherKey.data)

src/chain/signature.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,12 @@ export class Signature implements ABISerializableObject {
9999
/** Verify this signature with given message digest and public key. */
100100
verifyDigest(digest: Checksum256Type, publicKey: PublicKey) {
101101
digest = Checksum256.from(digest)
102-
return verify(this.data.array, digest.array, publicKey.data.array, this.type)
102+
return verify(
103+
this.data.array,
104+
digest.array,
105+
publicKey.data.array.subarray(0, 33),
106+
this.type
107+
)
103108
}
104109

105110
/** Verify this signature with given message and public key. */

src/crypto/curves.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export function getCurve(type: string): ec {
1313
rv = curves[type] = new ec('secp256k1')
1414
} else if (type === 'R1') {
1515
rv = curves[type] = new ec('p256')
16+
} else if (type === 'WA') {
17+
rv = curves[type] = new ec('p256')
1618
} else {
1719
throw new Error(`Unknown curve type: ${type}`)
1820
}

src/crypto/recover.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export function recover(signature: Uint8Array, message: Uint8Array, type: string
88
const curve = getCurve(type)
99
const recid = signature[0] - 31
1010
const r = signature.subarray(1, 33)
11-
const s = signature.subarray(33)
11+
const s = signature.subarray(33, 33 + 32)
1212
const point = curve.recoverPubKey(message, {r, s}, recid)
1313
return new Uint8Array(point.encodeCompressed())
1414
}

src/crypto/verify.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ export function verify(
1212
) {
1313
const curve = getCurve(type)
1414
const r = signature.subarray(1, 33)
15-
const s = signature.subarray(33)
15+
const s = signature.subarray(33, 33 + 32)
1616
return curve.verify(message, {r, s}, pubkey as any)
1717
}

test/webauthn.ts

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import {assert} from 'chai'
2+
import ecPackage from 'elliptic'
3+
const {ec: EC} = ecPackage
4+
5+
import {ABIEncoder, Bytes, Checksum256, KeyType, PrivateKey, PublicKey, Signature} from '$lib'
6+
7+
suite('WebAuthn (WA) Key Support', function () {
8+
this.slow(300)
9+
10+
const p256 = new EC('p256')
11+
const testKeyPair = p256.genKeyPair()
12+
const testPubPoint = testKeyPair.getPublic()
13+
const testCompressedPubKeyHex = testPubPoint.encodeCompressed('hex')
14+
const testCompressedPubKeyBytes = Bytes.from(testCompressedPubKeyHex, 'hex').array
15+
16+
const userPresenceByte = 0x01
17+
const hostname = 'example.com'
18+
const hostnameBytes = Bytes.from(hostname, 'utf8').array
19+
20+
const waPublicKeyData = new Uint8Array(33 + 1 + hostnameBytes.length)
21+
waPublicKeyData.set(testCompressedPubKeyBytes, 0)
22+
waPublicKeyData.set(new Uint8Array([userPresenceByte]), 33)
23+
waPublicKeyData.set(hostnameBytes, 34)
24+
const waPublicKeyBytes = new Bytes(waPublicKeyData)
25+
26+
const waPublicKey = new PublicKey(KeyType.WA, waPublicKeyBytes)
27+
const waPublicKeyString = waPublicKey.toString()
28+
29+
const messageToSign = Bytes.from('test message for WA keys', 'utf8')
30+
const messageDigest = Checksum256.hash(messageToSign)
31+
const sigFromElliptic = testKeyPair.sign(messageDigest.array, {canonical: true})
32+
33+
const rBytes = Bytes.from(sigFromElliptic.r.toArray('be', 32)).array
34+
const sBytes = Bytes.from(sigFromElliptic.s.toArray('be', 32)).array
35+
const recid = sigFromElliptic.recoveryParam!
36+
37+
const mockAuthData = Bytes.from(
38+
'mockAuthenticatorData012345678901234567890123456789',
39+
'utf8'
40+
).array
41+
const mockClientDataJSON = Bytes.from(
42+
'{"type":"webauthn.get","challenge":"...","origin":"https://example.com"}',
43+
'utf8'
44+
).array
45+
46+
const waSignatureDataEncoder = new ABIEncoder()
47+
waSignatureDataEncoder.writeByte(recid + 31)
48+
waSignatureDataEncoder.writeArray(rBytes)
49+
waSignatureDataEncoder.writeArray(sBytes)
50+
waSignatureDataEncoder.writeArray(mockAuthData) // Simulating Bytes.toABI
51+
waSignatureDataEncoder.writeArray(mockClientDataJSON) // Simulating Bytes.toABI
52+
const waSignatureBytes = waSignatureDataEncoder.getBytes()
53+
const waSignature = new Signature(KeyType.WA, waSignatureBytes)
54+
55+
test('WA PublicKey encoding', function () {
56+
assert.equal(waPublicKey.type, KeyType.WA, 'PublicKey type should be WA')
57+
assert.isTrue(waPublicKeyString.startsWith('PUB_WA_'), 'String format incorrect')
58+
59+
const parsedWAPublicKey = PublicKey.from(waPublicKeyString)
60+
assert.equal(parsedWAPublicKey.type, KeyType.WA, 'Parsed PublicKey type should be WA')
61+
assert.isTrue(
62+
parsedWAPublicKey.data.equals(waPublicKeyBytes),
63+
'Parsed PublicKey data mismatch'
64+
)
65+
assert.equal(
66+
parsedWAPublicKey.toString(),
67+
waPublicKeyString,
68+
'Parsed PublicKey toString mismatch'
69+
)
70+
71+
const compressedBytes = waPublicKey.getCompressedKeyBytes()
72+
assert.equal(compressedBytes.byteLength, 33, 'Compressed key bytes length')
73+
assert.deepEqual(
74+
compressedBytes,
75+
testCompressedPubKeyBytes,
76+
'Compressed key bytes content mismatch'
77+
)
78+
79+
assert.throws(
80+
() => {
81+
waPublicKey.toLegacyString()
82+
},
83+
/Unable to create legacy formatted string for non-K1 key/i,
84+
'WA key toLegacyString should throw'
85+
)
86+
})
87+
88+
test('WA Signature encoding', function () {
89+
assert.equal(waSignature.type, KeyType.WA, 'Signature type should be WA')
90+
const sigString = waSignature.toString()
91+
assert.isTrue(sigString.startsWith('SIG_WA_'), 'Signature string format incorrect')
92+
93+
const parsedWASignature = Signature.from(sigString)
94+
assert.equal(parsedWASignature.type, KeyType.WA, 'Parsed Signature type should be WA')
95+
assert.isTrue(
96+
parsedWASignature.data.equals(waSignatureBytes),
97+
'Parsed Signature data mismatch'
98+
)
99+
})
100+
101+
test('WA sign and verify', function () {
102+
assert.isTrue(
103+
waSignature.verifyDigest(messageDigest, waPublicKey),
104+
'WA signature should verify with correct digest and public key'
105+
)
106+
107+
const wrongMessageDigest = Checksum256.hash(Bytes.from('wrong message', 'utf8'))
108+
assert.isFalse(
109+
waSignature.verifyDigest(wrongMessageDigest, waPublicKey),
110+
'WA signature should not verify with incorrect digest'
111+
)
112+
113+
const anotherKeyPair = p256.genKeyPair()
114+
const differentCompressedPubKeyHex = anotherKeyPair.getPublic().encodeCompressed('hex')
115+
const differentCompressedPubKeyBytes = Bytes.from(differentCompressedPubKeyHex, 'hex').array
116+
117+
const differentWAPublicKeyData = new Uint8Array(33 + 1 + hostnameBytes.length)
118+
differentWAPublicKeyData.set(differentCompressedPubKeyBytes, 0)
119+
differentWAPublicKeyData.set(new Uint8Array([userPresenceByte]), 33)
120+
differentWAPublicKeyData.set(hostnameBytes, 34)
121+
const differentWAPublicKey = new PublicKey(KeyType.WA, new Bytes(differentWAPublicKeyData))
122+
123+
assert.isFalse(
124+
waSignature.verifyDigest(messageDigest, differentWAPublicKey),
125+
'WA signature should not verify with incorrect public key'
126+
)
127+
})
128+
129+
test('WA sign and recover', function () {
130+
const recoveredKey = waSignature.recoverDigest(messageDigest)
131+
assert.equal(recoveredKey.type, KeyType.WA, 'Recovered key type should be WA')
132+
133+
// Compare the core 33-byte compressed key part
134+
assert.deepEqual(
135+
recoveredKey.getCompressedKeyBytes(),
136+
waPublicKey.getCompressedKeyBytes(),
137+
'Recovered key compressed bytes should match original'
138+
)
139+
const expectedRecoveredData = new Bytes(testCompressedPubKeyBytes)
140+
const expectedRecoveredPublicKey = new PublicKey(KeyType.WA, expectedRecoveredData)
141+
142+
assert.isTrue(
143+
recoveredKey.data.equals(expectedRecoveredData),
144+
'Recovered PublicKey.data should only be the 33-byte key'
145+
)
146+
assert.equal(
147+
recoveredKey.toString(),
148+
expectedRecoveredPublicKey.toString(),
149+
'Recovered key string representation should match'
150+
)
151+
152+
const wrongMessageDigest = Checksum256.hash(Bytes.from('wrong message', 'utf8'))
153+
const recoveredKeyFromWrongDigest = waSignature.recoverDigest(wrongMessageDigest)
154+
assert.isFalse(
155+
recoveredKeyFromWrongDigest.toString() === waPublicKey.toString(),
156+
'Recovered key from wrong digest should not match'
157+
)
158+
})
159+
160+
test('PrivateKey generation for WA', function () {
161+
const waPrivKey = PrivateKey.generate(KeyType.WA)
162+
assert.equal(waPrivKey.type, KeyType.WA, 'Generated WA private key should have type WA')
163+
const waPubKeyFromGen = waPrivKey.toPublic()
164+
assert.equal(
165+
waPubKeyFromGen.type,
166+
KeyType.WA,
167+
'Public key from generated WA private key should have type WA'
168+
)
169+
})
170+
})

0 commit comments

Comments
 (0)