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
23 changes: 23 additions & 0 deletions .changeset/thirty-wasps-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@onflow/transport-http": minor
"@onflow/fcl-core": minor
"@onflow/typedefs": minor
"@onflow/sdk": minor
---

Adds support for signature extension data introduced by [FLIP 264](https://github.com/onflow/flips/blob/main/protocol/20250203-webauthn-credential-support.md).

Users can now include signature extension data in their transactions by returning an additional `extensionData` property in their signing functions.

```typescript
const authz = (ix: Interaction) => {
return {
addr: "0x1234567890abcdef",
keyId: 0,
signingFunction: (signable: Signable) => ({
signature: "1234",
extensionData: "1234"
}),
}
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function normalizeCompositeSignature(
addr: sansPrefix(resp.addr || (resp as any).address),
signature: resp.signature || (resp as any).sig,
keyId: resp.keyId,
...(resp.extensionData ? {extensionData: resp.extensionData} : {}),
} as unknown as CompositeSignature
}

Expand Down
21 changes: 20 additions & 1 deletion packages/sdk/src/encode/encode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,21 @@ describe("encode transaction", () => {
"464c4f572d56302e302d7472616e73616374696f6e0000000000000000000000f87bb07472616e73616374696f6e207b2065786563757465207b206c6f67282248656c6c6f2c20576f726c64212229207d207dc0a0f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b2a880000000000000001040a880000000000000001d2880000000000000001880000000000000002",
"464c4f572d56302e302d7472616e73616374696f6e0000000000000000000000f8a2f87bb07472616e73616374696f6e207b2065786563757465207b206c6f67282248656c6c6f2c20576f726c64212229207d207dc0a0f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b2a880000000000000001040a880000000000000001d2880000000000000001880000000000000002e4e38004a0f7225388c1d69d57e6251c9fda50cbbf9e05131e5adb81e5aa0422402f048162",
],
[
"payload sig with extensionData appends 4th element",
buildTx({
payloadSigs: [
{
address: "01",
keyId: 4,
sig: "f7225388c1d69d57e6251c9fda50cbbf9e05131e5adb81e5aa0422402f048162",
extensionData: "abcd",
},
],
}),
"464c4f572d56302e302d7472616e73616374696f6e0000000000000000000000f872b07472616e73616374696f6e207b2065786563757465207b206c6f67282248656c6c6f2c20576f726c64212229207d207dc0a0f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b2a880000000000000001040a880000000000000001c9880000000000000001",
null,
],
]

// Test case format:
Expand Down Expand Up @@ -236,7 +251,11 @@ describe("encode transaction", () => {
test.each(validPayloadCases)(
"%s",
(_, tx, expectedPayload, expectedEnvelope) => {
expect(encodeTransactionEnvelope(tx)).toBe(expectedEnvelope)
if (expectedEnvelope != null) {
expect(encodeTransactionEnvelope(tx)).toBe(expectedEnvelope)
} else {
expect(() => encodeTransactionEnvelope(tx)).not.toThrow()
}
}
)

Expand Down
30 changes: 26 additions & 4 deletions packages/sdk/src/encode/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ const argumentToString = (arg: Record<string, any>) =>

const scriptBuffer = (script: string) => Buffer.from(script, "utf8")
const signatureBuffer = (signature: string) => Buffer.from(signature, "hex")
const extensionBuffer = (ext?: string) => {
if (ext == null) return undefined
return Buffer.from(ext, "hex")
}

const rlpEncode = (v: EncodeInput) => {
return encode(v).toString("hex")
Expand Down Expand Up @@ -186,6 +190,7 @@ const preparePayloadSignatures = (tx: Transaction) => {
signerIndex: signers.get(sansPrefix(sig.address)) || "",
keyId: sig.keyId,
sig: sig.sig,
sigExt: extensionBuffer(sig.extensionData),
}
})
.sort((a, b) => {
Expand All @@ -198,7 +203,9 @@ const preparePayloadSignatures = (tx: Transaction) => {
return 0
})
.map(sig => {
return [sig.signerIndex, sig.keyId, signatureBuffer(sig.sig)]
const base: any[] = [sig.signerIndex, sig.keyId, signatureBuffer(sig.sig)]
if (sig.sigExt != null) base.push(sig.sigExt)
return base
})
}

Expand Down Expand Up @@ -229,8 +236,13 @@ const prepareVoucher = (voucher: Voucher) => {

const prepareSigs = (sigs: Sig[]) => {
return sigs
.map(({address, keyId, sig}) => {
return {signerIndex: signers.get(sansPrefix(address)) || "", keyId, sig}
.map(({address, keyId, sig, extensionData}) => {
return {
signerIndex: signers.get(sansPrefix(address)) || "",
keyId,
sig,
sigExt: extensionBuffer(extensionData),
}
})
.sort((a, b) => {
if (a.signerIndex > b.signerIndex) return 1
Expand All @@ -241,7 +253,13 @@ const prepareVoucher = (voucher: Voucher) => {
return 0
})
.map(sig => {
return [sig.signerIndex, sig.keyId, signatureBuffer(sig.sig)]
const base: any[] = [
sig.signerIndex,
sig.keyId,
signatureBuffer(sig.sig),
]
if (sig.sigExt != null) base.push(sig.sigExt)
return base
})
}

Expand Down Expand Up @@ -319,6 +337,10 @@ interface Sig {
address: string
keyId: number | string
sig: string
// Optional signature extension data (e.g., WebAuthn clientDataJSON + authenticatorData)
// Propagated via "extensionData" when present by wallets/connectors.
// Not required for existing signatures; encoding remains identical when absent.
extensionData?: string
}

export interface TransactionProposalKey {
Expand Down
47 changes: 40 additions & 7 deletions packages/sdk/src/resolve/resolve-signatures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,15 @@ test("voucher in signable", async () => {
transaction``,
limit(156),
proposer(authz),
authorizations([authz]),
payer({
addr: "0x02",
signingFunction: () => ({signature: "123"}),
keyId: 0,
sequenceNum: 123,
}),
authorizations([authz as any]),
payer([
{
addr: "0x02",
signingFunction: () => ({signature: "123"}),
keyId: 0,
sequenceNum: 123,
},
]),
ref("123"),
])
)
Expand All @@ -93,6 +95,37 @@ test("voucher in signable", async () => {
})
})

test("extensionData is propagated into voucher", async () => {
const authz = {
addr: "0x01",
signingFunction: () => ({signature: "123", extensionData: "abcd"}),
keyId: 0,
sequenceNum: 123,
}
const ix = await resolve(
await build([
transaction``,
limit(156),
proposer(authz),
authorizations([authz as any]),
payer([
{
addr: "0x02",
signingFunction: () => ({signature: "123"}),
keyId: 0,
sequenceNum: 123,
},
]),
ref("123"),
])
)

const signable = buildSignable(ix.accounts[ix.proposer!], {} as any, ix)
expect(signable.voucher.payloadSigs[0]).toEqual(
expect.objectContaining({extensionData: "abcd"})
)
})

test("Golden Path", async () => {
const ix = await resolveSignatures(TRANSACTION)

Expand Down
21 changes: 15 additions & 6 deletions packages/sdk/src/resolve/resolve-signatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,17 @@ export async function resolveSignatures(ix: Interaction) {
let outsideSigners = findOutsideSigners(ix)
const outsidePayload = encodeOutsideMessage({
...prepForEncoding(ix),
payloadSigs: insideSigners.map(id => ({
address: ix.accounts[id].addr || "",
keyId: ix.accounts[id].keyId || 0,
sig: ix.accounts[id].signature || "",
})),
payloadSigs: insideSigners.map(id => {
const base: any = {
address: ix.accounts[id].addr || "",
keyId: ix.accounts[id].keyId || 0,
sig: ix.accounts[id].signature || "",
}
const acc = ix.accounts[id] as InteractionAccount
const ext = (acc as any).extensionData as string | null | undefined
if (ext != null) (base as any).extensionData = ext
return base
}),
})

// Promise.all could potentially break the flow if there are multiple outside signers trying to resolve at the same time
Expand All @@ -62,10 +68,13 @@ function fetchSignature(ix: Interaction, payload: string) {
return async function innerFetchSignature(id: string) {
const acct = ix.accounts[id]
if (acct.signature != null && acct.signature !== undefined) return
const {signature} = await acct.signingFunction(
const {signature, extensionData} = await acct.signingFunction(
buildSignable(acct, payload, ix)
)
ix.accounts[id].signature = signature
if (extensionData != null) {
ix.accounts[id].extensionData = extensionData
}
}
}

Expand Down
32 changes: 21 additions & 11 deletions packages/sdk/src/resolve/voucher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {withPrefix} from "@onflow/util-address"
import {Voucher, encodeTxIdFromVoucher} from "../encode/encode"
import {Interaction} from "@onflow/typedefs"
import {Interaction, InteractionAccount} from "@onflow/typedefs"

/**
* Identifies signers for the transaction payload (authorizers + proposer, excluding payer).
Expand Down Expand Up @@ -120,18 +120,28 @@ export const createSignableVoucher = (ix: Interaction) => {
}

const buildInsideSigners = () =>
findInsideSigners(ix).map(id => ({
address: withPrefix(ix.accounts[id].addr),
keyId: ix.accounts[id].keyId,
sig: ix.accounts[id].signature,
}))
findInsideSigners(ix).map(id => {
const base: any = {
address: withPrefix(ix.accounts[id].addr),
keyId: ix.accounts[id].keyId,
sig: ix.accounts[id].signature,
}
const ext = (ix.accounts[id] as InteractionAccount as any).extensionData
if (ext != null) (base as any).extensionData = ext
return base
})

const buildOutsideSigners = () =>
findOutsideSigners(ix).map(id => ({
address: withPrefix(ix.accounts[id].addr),
keyId: ix.accounts[id].keyId,
sig: ix.accounts[id].signature,
}))
findOutsideSigners(ix).map(id => {
const base: any = {
address: withPrefix(ix.accounts[id].addr),
keyId: ix.accounts[id].keyId,
sig: ix.accounts[id].signature,
}
const ext = (ix.accounts[id] as InteractionAccount as any).extensionData
if (ext != null) (base as any).extensionData = ext
return base
})

const proposalKey = ix.proposer
? {
Expand Down
18 changes: 18 additions & 0 deletions packages/sdk/src/wallet-utils/encode-signable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,22 @@ describe("encode signable", () => {
UnableToDetermineMessageEncodingTypeForSignerAddress
)
})

test("payload sig extensionData is forwarded for envelope encoding", () => {
const withExt = {
...VOUCHER,
payloadSigs: [
{address: "0x02", keyId: 1, sig: "123", extensionData: "abcd"},
],
}

const signable = {
...PAYER_SIGNABLE,
voucher: withExt,
}

// Should not throw when encoding envelope including an extension on payload sig
const message = encodeMessageFromSignable(signable as any, "0x01")
expect(typeof message).toBe("string")
})
})
14 changes: 10 additions & 4 deletions packages/sdk/src/wallet-utils/encode-signable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,16 @@ export const encodeMessageFromSignable = (
},
payer: sansPrefix(signable.voucher.payer),
authorizers: signable.voucher.authorizers.map(sansPrefix),
payloadSigs: signable.voucher.payloadSigs.map(ps => ({
...ps,
address: sansPrefix(ps.address),
})),
payloadSigs: signable.voucher.payloadSigs.map(ps => {
const base: any = {
...ps,
address: sansPrefix(ps.address),
}
if ((ps as any).extensionData != null) {
base.extensionData = (ps as any).extensionData
}
return base
}),
}

return isPayloadSigner
Expand Down
8 changes: 8 additions & 0 deletions packages/transport-http/src/send/send-get-transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ export async function sendGetTransaction(ix, context = {}, opts = {}) {
address: sig.address,
keyId: Number(sig.key_index),
signature: sig.signature,
...(sig.extension_data
? {
extensionData: context.Buffer.from(
sig.extension_data,
"base64"
).toString("hex"),
}
: {}),
})

const unwrapArg = arg =>
Expand Down
36 changes: 32 additions & 4 deletions packages/transport-http/src/send/send-get-transaction.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,22 @@ describe("Get Transaction", () => {
},
payer: "1654653399040a61",
authorizers: [],
payload_signatures: [],
envelope_signatures: [],
payload_signatures: [
{
address: "1654653399040a61",
key_index: "1",
signature: "001122",
extension_data: Buffer.from("abcd", "hex").toString("base64"),
},
],
envelope_signatures: [
{
address: "1654653399040a61",
key_index: "1",
signature: "aabbcc",
extension_data: Buffer.from("deadbeef", "hex").toString("base64"),
},
],
}

httpRequestMock.mockReturnValue(returnedTransaction)
Expand Down Expand Up @@ -69,8 +83,22 @@ describe("Get Transaction", () => {
},
payer: "1654653399040a61",
authorizers: [],
payloadSignatures: [],
envelopeSignatures: [],
payloadSignatures: [
{
address: "1654653399040a61",
keyId: 1,
signature: "001122",
extensionData: "abcd",
},
],
envelopeSignatures: [
{
address: "1654653399040a61",
keyId: 1,
signature: "aabbcc",
extensionData: "deadbeef",
},
],
})
})
})
Loading