Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T

## Added

- Add standalone `verifySignature` and `verifySignatureAsync` utility functions that verify a signature against any supported public key type (Ed25519, Secp256k1, MultiEd25519, MultiKey, Keyless) without requiring callers to know the key type in advance
- Add MultiKey (K-of-N mixed key types) transfer example (`examples/typescript/multikey_transfer.ts`)
- Add MultiEd25519 (K-of-N Ed25519) transfer example (`examples/typescript/multi_ed25519_transfer.ts`)

Expand Down
1 change: 1 addition & 0 deletions src/core/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export * from "./signature";
export * from "./singleKey";
export * from "./types";
export * from "./deserializationUtils";
export * from "./verifySignature";
162 changes: 162 additions & 0 deletions src/core/crypto/verifySignature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import type { AptosConfig } from "../../api";
import { HexInput } from "../../types";
import { PublicKey } from "./publicKey";
import { Signature } from "./signature";
import { AnyPublicKey, AnySignature } from "./singleKey";
import { KeylessPublicKey } from "./keyless";
import { FederatedKeylessPublicKey } from "./federatedKeyless";

/**
* Arguments for the standalone {@link verifySignature} function.
*
* @param message - The message that was signed, as a hex string or Uint8Array.
* @param signature - The signature to verify.
* @param publicKey - The public key to verify against.
* @group Implementation
* @category Serialization
*/
export interface VerifyMessageSignatureArgs {
message: HexInput;
signature: Signature;
publicKey: PublicKey;
}

/**
* Arguments for the standalone {@link verifySignatureAsync} function.
*
* Required for signature types that depend on network state (e.g. Keyless).
*
* @param aptosConfig - The Aptos configuration for network calls.
* @param message - The message that was signed, as a hex string or Uint8Array.
* @param signature - The signature to verify.
* @param publicKey - The public key to verify against.
* @param options - Optional verification options.
* @group Implementation
* @category Serialization
*/
export interface VerifyMessageSignatureAsyncArgs extends VerifyMessageSignatureArgs {
aptosConfig: AptosConfig;
options?: { throwErrorWithReason?: boolean };
}

function wrapInAnySignature(signature: Signature): AnySignature {
return signature instanceof AnySignature ? signature : new AnySignature(signature);
}

/**
* Verifies a digital signature for a given message, automatically handling
* all supported public key and signature types.
*
* This function removes the need to know the specific key type (Ed25519, Secp256k1,
* MultiEd25519, MultiKey, etc.) before performing verification. Simply pass any
* public key, its corresponding signature, and the original message.
*
* **Note:** Keyless and FederatedKeyless signatures require network state for verification.
* Use {@link verifySignatureAsync} for those types, or when the key type is unknown and
* may be Keyless.
*
* @param args - The verification arguments.
* @param args.message - The original message that was signed.
* @param args.signature - The signature to verify.
* @param args.publicKey - The signer's public key.
* @returns `true` if the signature is valid, `false` otherwise.
* @throws Error if the public key is a Keyless variant (use `verifySignatureAsync` instead).
*
* @example
* ```typescript
* import { Ed25519PublicKey, Ed25519Signature, verifySignature } from "@aptos-labs/ts-sdk";
*
* const publicKey = new Ed25519PublicKey("0x...");
* const signature = new Ed25519Signature("0x...");
* const isValid = verifySignature({
* message: "hello world",
* signature,
* publicKey,
* });
* ```
*
* @example
* ```typescript
* import { AnyPublicKey, AnySignature, verifySignature } from "@aptos-labs/ts-sdk";
* import { Deserializer } from "@aptos-labs/ts-sdk";
*
* // Deserialize BCS-encoded public key and signature from wallet
* const publicKey = AnyPublicKey.deserialize(new Deserializer(publicKeyBytes));
* const signature = AnySignature.deserialize(new Deserializer(signatureBytes));
* const isValid = verifySignature({
* message: "hello world",
* signature,
* publicKey,
* });
* ```
* @group Implementation
* @category Serialization
*/
export function verifySignature(args: VerifyMessageSignatureArgs): boolean {
const { message, signature, publicKey } = args;

if (publicKey instanceof AnyPublicKey) {
if (publicKey.publicKey instanceof KeylessPublicKey || publicKey.publicKey instanceof FederatedKeylessPublicKey) {
throw new Error(
"Keyless signatures require async verification with an AptosConfig. Use verifySignatureAsync instead.",
);
}
return publicKey.verifySignature({ message, signature: wrapInAnySignature(signature) });
}

// For Ed25519PublicKey, Secp256k1PublicKey, MultiEd25519PublicKey, MultiKey, etc.
// the concrete verifySignature narrows the signature type but accepts the base at runtime.
return publicKey.verifySignature({ message, signature } as Parameters<typeof publicKey.verifySignature>[0]);
}

/**
* Verifies a digital signature for a given message, supporting all key types
* including those that require asynchronous network lookups (e.g. Keyless).
*
* This is the recommended verification function when the key type is unknown,
* as it handles every supported type including Keyless and FederatedKeyless.
*
* @param args - The verification arguments.
* @param args.aptosConfig - The Aptos network configuration (needed for Keyless verification).
* @param args.message - The original message that was signed.
* @param args.signature - The signature to verify.
* @param args.publicKey - The signer's public key.
* @param args.options - Optional settings for verification.
* @param args.options.throwErrorWithReason - If true, throws an error with the reason for failure
* instead of returning false.
* @returns `true` if the signature is valid, `false` otherwise.
*
* @example
* ```typescript
* import { Aptos, AptosConfig, Network, verifySignatureAsync } from "@aptos-labs/ts-sdk";
*
* const config = new AptosConfig({ network: Network.TESTNET });
*
* // Works with any public key type — Ed25519, Secp256k1, Keyless, MultiKey, etc.
* const isValid = await verifySignatureAsync({
* aptosConfig: config,
* message: "hello world",
* signature: anySignatureObject,
* publicKey: anyPublicKeyObject,
* });
* ```
* @group Implementation
* @category Serialization
*/
export async function verifySignatureAsync(args: VerifyMessageSignatureAsyncArgs): Promise<boolean> {
const { message, signature, publicKey, aptosConfig, options } = args;

if (publicKey instanceof AnyPublicKey) {
return publicKey.verifySignatureAsync({
aptosConfig,
message,
signature: wrapInAnySignature(signature),
options,
});
}

return publicKey.verifySignatureAsync({ aptosConfig, message, signature });
}
143 changes: 143 additions & 0 deletions tests/unit/verifySignature.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright © Aptos Foundation
// SPDX-License-Identifier: Apache-2.0

import {
Account,
AnyPublicKey,
Ed25519PrivateKey,
Ed25519PublicKey,
Ed25519Signature,
MultiEd25519Account,
MultiEd25519PublicKey,
MultiKey,
MultiKeyAccount,
Secp256k1PrivateKey,
Secp256k1PublicKey,
SigningSchemeInput,
} from "../../src";
import { verifySignature } from "../../src/core/crypto/verifySignature";
import { ed25519, secp256k1TestObject, singleSignerED25519 } from "./helper";

describe("verifySignature", () => {
const message = "hello world";

describe("Ed25519 (legacy)", () => {
it("verifies a valid signature", () => {
const publicKey = new Ed25519PublicKey(ed25519.publicKey);
const signature = new Ed25519Signature(ed25519.signatureHex);
expect(verifySignature({ message: ed25519.messageEncoded, signature, publicKey })).toBe(true);
});

it("rejects an invalid signature", () => {
const publicKey = new Ed25519PublicKey(ed25519.publicKey);
const wrongSig = new Ed25519Signature(
"0xc5de9e40ac00b371cd83b1c197fa5b665b7449b33cd3cdd305bb78222e06a671a49625ab9aea8a039d4bb70e275768084d62b094bc1b31964f2357b7c1af7e0a",
);
expect(verifySignature({ message: ed25519.messageEncoded, signature: wrongSig, publicKey })).toBe(false);
});

it("rejects a message that was not signed", () => {
const publicKey = new Ed25519PublicKey(ed25519.publicKey);
const signature = new Ed25519Signature(ed25519.signatureHex);
expect(verifySignature({ message: "wrong message", signature, publicKey })).toBe(false);
});
});

describe("Ed25519 (SingleKey / AnyPublicKey)", () => {
it("verifies a valid signature with AnySignature", () => {
const privateKey = new Ed25519PrivateKey(singleSignerED25519.privateKey);
const account = Account.fromPrivateKey({ privateKey, legacy: false });
const signature = account.sign(message);
expect(verifySignature({ message, signature, publicKey: account.publicKey })).toBe(true);
});

it("verifies a valid signature with raw Ed25519Signature auto-wrapped", () => {
const privateKey = new Ed25519PrivateKey(singleSignerED25519.privateKey);
const rawSignature = privateKey.sign(message);
const publicKey = new AnyPublicKey(privateKey.publicKey());
expect(verifySignature({ message, signature: rawSignature, publicKey })).toBe(true);
});

it("rejects an invalid message", () => {
const privateKey = new Ed25519PrivateKey(singleSignerED25519.privateKey);
const account = Account.fromPrivateKey({ privateKey, legacy: false });
const signature = account.sign(message);
expect(verifySignature({ message: "wrong message", signature, publicKey: account.publicKey })).toBe(false);
});
});

describe("Secp256k1 (SingleKey / AnyPublicKey)", () => {
it("verifies a valid signature", () => {
const privateKey = new Secp256k1PrivateKey(secp256k1TestObject.privateKey);
const account = Account.fromPrivateKey({ privateKey });
const signature = account.sign(message);
expect(verifySignature({ message, signature, publicKey: account.publicKey })).toBe(true);
});

it("verifies a valid signature with raw Secp256k1Signature auto-wrapped", () => {
const privateKey = new Secp256k1PrivateKey(secp256k1TestObject.privateKey);
const rawSignature = privateKey.sign(message);
const publicKey = new AnyPublicKey(new Secp256k1PublicKey(secp256k1TestObject.publicKey));
expect(verifySignature({ message, signature: rawSignature, publicKey })).toBe(true);
});

it("rejects a wrong message", () => {
const privateKey = new Secp256k1PrivateKey(secp256k1TestObject.privateKey);
const account = Account.fromPrivateKey({ privateKey });
const signature = account.sign(message);
expect(verifySignature({ message: "other message", signature, publicKey: account.publicKey })).toBe(false);
});
});

describe("MultiEd25519", () => {
const privateKey1 = Ed25519PrivateKey.generate();
const privateKey2 = Ed25519PrivateKey.generate();
const privateKey3 = Ed25519PrivateKey.generate();
const multiPublicKey = new MultiEd25519PublicKey({
publicKeys: [privateKey1.publicKey(), privateKey2.publicKey(), privateKey3.publicKey()],
threshold: 2,
});

it("verifies a valid multi-ed25519 signature", () => {
const account = new MultiEd25519Account({
publicKey: multiPublicKey,
signers: [privateKey1, privateKey3],
});
const signature = account.sign(message);
expect(verifySignature({ message, signature, publicKey: multiPublicKey })).toBe(true);
});
});

describe("MultiKey", () => {
const ed25519Account = Account.generate({ scheme: SigningSchemeInput.Ed25519, legacy: false });
const secp256k1Account = Account.generate({ scheme: SigningSchemeInput.Secp256k1Ecdsa });
const multiKey = new MultiKey({
publicKeys: [ed25519Account.publicKey, secp256k1Account.publicKey],
signaturesRequired: 2,
});

it("verifies a valid multi-key signature", () => {
const account = new MultiKeyAccount({
multiKey,
signers: [ed25519Account, secp256k1Account],
});
const signature = account.sign(message);
expect(verifySignature({ message, signature, publicKey: multiKey })).toBe(true);
});
});

describe("cross-key-type verification", () => {
it("returns false when signature does not match the public key", () => {
const ed25519Key = Ed25519PrivateKey.generate();
const otherKey = Ed25519PrivateKey.generate();
const signature = ed25519Key.sign(message);
expect(
verifySignature({
message,
signature,
publicKey: otherKey.publicKey(),
}),
).toBe(false);
});
});
});
Loading