Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/iso-webauthn-varsig/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@le-space/iso-webauthn-varsig",
"type": "module",
"version": "0.1.0",
"version": "0.2.0",
"description": "WebAuthn varsig encoding/decoding helpers.",
"author": "Nico Krause <post@nicokrause.com> (nicokrause.com)",
"license": "MIT",
Expand Down
42 changes: 42 additions & 0 deletions packages/iso-webauthn-varsig/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,36 @@

WebAuthn varsig encoding/decoding helpers for Ed25519 and P-256.

Implements the **non-recursive** WebAuthn varsig layout per [Gozala's suggestion](https://github.com/ChainAgnostic/varsig/pull/11#discussion_r2766471176) on the ChainAgnostic varsig spec.

## Wire Format

All multi-byte integers are unsigned varint-encoded.

```abnf
webauthn-varsig = webauthn-varsig-header
client-data-length client-data-json
authenticator-data-length authenticator-data
signature-hash-algorithm encoding-info signature-bytes

webauthn-varsig-header = varsig-prefix varsig-version inner-algorithm curve webauthn-marker
varsig-prefix = %x34
varsig-version = %x01
inner-algorithm = %xED / %xEC ; EdDSA / ECDSA
curve = %xED01 / %x1200 ; Ed25519 / P-256 (multicodec)
webauthn-marker = %x300001 ; private-use multicodec

client-data-length = 1*unsigned-varint
client-data-json = *OCTET
authenticator-data-length = 1*unsigned-varint
authenticator-data = *OCTET
signature-hash-algorithm = 1*unsigned-varint ; multihash code (default 0x12 = SHA-256)
encoding-info = 1*unsigned-varint ; payload encoding (default 0x5f = raw)
signature-bytes = *OCTET ; raw WebAuthn signature
```

This layout avoids recursion (no nested varsig-body) and aligns with the [dialog-db](https://github.com/dialog-db/dialog-db) Go/Rust reference implementation.

## Usage

```js
Expand All @@ -18,10 +48,21 @@ const assertion = {
signature: new Uint8Array([/* ... */]),
}

// Encode with defaults (SHA-256 hash, raw encoding)
const varsig = encodeWebAuthnVarsigV1(assertion, 'Ed25519')

// Or with explicit signature metadata
const varsig2 = encodeWebAuthnVarsigV1(assertion, 'P-256', {
signatureHashAlgorithm: 0x12, // SHA-256
encodingInfo: 0x5f, // raw
})

const decoded = decodeWebAuthnVarsigV1(varsig)
const clientData = parseClientDataJSON(decoded.clientDataJSON)

console.log(decoded.signatureHashAlgorithm) // 0x12
console.log(decoded.encodingInfo) // 0x5f

const result = await verifyWebAuthnAssertion(decoded, {
expectedOrigin: clientData.origin,
expectedRpId: new URL(clientData.origin).hostname,
Expand All @@ -34,3 +75,4 @@ console.log(result.valid)
## Notes

- Ed25519 WebAuthn credentials are not supported on all platforms. If your authenticator does not return an Ed25519 key, you must fall back to P-256.
- **Breaking change in v0.2.0**: The wire format changed from v0.1.0. Encoded bytes from the old format are not compatible with this version. See [issue #1](https://github.com/NiKrause/iso-repo/issues/1) for details.
69 changes: 26 additions & 43 deletions packages/iso-webauthn-varsig/src/decoder.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { varintDecode } from './utils.js'
import {
VARSIG_PREFIX,
VARSIG_VERSION,
INNER_EDDSA,
INNER_ECDSA,
CURVE_ED25519,
CURVE_P256,
MULTIHASH_SHA256,
MULTIHASH_SHA256_LEN,
INNER_ECDSA,
INNER_EDDSA,
VARSIG_PREFIX,
VARSIG_VERSION,
WEBAUTHN_WRAPPER,
PAYLOAD_ENCODING_RAW,
} from './multicodec.js'
import { varintDecode } from './utils.js'

/**
* @typedef {import('./types').DecodedVarsigV1} DecodedVarsigV1
Expand All @@ -19,14 +16,12 @@ import {
*/

/**
* Decode a WebAuthn varsig v1 into its components.
* Decode a WebAuthn varsig v1 (non-recursive layout) into its components.
*
* Format:
* - prefix (0x34)
* - version (0x01)
* - signature-algorithm metadata (varints)
* - payload-encoding metadata (varint)
* - assertion serialization
* Wire format:
* header: prefix version innerAlgorithm curve webauthnMarker
* body: clientDataLen clientDataJSON authDataLen authenticatorData
* signatureHashAlgorithm encodingInfo signatureBytes
*
* @param {Uint8Array} varsig
* @returns {DecodedVarsigV1}
Expand All @@ -40,19 +35,14 @@ export function decodeWebAuthnVarsigV1(varsig) {
throw new Error('Unsupported varsig header')
}

// --- header: 3 varints ---
let offset = 2
const [innerAlgorithm, innerAlgorithmLen] = varintDecode(varsig, offset)
offset += innerAlgorithmLen
const [curve, curveLen] = varintDecode(varsig, offset)
offset += curveLen
const [multihashCode, multihashCodeLen] = varintDecode(varsig, offset)
offset += multihashCodeLen
const [multihashLength, multihashLengthLen] = varintDecode(varsig, offset)
offset += multihashLengthLen
const [webauthnMarker, markerLen] = varintDecode(varsig, offset)
offset += markerLen
const [payloadEncoding, payloadEncodingLen] = varintDecode(varsig, offset)
offset += payloadEncodingLen

/** @type {SignatureAlgorithm | null} */
const algorithm =
Expand All @@ -76,22 +66,21 @@ export function decodeWebAuthnVarsigV1(varsig) {
throw new Error(`Unexpected P-256 curve code: 0x${curve.toString(16)}`)
}

if (multihashCode !== MULTIHASH_SHA256) {
throw new Error('Unsupported multihash header')
}

if (multihashLength !== MULTIHASH_SHA256_LEN) {
throw new Error('Unsupported multihash header length')
}

if (webauthnMarker !== WEBAUTHN_WRAPPER) {
throw new Error('Missing WebAuthn extension marker')
}

if (payloadEncoding !== PAYLOAD_ENCODING_RAW) {
throw new Error('Unsupported payload encoding')
// --- body: clientData, authData, signature metadata, signature ---
const [clientDataLen, clientDataLenLen] = varintDecode(varsig, offset)
offset += clientDataLenLen

if (offset + clientDataLen > varsig.length) {
throw new Error('Invalid clientDataJSON length')
}

const clientDataJSON = varsig.slice(offset, offset + clientDataLen)
offset += clientDataLen

const [authDataLen, authDataLenLen] = varintDecode(varsig, offset)
offset += authDataLenLen

Expand All @@ -102,15 +91,10 @@ export function decodeWebAuthnVarsigV1(varsig) {
const authenticatorData = varsig.slice(offset, offset + authDataLen)
offset += authDataLen

const [clientDataLen, clientDataLenLen] = varintDecode(varsig, offset)
offset += clientDataLenLen

if (offset + clientDataLen > varsig.length) {
throw new Error('Invalid clientDataJSON length')
}

const clientDataJSON = varsig.slice(offset, offset + clientDataLen)
offset += clientDataLen
const [signatureHashAlgorithm, sigHashLen] = varintDecode(varsig, offset)
offset += sigHashLen
const [encodingInfo, encInfoLen] = varintDecode(varsig, offset)
offset += encInfoLen

const signature = varsig.slice(offset)
if (signature.length === 0) {
Expand All @@ -121,12 +105,11 @@ export function decodeWebAuthnVarsigV1(varsig) {
algorithm,
innerAlgorithm,
curve,
multihashCode,
multihashLength,
webauthnMarker,
payloadEncoding,
authenticatorData,
clientDataJSON,
signatureHashAlgorithm,
encodingInfo,
signature,
}
}
Expand Down
47 changes: 27 additions & 20 deletions packages/iso-webauthn-varsig/src/encoder.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,63 @@
import {
VARSIG_PREFIX,
VARSIG_VERSION,
INNER_EDDSA,
INNER_ECDSA,
CURVE_ED25519,
CURVE_P256,
INNER_ECDSA,
INNER_EDDSA,
MULTIHASH_SHA256,
MULTIHASH_SHA256_LEN,
PAYLOAD_ENCODING_RAW,
VARSIG_PREFIX,
VARSIG_VERSION,
WEBAUTHN_WRAPPER,
} from './multicodec.js'
import { concat, varintEncode } from './utils.js'

/**
* @typedef {import('./types').WebAuthnAssertion} WebAuthnAssertion
* @typedef {import('./types').SignatureAlgorithm} SignatureAlgorithm
* @typedef {import('./types').EncodeOptions} EncodeOptions
*/

/**
* Encode a WebAuthn assertion as varsig v1.
* Encode a WebAuthn assertion as varsig v1 (non-recursive layout).
*
* Format:
* - varsig prefix (0x34)
* - varsig version (0x01)
* - signature algorithm metadata (varints)
* - payload encoding metadata (varint)
* - assertion serialization
* Wire format:
* header: prefix version innerAlgorithm curve webauthnMarker
* body: clientDataLen clientDataJSON authDataLen authenticatorData
* signatureHashAlgorithm encodingInfo signatureBytes
*
* @param {WebAuthnAssertion} assertion
* @param {SignatureAlgorithm} [algorithm]
* @param {EncodeOptions} [options]
*/
export function encodeWebAuthnVarsigV1(assertion, algorithm = 'Ed25519') {
export function encodeWebAuthnVarsigV1(
assertion,
algorithm = 'Ed25519',
options = {}
) {
const { authenticatorData, clientDataJSON, signature } = assertion

validateWebAuthnAssertion(assertion)

const innerAlgorithm = algorithm === 'Ed25519' ? INNER_EDDSA : INNER_ECDSA
const curve = algorithm === 'Ed25519' ? CURVE_ED25519 : CURVE_P256
const sigHashAlg = options.signatureHashAlgorithm ?? MULTIHASH_SHA256
const encInfo = options.encodingInfo ?? PAYLOAD_ENCODING_RAW

const header = new Uint8Array([VARSIG_PREFIX, VARSIG_VERSION])
const authDataLenBytes = varintEncode(authenticatorData.length)
const clientDataLenBytes = varintEncode(clientDataJSON.length)
const authDataLenBytes = varintEncode(authenticatorData.length)

return concat([
header,
varintEncode(innerAlgorithm),
varintEncode(curve),
varintEncode(MULTIHASH_SHA256),
varintEncode(MULTIHASH_SHA256_LEN),
varintEncode(WEBAUTHN_WRAPPER),
varintEncode(PAYLOAD_ENCODING_RAW),
authDataLenBytes,
authenticatorData,
clientDataLenBytes,
clientDataJSON,
authDataLenBytes,
authenticatorData,
varintEncode(sigHashAlg),
varintEncode(encInfo),
signature,
])
}
Expand All @@ -64,7 +68,10 @@ export function encodeWebAuthnVarsigV1(assertion, algorithm = 'Ed25519') {
* @param {WebAuthnAssertion} assertion
*/
export function validateWebAuthnAssertion(assertion) {
if (!assertion.authenticatorData || assertion.authenticatorData.length === 0) {
if (
!assertion.authenticatorData ||
assertion.authenticatorData.length === 0
) {
throw new Error('authenticatorData is required and cannot be empty')
}

Expand Down
51 changes: 24 additions & 27 deletions packages/iso-webauthn-varsig/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,40 @@
* WebAuthn Varsig - Public API.
*/

export { decodeWebAuthnVarsigV1, parseClientDataJSON } from './decoder.js'

export { encodeWebAuthnVarsigV1, validateWebAuthnAssertion } from './encoder.js'
export {
WEBAUTHN_ED25519,
WEBAUTHN_P256,
ED25519_PUB,
P256_PUB,
VARSIG_PREFIX,
VARSIG_VERSION,
INNER_EDDSA,
INNER_ECDSA,
ALGORITHM_TO_MULTICODEC,
CURVE_ED25519,
CURVE_P256,
ED25519_PUB,
getAlgorithm,
INNER_ECDSA,
INNER_EDDSA,
isWebAuthnMulticodec,
MULTICODEC_TO_ALGORITHM,
MULTIHASH_SHA256,
MULTIHASH_SHA256_LEN,
P256_PUB,
PAYLOAD_ENCODING_RAW,
VARSIG_PREFIX,
VARSIG_VERSION,
WEBAUTHN_ED25519,
WEBAUTHN_P256,
WEBAUTHN_WRAPPER,
ALGORITHM_TO_MULTICODEC,
MULTICODEC_TO_ALGORITHM,
isWebAuthnMulticodec,
getAlgorithm,
} from './multicodec.js'

export { encodeWebAuthnVarsigV1, validateWebAuthnAssertion } from './encoder.js'

export { decodeWebAuthnVarsigV1, parseClientDataJSON } from './decoder.js'

export {
verifyWebAuthnAssertion,
base64urlToBytes,
bytesEqual,
bytesToBase64url,
concat,
varintDecode,
varintEncode,
} from './utils.js'
export {
reconstructSignedData,
verifyEd25519Signature,
verifyP256Signature,
verifyWebAuthnAssertion,
} from './verifier.js'

export {
varintEncode,
varintDecode,
concat,
base64urlToBytes,
bytesToBase64url,
bytesEqual,
} from './utils.js'
6 changes: 1 addition & 5 deletions packages/iso-webauthn-varsig/src/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@
* @param {{ userPresent?: boolean, userVerified?: boolean, signCount?: number }} [options]
*/
export function createMockAuthenticatorData(options = {}) {
const {
userPresent = true,
userVerified = true,
signCount = 1,
} = options
const { userPresent = true, userVerified = true, signCount = 1 } = options

const rpIdHash = new Uint8Array(32)
for (let i = 0; i < 32; i++) {
Expand Down
Loading