Skip to content

Commit

Permalink
Merge pull request #107 from KaiCode2/feature/webauthn-p256creds
Browse files Browse the repository at this point in the history
Update Webauthn module to support hex/byte inputs + viem compatible types
  • Loading branch information
kopy-kat authored Dec 12, 2024
2 parents 4b4174f + e339ceb commit 8648b44
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 35 deletions.
5 changes: 5 additions & 0 deletions src/module/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export {
getWebAuthnValidator,
getWebauthnValidatorSignature,
getWebauthnValidatorMockSignature,
WebauthnValidatorSignature,
WebauthnCredential,
WebAuthnData,
WebauthnSignature,
WebauthnPublicKey,
} from './webauthn-validator'

export {
Expand Down
7 changes: 7 additions & 0 deletions src/module/webauthn-validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@ export {
getWebauthnValidatorSignature,
getWebauthnValidatorMockSignature,
} from './usage'
export type {
WebauthnValidatorSignature,
WebauthnCredential,
WebAuthnData,
WebauthnSignature,
PublicKey as WebauthnPublicKey,
} from './types'
32 changes: 23 additions & 9 deletions src/module/webauthn-validator/installation.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import { Address, encodeAbiParameters, keccak256, toHex } from 'viem'
import { encodeAbiParameters, keccak256, toHex } from 'viem'
import { Module } from '../types'
import { WEBAUTHN_VALIDATOR_ADDRESS } from './constants'
import { parsePublicKey } from './utils'
import { WebauthnCredential } from './types'

export type WebauthnCredential = {
pubKeyX: number
pubKeyY: number
authenticatorId: string
hook?: Address
}

export const getWebAuthnValidator = (
webAuthnCredential: WebauthnCredential,
): Module => {
let pubKeyX: bigint
let pubKeyY: bigint

// Distinguish between PublicKey and Hex / byte encoded public key
if (typeof webAuthnCredential.pubKey === 'string' || webAuthnCredential.pubKey instanceof Uint8Array) {
// It's a P256Credential
const { x, y, prefix } = parsePublicKey(webAuthnCredential.pubKey)
pubKeyX = x
pubKeyY = y
if (prefix && prefix !== 4) {
throw new Error('Only uncompressed public keys are supported')
}
} else {
// It's already a PublicKey
pubKeyX = webAuthnCredential.pubKey.x
pubKeyY = webAuthnCredential.pubKey.y
}

return {
address: WEBAUTHN_VALIDATOR_ADDRESS,
module: WEBAUTHN_VALIDATOR_ADDRESS,
Expand All @@ -37,8 +51,8 @@ export const getWebAuthnValidator = (
],
[
{
pubKeyX: BigInt(webAuthnCredential.pubKeyX),
pubKeyY: BigInt(webAuthnCredential.pubKeyY),
pubKeyX,
pubKeyY,
},
keccak256(toHex(webAuthnCredential.authenticatorId)),
],
Expand Down
30 changes: 30 additions & 0 deletions src/module/webauthn-validator/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Address, type Hex } from 'viem'

export type WebauthnCredential = {
pubKey: PublicKey | Hex | Uint8Array
authenticatorId: string
hook?: Address
}

export type WebauthnValidatorSignature = {
webauthn: WebAuthnData
signature: WebauthnSignature | Hex | Uint8Array
usePrecompiled: boolean
}

export type WebAuthnData = {
authenticatorData: Hex
clientDataJSON: string
typeIndex: number | bigint
}

export type WebauthnSignature = {
r: bigint
s: bigint
}

export type PublicKey = {
prefix?: number | undefined
x: bigint
y: bigint
}
36 changes: 20 additions & 16 deletions src/module/webauthn-validator/usage.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { encodeAbiParameters, Hex } from 'viem'
import { WebauthnValidatorSignature } from './types'
import { parseSignature } from './utils'


export const getWebauthnValidatorSignature = ({
authenticatorData,
clientDataJSON,
responseTypeLocation,
r,
s,
webauthn,
signature,
usePrecompiled,
}: {
authenticatorData: Hex
clientDataJSON: string
responseTypeLocation: bigint
r: bigint
s: bigint
usePrecompiled: boolean
}) => {
}: WebauthnValidatorSignature) => {
const { authenticatorData, clientDataJSON, typeIndex } = webauthn
let r: bigint
let s: bigint
if (typeof signature === 'string' || signature instanceof Uint8Array) {
const parsedSignature = parseSignature(signature)
r = parsedSignature.r
s = parsedSignature.s
} else {
r = signature.r
s = signature.s
}
return encodeAbiParameters(
[
{ type: 'bytes', name: 'authenticatorData' },
Expand Down Expand Up @@ -42,9 +46,9 @@ export const getWebauthnValidatorSignature = ({
[
authenticatorData,
clientDataJSON,
BigInt(responseTypeLocation),
BigInt(r),
BigInt(s),
typeof typeIndex === 'bigint' ? typeIndex : BigInt(typeIndex),
r,
s,
usePrecompiled,
],
)
Expand Down
26 changes: 26 additions & 0 deletions src/module/webauthn-validator/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { hexToBytes, bytesToHex, type Hex } from 'viem'
import { PublicKey, WebauthnSignature } from './types'

export function parsePublicKey(publicKey: Hex | Uint8Array): PublicKey {
const bytes =
typeof publicKey === 'string' ? hexToBytes(publicKey) : publicKey
const offset = bytes.length === 65 ? 1 : 0
const x = bytes.slice(offset, 32 + offset)
const y = bytes.slice(32 + offset, 64 + offset)
return {
prefix: bytes.length === 65 ? bytes[0] : undefined,
x: BigInt(bytesToHex(x)),
y: BigInt(bytesToHex(y)),
}
}

export function parseSignature(signature: Hex | Uint8Array): WebauthnSignature {
const bytes =
typeof signature === 'string' ? hexToBytes(signature) : signature
const r = bytes.slice(0, 32)
const s = bytes.slice(32, 64)
return {
r: BigInt(bytesToHex(r)),
s: BigInt(bytesToHex(s)),
}
}
130 changes: 120 additions & 10 deletions test/unit/module/webauthnValidator/webauthnValidator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,38 @@ import {
getWebAuthnValidator,
getWebauthnValidatorSignature,
} from 'src/module'
import { toHex } from 'viem'
import { toHex, concatHex, toBytes, type Hex } from 'viem'

function serializePublicKey(x: bigint, y: bigint, prefix?: number, asBytes: boolean = false): Hex | Uint8Array {
let hexKey = concatHex([toHex(x, { size: 32 }), toHex(y, { size: 32 })])
if (prefix) {
hexKey = concatHex([toHex(prefix, { size: 1 }), hexKey])
}
return asBytes ? toBytes(hexKey) : hexKey
}


function serializeSignature(r: bigint, s: bigint, asBytes: boolean = false): Hex | Uint8Array {
const hexSignature = concatHex([toHex(r, { size: 32 }), toHex(s, { size: 32 })])
return asBytes ? toBytes(hexSignature) : hexSignature
}

describe('Webauthn Validator Module', () => {
// Setup
const credentials = {
pubKeyX: 123,
pubKeyY: 456,
pubKey: {
x: 123n,
y: 456n,
},
authenticatorId: 'authenticatorId',
}

it('should get install webauthn validator module', async () => {
const installWebauthnValidatorModule = getWebAuthnValidator({
pubKeyX: credentials.pubKeyX,
pubKeyY: credentials.pubKeyY,
pubKey: {
x: credentials.pubKey.x,
y: credentials.pubKey.y,
},
authenticatorId: credentials.authenticatorId,
})

Expand All @@ -27,16 +45,108 @@ describe('Webauthn Validator Module', () => {
expect(installWebauthnValidatorModule.type).toEqual('validator')
})

it('should get install webauthn validator module with packed P256Credential', async () => {
const pubKey = serializePublicKey(credentials.pubKey.x, credentials.pubKey.y)
const installWebauthnValidatorModule = getWebAuthnValidator({
pubKey,
authenticatorId: credentials.authenticatorId,
})

expect(installWebauthnValidatorModule.module).toEqual(
WEBAUTHN_VALIDATOR_ADDRESS,
)
expect(installWebauthnValidatorModule.initData).toBeDefined()
expect(installWebauthnValidatorModule.type).toEqual('validator')
})

it('should get install webauthn validator module with packed P256Credential as Uint8Array', async () => {
const pubKey = serializePublicKey(credentials.pubKey.x, credentials.pubKey.y, undefined, true)
const installWebauthnValidatorModule = getWebAuthnValidator({
pubKey,
authenticatorId: credentials.authenticatorId,
})

expect(installWebauthnValidatorModule.module).toEqual(
WEBAUTHN_VALIDATOR_ADDRESS,
)
expect(installWebauthnValidatorModule.initData).toBeDefined()
expect(installWebauthnValidatorModule.type).toEqual('validator')
})

it('should get install webauthn validator module with prefix-packed P256Credential', async () => {
const pubKey = serializePublicKey(credentials.pubKey.x, credentials.pubKey.y, 4)
const installWebauthnValidatorModule = getWebAuthnValidator({
pubKey,
authenticatorId: credentials.authenticatorId,
})

expect(installWebauthnValidatorModule.module).toEqual(
WEBAUTHN_VALIDATOR_ADDRESS,
)
expect(installWebauthnValidatorModule.initData).toBeDefined()
expect(installWebauthnValidatorModule.type).toEqual('validator')
})

it('should throw error on install webauthn validator module with non-P256 pubkey', async () => {
const pubKey = serializePublicKey(credentials.pubKey.x, credentials.pubKey.y, 1)
expect(() =>
getWebAuthnValidator({
pubKey,
authenticatorId: credentials.authenticatorId,
}),
).toThrow('Only uncompressed public keys are supported')
})

it('should return encoded signature from webauthn data', async () => {
const signature = getWebauthnValidatorSignature({
const webauthn = {
authenticatorData: toHex('authenticatorData'),
clientDataJSON: 'clientDataHash',
typeIndex: 0,
}
const signature = {
r: 10n,
s: 5n,
}
const validatorSignature = getWebauthnValidatorSignature({
webauthn,
signature,
usePrecompiled: true,
})

expect(validatorSignature).toBeTruthy()
})

it('should return encoded signature from webauthn data with hex signature', async () => {
const webauthn = {
authenticatorData: toHex('authenticatorData'),
clientDataJSON: 'clientDataHash',
typeIndex: 0,
}
const signature = serializeSignature(10n, 5n)

const validatorSignature = getWebauthnValidatorSignature({
webauthn,
signature,
usePrecompiled: true,
})

expect(validatorSignature).toBeTruthy()
})

it('should return encoded signature from webauthn data with bytes signature', async () => {
const webauthn = {
authenticatorData: toHex('authenticatorData'),
clientDataJSON: 'clientDataHash',
responseTypeLocation: BigInt(0),
r: BigInt(10),
s: BigInt(5),
typeIndex: 0,
}
const signature = serializeSignature(10n, 5n, true)

const validatorSignature = getWebauthnValidatorSignature({
webauthn,
signature,
usePrecompiled: true,
})

expect(signature).toBeTruthy()
expect(validatorSignature).toBeTruthy()
})
})

0 comments on commit 8648b44

Please sign in to comment.