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
5 changes: 5 additions & 0 deletions .changeset/access-key-verify-hash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

**viem/tempo:** Added access key signature verification support to `verifyHash` via `mode: 'allowAccessKey'`.
2 changes: 1 addition & 1 deletion src/actions/public/verifyHash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export type VerifyHashParameters = Pick<
/** @deprecated use `erc6492VerifierAddress` instead. */
universalSignatureVerifierAddress?: Address | undefined
/** Chooses which verification path to try first before falling back. */
mode?: 'auto' | 'eoa' | undefined
mode?: 'auto' | 'eoa' | (string & {}) | undefined
} & OneOf<{ factory: Address; factoryData: Hex } | {}>

export type VerifyHashReturnType = boolean
Expand Down
12 changes: 6 additions & 6 deletions src/tempo/Account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ describe('fromP256', () => {
hash: '0xdeadbeef',
})
expect(signature).toMatchInlineSnapshot(
`"0x01daab749a3dea3f76c52ff0cfc86f0d433ecaf4d20f2ea327042bf5c15bccf847098dc3591fc68bf94d8db6d16cf326808dbf0f44d8e8373e8a7fcaf39b38281020fe09fa1af47a6b3b4e973040f0588a1c2c96df1ce78b10e50903566ad9b7d87ffe0b281b616196c2ccdb64cd51230d8dc1f1d258ca7e8cb33a63cf8c812240007777777777777777777777777777777777777777777777777777777777777777"`,
`"0x01daab749a3dea3f76c52ff0cfc86f0d433ecaf4d20f2ea327042bf5c15bccf847098dc3591fc68bf94d8db6d16cf326808dbf0f44d8e8373e8a7fcaf39b38281020fe09fa1af47a6b3b4e973040f0588a1c2c96df1ce78b10e50903566ad9b7d87ffe0b281b616196c2ccdb64cd51230d8dc1f1d258ca7e8cb33a63cf8c81224000"`,
)

expect(
Expand Down Expand Up @@ -182,7 +182,7 @@ describe('fromHeadlessWebAuthn', () => {
hash: '0xdeadbeef',
})
expect(signature).toMatchInlineSnapshot(
`"0x0249960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a223371322d3777222c226f726967696e223a22687474703a2f2f6c6f63616c686f7374222c2263726f73734f726967696e223a66616c73657d1b3346991a9ad1498e401dc0448e93d1bde113778d442f5bcafc44925cf3121961e9b1c21b054e54fe6c2eec0cd310c8535b7e7dd1f7dd7bf749e6d78154b48120fe09fa1af47a6b3b4e973040f0588a1c2c96df1ce78b10e50903566ad9b7d87ffe0b281b616196c2ccdb64cd51230d8dc1f1d258ca7e8cb33a63cf8c8122407777777777777777777777777777777777777777777777777777777777777777"`,
`"0x0249960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a223371322d3777222c226f726967696e223a22687474703a2f2f6c6f63616c686f7374222c2263726f73734f726967696e223a66616c73657d1b3346991a9ad1498e401dc0448e93d1bde113778d442f5bcafc44925cf3121961e9b1c21b054e54fe6c2eec0cd310c8535b7e7dd1f7dd7bf749e6d78154b48120fe09fa1af47a6b3b4e973040f0588a1c2c96df1ce78b10e50903566ad9b7d87ffe0b281b616196c2ccdb64cd51230d8dc1f1d258ca7e8cb33a63cf8c812240"`,
)

expect(
Expand Down Expand Up @@ -275,7 +275,7 @@ describe('signMessage', () => {
const account = Account.fromP256(privateKey_p256)
const signature = await account.signMessage({ message: 'hello world' })
expect(signature).toMatchInlineSnapshot(
`"0x019e8afd9a5a2a6034a89d1dc09d6351eb83a3bcf3ee55e55973959c3b90b8103726f0de082476045ec872c42efb27ef2159a848df1d5c8326f3ad14dcfd00653220fe09fa1af47a6b3b4e973040f0588a1c2c96df1ce78b10e50903566ad9b7d87ffe0b281b616196c2ccdb64cd51230d8dc1f1d258ca7e8cb33a63cf8c812240007777777777777777777777777777777777777777777777777777777777777777"`,
`"0x019e8afd9a5a2a6034a89d1dc09d6351eb83a3bcf3ee55e55973959c3b90b8103726f0de082476045ec872c42efb27ef2159a848df1d5c8326f3ad14dcfd00653220fe09fa1af47a6b3b4e973040f0588a1c2c96df1ce78b10e50903566ad9b7d87ffe0b281b616196c2ccdb64cd51230d8dc1f1d258ca7e8cb33a63cf8c81224000"`,
)

expect(
Expand All @@ -294,7 +294,7 @@ describe('signMessage', () => {
})
const signature = await account.signMessage({ message: 'hello world' })
expect(signature).toMatchInlineSnapshot(
`"0x0249960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a223265756862744473726b4d72636634416a4a6a4d687975307a43464e4d69436a627a5a544a732d41665767222c226f726967696e223a22687474703a2f2f6c6f63616c686f7374222c2263726f73734f726967696e223a66616c73657d465aa5cd2f5155792a3d5585c059bfacbca733664436aac190c6d2f6c8cd76156a519c9ece3e757a075423f12f87b0dbbb536e158e4b19e6ac94bcc59330843720fe09fa1af47a6b3b4e973040f0588a1c2c96df1ce78b10e50903566ad9b7d87ffe0b281b616196c2ccdb64cd51230d8dc1f1d258ca7e8cb33a63cf8c8122407777777777777777777777777777777777777777777777777777777777777777"`,
`"0x0249960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a223265756862744473726b4d72636634416a4a6a4d687975307a43464e4d69436a627a5a544a732d41665767222c226f726967696e223a22687474703a2f2f6c6f63616c686f7374222c2263726f73734f726967696e223a66616c73657d465aa5cd2f5155792a3d5585c059bfacbca733664436aac190c6d2f6c8cd76156a519c9ece3e757a075423f12f87b0dbbb536e158e4b19e6ac94bcc59330843720fe09fa1af47a6b3b4e973040f0588a1c2c96df1ce78b10e50903566ad9b7d87ffe0b281b616196c2ccdb64cd51230d8dc1f1d258ca7e8cb33a63cf8c812240"`,
)

expect(
Expand Down Expand Up @@ -404,7 +404,7 @@ describe('signTypedData', () => {
message: { value: 'hello' },
})
expect(signature).toMatchInlineSnapshot(
`"0x01d0e4eba4b8715e90b17d6fae63521ec4f51e119c4f3857ed04120bebc19f61d411606f5b07163c071f4c5e553b9b88ec5d8e0a31c9c3a7472af0b4c3e1bd4c2420fe09fa1af47a6b3b4e973040f0588a1c2c96df1ce78b10e50903566ad9b7d87ffe0b281b616196c2ccdb64cd51230d8dc1f1d258ca7e8cb33a63cf8c812240007777777777777777777777777777777777777777777777777777777777777777"`,
`"0x01d0e4eba4b8715e90b17d6fae63521ec4f51e119c4f3857ed04120bebc19f61d411606f5b07163c071f4c5e553b9b88ec5d8e0a31c9c3a7472af0b4c3e1bd4c2420fe09fa1af47a6b3b4e973040f0588a1c2c96df1ce78b10e50903566ad9b7d87ffe0b281b616196c2ccdb64cd51230d8dc1f1d258ca7e8cb33a63cf8c81224000"`,
)

expect(
Expand Down Expand Up @@ -443,7 +443,7 @@ describe('signTypedData', () => {
message: { value: 'hello' },
})
expect(signature).toMatchInlineSnapshot(
`"0x0249960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2255444b505432495376767437546f35656436695a70346869485f364c4e6d3570446851646e7878654b5741222c226f726967696e223a22687474703a2f2f6c6f63616c686f7374222c2263726f73734f726967696e223a66616c73657d497b47c010ed378fca3ffba3939edce1a61d994fa0e83c473ef976c9527492f554003f6e898d2b1986aeb8e1731d622d6501f65d09bdefb70d2f72849580ddb020fe09fa1af47a6b3b4e973040f0588a1c2c96df1ce78b10e50903566ad9b7d87ffe0b281b616196c2ccdb64cd51230d8dc1f1d258ca7e8cb33a63cf8c8122407777777777777777777777777777777777777777777777777777777777777777"`,
`"0x0249960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d976305000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2255444b505432495376767437546f35656436695a70346869485f364c4e6d3570446851646e7878654b5741222c226f726967696e223a22687474703a2f2f6c6f63616c686f7374222c2263726f73734f726967696e223a66616c73657d497b47c010ed378fca3ffba3939edce1a61d994fa0e83c473ef976c9527492f554003f6e898d2b1986aeb8e1731d622d6501f65d09bdefb70d2f72849580ddb020fe09fa1af47a6b3b4e973040f0588a1c2c96df1ce78b10e50903566ad9b7d87ffe0b281b616196c2ccdb64cd51230d8dc1f1d258ca7e8cb33a63cf8c812240"`,
)

expect(
Expand Down
5 changes: 1 addition & 4 deletions src/tempo/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,10 +444,7 @@ function fromBase(parameters: fromBase.Parameters): Account_base {
version: internal_version,
}),
)
// Don't need to append magic bytes to secp256k1 signatures as they are
// backwards compatible with existing verification logic.
if (keyType === 'secp256k1') return signature
return Hex.concat(signature, SignatureEnvelope.magicBytes)
return signature
}

return {
Expand Down
78 changes: 78 additions & 0 deletions src/tempo/chainConfig.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, test } from 'vitest'
import { accounts, feeToken, getClient } from '~test/tempo/config.js'
import { generatePrivateKey } from '../accounts/generatePrivateKey.js'
import {
getTransaction,
getTransactionReceipt,
Expand All @@ -12,6 +13,7 @@ import { mainnet, tempoLocalnet } from '../chains/index.js'
import { createClient, http } from '../index.js'
import { defineChain } from '../utils/chain/defineChain.js'
import { hashMessage } from '../utils/index.js'
import * as accessKeyActions from './actions/accessKey.js'
import { Account, P256, WebCryptoP256 } from './index.js'

const client = getClient({
Expand Down Expand Up @@ -361,6 +363,82 @@ describe('verifyHash', () => {
).toBe(false)
})

test('accessKey: valid signature', async () => {
const rootAccount = accounts.at(0)!
const accessKey = Account.fromP256(generatePrivateKey(), {
access: rootAccount,
})

await accessKeyActions.authorizeSync(client, {
accessKey,
expiry: Math.floor((Date.now() + 30_000) / 1000),
})

const hash = hashMessage('hello world')
const signature = await accessKey.sign({ hash })

expect(
await verifyHash(client, {
address: accessKey.address,
hash,
signature,
mode: 'allowAccessKey',
}),
).toBe(true)
})

test('accessKey: invalid signature returns false', async () => {
const rootAccount = accounts.at(0)!
const accessKey = Account.fromP256(generatePrivateKey(), {
access: rootAccount,
})

await accessKeyActions.authorizeSync(client, {
accessKey,
expiry: Math.floor((Date.now() + 30_000) / 1000),
})

const hash = hashMessage('hello world')
const wrongHash = hashMessage('wrong message')
const signature = await accessKey.sign({ hash })

expect(
await verifyHash(client, {
address: accessKey.address,
hash: wrongHash,
signature,
mode: 'allowAccessKey',
}),
).toBe(false)
})

test('accessKey: revoked key returns false', async () => {
const rootAccount = accounts.at(0)!
const accessKey = Account.fromP256(generatePrivateKey(), {
access: rootAccount,
})

await accessKeyActions.authorizeSync(client, {
accessKey,
expiry: Math.floor((Date.now() + 30_000) / 1000),
})

const hash = hashMessage('hello world')
const signature = await accessKey.sign({ hash })

// Revoke the key
await accessKeyActions.revokeSync(client, { accessKey })

expect(
await verifyHash(client, {
address: accessKey.address,
hash,
signature,
mode: 'allowAccessKey',
}),
).toBe(false)
})

test('behavior: non-tempo chain', async () => {
const privateKey = P256.randomPrivateKey()
const account = Account.fromP256(privateKey)
Expand Down
58 changes: 49 additions & 9 deletions src/tempo/chainConfig.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import * as Address from 'ox/Address'
import * as Hex from 'ox/Hex'
import * as PublicKey from 'ox/PublicKey'
import { SignatureEnvelope, type TokenId } from 'ox/tempo'
import { getCode } from '../actions/public/getCode.js'
import { verifyHash } from '../actions/public/verifyHash.js'
Expand All @@ -8,8 +11,10 @@ import { defineTransaction } from '../utils/formatters/transaction.js'
import { defineTransactionReceipt } from '../utils/formatters/transactionReceipt.js'
import { defineTransactionRequest } from '../utils/formatters/transactionRequest.js'
import { getAction } from '../utils/getAction.js'
import { keccak256 } from '../utils/hash/keccak256.js'
import type { SerializeTransactionFn } from '../utils/transaction/serializeTransaction.js'
import type { Account } from './Account.js'
import { getMetadata } from './actions/accessKey.js'
import * as Formatters from './Formatters.js'
import * as Concurrent from './internal/concurrent.js'
import * as Transaction from './Transaction.js'
Expand Down Expand Up @@ -89,18 +94,53 @@ export const chainConfig = {
Transaction.serialize(transaction, signature)) as SerializeTransactionFn,
},
async verifyHash(client, parameters) {
const { address, hash, signature } = parameters
const { address, hash, signature, mode } = parameters

const envelope = (() => {
if (typeof signature !== 'string') return
try {
return SignatureEnvelope.deserialize(signature)
} catch {
return undefined
}
})()

// `verifyHash` supports "signature envelopes" (a Tempo proposal) to natively verify arbitrary
// envelope-compatible (WebAuthn, P256, etc.) signatures.
// We can directly verify stateless, non-keychain signature envelopes without a
// network request to the chain.
if (
typeof signature === 'string' &&
signature.endsWith(SignatureEnvelope.magicBytes.slice(2))
) {
const envelope = SignatureEnvelope.deserialize(signature)
if (envelope.type !== 'keychain') {
if (envelope) {
// Access key (keychain) signature verification: check the key is
// authorized, not expired, and not revoked on the AccountKeychain.
if (envelope?.type === 'keychain' && mode === 'allowAccessKey') {
const accessKeyAddress = Address.fromPublicKey(
PublicKey.from(envelope.inner.publicKey as PublicKey.PublicKey),
)

const keyInfo = await getMetadata(client, {
account: address,
accessKey: accessKeyAddress,
blockNumber: parameters.blockNumber,
blockTag: parameters.blockTag,
} as never)

if (keyInfo.isRevoked) return false
if (keyInfo.expiry <= BigInt(Math.floor(Date.now() / 1000)))
return false

// For v2 keychain envelopes, the inner signature signs
// keccak256(0x04 || hash || userAddress).
const innerPayload =
envelope.version === 'v2'
? keccak256(Hex.concat('0x04', hash, address))
: hash
return SignatureEnvelope.verify(envelope.inner, {
address: accessKeyAddress,
payload: innerPayload,
})
}

// Stateless, non-keychain signature envelopes (P256, WebAuthn) can be
// verified directly without a network request.
if (envelope.type === 'p256' || envelope.type === 'webAuthn') {
const code = await getCode(client, {
address,
blockNumber: parameters.blockNumber,
Expand Down
Loading