|
| 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