Sitelet https://github.com/ChainSafe/canton-snap/commit/a8394833304a13db0b28559fbfd560c1c770146e
Skip to content

Commit a839483

Browse files
authored
feat!: prepared-transaction envelope, origin gating, audit fixes (#52)
* docs(snap): internal security audit for v0.2.0 Reviewed packages/snap source, manifest, build config, and dependency tree. No critical findings. Two High and six Medium findings concentrated in input handling and authorization on src/index.ts: - H-01: origin is never inspected; any installed dApp can drive every RPC method and the dialog does not say which dApp is asking. - H-02: dApp-supplied SignHashMetadata is rendered in the signing dialog as if authoritative, while only the hash is signed. - M-01/M-02: hexToBytes silently substitutes zero for non-hex chars and truncates odd-length input. - M-03: hash length is not validated before the dialog renders, so a multi-megabyte hex string is passed to Copyable. - M-04: keyIndex accepts NaN, floats, negatives, huge numbers — the derivation salt space is effectively unbounded. - M-05: canton_getFingerprint has no dialog and discloses a stable Canton identity to any caller. - M-06: topology dialog wording assumes initial registration and is misleading for key rotation or party-hosting changes. All transitive npm-audit advisories are in development-only paths and do not reach the published bundle. Recommended remediation order and file-level pointers are included in §5 of the report. * fix(snap): close audit findings H-01..L-03 before registry submission H-01 Pass `origin` into `onRpcRequest` and surface it in every dialog so the user sees which dApp is asking before approving. H-02 Caller-supplied `SignHashMetadata` is now rendered under an unmissable "details are supplied by the dApp and are NOT verified by the snap" warning, and the dialog footer states explicitly that approving will share the Canton fingerprint with the dApp. M-01 New `src/hex.ts` with a strict parser: rejects non-hex M-02 characters and odd-length input. The four duplicate local hexToBytes helpers (silently zero-substituting NaN, silently truncating odd-length) are gone. M-03 `parseSignHash` / `parseTopologyHash` validate hash length and format BEFORE any dialog renders, so an oversized hex blob can no longer be passed to `Copyable`. signHash is 32 bytes exactly; signTopology is 1..128 bytes. M-04 `validateKeyIndex` rejects NaN, floats, negatives, non-integers, and values > 1000 — the derivation salt space is no longer unbounded and integer-equal indices no longer split into distinct keys due to float stringification. M-05 `canton_getFingerprint` now asks for consent on the first call L-01 from each origin and persists the allowlist via snap_manageState (closing L-01 by actually using the permission the manifest already declares). Subsequent calls from approved origins return silently — no dialog spam for legitimate apps. M-06 Topology dialog wording rewritten to be operation-agnostic ("Sign Canton Topology Transaction") with a warning that topology ops can rotate keys or change party membership. L-02 Misleading comment in keyDerivation.ts removed. The real reason we use snap_getEntropy is that it returns snap-scoped entropy unlinkable from BIP-44 wallet paths — the previous comment claimed snap_getBip44Entropy "forbids coin type 60" which is backwards. L-03 Signing dialog footer notes that approving shares the fingerprint with the dApp, so the disclosure is no longer implicit. Tests: - jest: new cases for invalid keyIndex, malformed hex, wrong-length hash, oversized topology hash, first-call-vs-subsequent-call fingerprint flow, rejection paths. 17 passing. - vitest: new validation.test.ts covers NaN/Infinity/null/strings that the JSON-RPC transport can't carry into jest. 16 new + 28 existing crypto vectors = 44 passing. - mm-snap build: clean, no warnings. I-01 (SECURITY.md), I-02 (broader fuzz coverage), and I-03 (use @noble/curves' built-in DER encoder) deferred to follow-ups. * refactor(snap): use @noble/hashes utils for hex codec @noble/hashes already exports hexToBytes / bytesToHex from @noble/hashes/utils, and the snap already depends on the library. The custom src/hex.ts wrapper just re-derived behavior the library already provides — same strict rejection of non-hex characters and odd-length input, but with an audited implementation we don't have to maintain or review separately. - Delete packages/snap/src/hex.ts. - validation.ts, index.ts, sign.ts, keyDerivation.ts, fingerprint.ts, and the validation test import hexToBytes / bytesToHex directly from @noble/hashes/utils. - The one-line stripHexPrefix helper stays inline in the two files that use it (validation.ts and keyDerivation.ts). * refactor(snap): deeper audit pass — drop hand-rolled DER, validate metadata, harden state, broaden tests Second pass beyond the H/M findings already closed. Focus areas: replace remaining hand-rolled code with audited library primitives, plug the gaps in input validation, and bound state growth. Library swaps: - Drop the 50-line hand-rolled ASN.1 DER encoder in sign.ts. @noble/curves exposes Signature.toDERRawBytes(), which is canonical DER and produces byte-identical output to our previous encoder against every Go test vector (verified before swap). Input validation: - SignHashMetadata is now validated by validateMetadata: each field must be a string ≤ 200 chars, required fields are checked, arrays and non-objects are rejected. Previously a non-string field would render as "[object Object]" in the dialog; an oversized string could DoS the dialog renderer. - compressedPubKeyToSPKIDer now rejects keys whose first byte is not 0x02 or 0x03 (the valid compressed-point prefixes). Defense in depth — @noble/curves' ProjectivePoint.fromHex would catch it downstream, but failing at the input check makes the error site obvious. State: - allowFingerprintOrigin caps the allowlist at 200 entries with FIFO eviction. A long-lived install can no longer grow snap_manageState unbounded if the user keeps approving new origins. - Drop the unused EMPTY_STATE export. Tests (76 total — was 61): - crypto.test.ts: new "rejects compressed key with invalid prefix" + "accepts both valid compressed prefixes" cases (30 total). - validation.test.ts: 7 new metadata cases + boundary cases for parseTopologyHash min/max byte length, one-over-max, odd-length (27 total). - index.test.js: rejects metadata with non-string field, rejects metadata with oversized string (19 total). SECURITY.md: closes audit finding I-01. Lists supported versions, private reporting channels (GitHub Security Advisories + security@chainsafe.io), scope, and coordinated disclosure policy. * Revert SECURITY.md addition * fix(snap): address external review — strict topology, keyed allowlist, key context in dialogs External review (H/M/L) on top of the internal audit. Five real items land here; H-1 (blind hash signing) is acknowledged but only partially mitigated until canonical PreparedTransaction parsing is in scope. H-1 (partial) Signing dialog now leads with metadata when present; hash drops below as "Hash to sign". When metadata is absent, the dialog emits a "RAW HASH SIGNING — the dApp did not provide any transaction context" warning. The cryptographic binding problem (snap cannot verify that metadata matches the signed digest) is unchanged — fixing it requires the snap to re-derive Canton's canonical transaction hash from a structured payload, which needs canton-middleware to ship the schema + cross-validation vectors. H-2 parseTopologyHash now requires a SHA-256 multihash exactly: 34 bytes with the 0x1220 prefix. Wrong prefix, wrong digest length, or wrong overall length all reject. The previous 1..128 byte window let any byte string through. M-4 Fingerprint allowlist is now keyed by (origin, keyIndex), not just origin. Approving keyIndex 0 no longer lets a dApp silently enumerate keyIndex 1..1000. Schema changed from string[] to Record<string, number[]>; the loader handles the older shape as "no approvals" so existing installs don't crash. Cap is 200 origins × 32 keyIndexes per origin with FIFO eviction. M-5 Every dialog (export, sign, topology, fingerprint) now shows "Key index: N" and the corresponding Canton fingerprint via a Copyable. A dApp can no longer drive a non-default keyIndex invisibly. dialogs.ts factored out a shared contextLines() helper. M-6 README architecture diagram corrected: replaced the inaccurate "Derives key at m/44'/60'/1'/0/0" line with the actual snap_getEntropy derivation, and added a paragraph documenting the implication — keys are scoped to the snap ID, so local vs npm snap IDs derive different identities and there is no migration between them. L-8 Private-key derivation now rejection-samples via secp256k1.utils.isValidPrivateKey. sha256 output ≥ n is ≈ 2⁻¹²⁸, but rolling forward with a counter is cleaner than the previous "would throw downstream" path. Other: - handleSignHash/Topology/GetFingerprint/GetPublicKey all use a small deriveFull() helper so origin, keyIndex, and fingerprint are computed once per request. - Tests: +1 jest (re-prompts for different keyIndex from same origin), +1 vitest (multihash wrong-algorithm-prefix rejected), updated the oversized-topology-hash and validation test fixtures to use a real multihash. 58 vitest + 20 jest passing. * fix(snap): finalize external-review fixes — prepared-tx envelope, origin module, sha2 imports Lands the canton_signHash redesign that binds the dialog to the signed digest, plus the small clean-ups surfaced by the self-validation pass. Snap - canton_signHash no longer accepts a raw hash. Callers must supply a `canton-snap.prepared-transaction.v1` envelope; the snap recomputes the canonical SHA-256 multihash from a whitelisted field set and rejects with "transactionHash does not match canonical transaction data" on any drift. The signature is over sha256(transactionHash), so what the dialog renders provably maps to what gets signed. - Schema string lives in src/constants.ts and is imported by the validator and the vitest fixtures so the three call sites can't silently drift. - canonicalJson algorithm is now documented in detail (key sort, no whitespace, ECMA-262 string escaping, finite-number rejection) so a Go-side middleware implementer has an exact target. - New non-ASCII round-trip test (café / CJK / emoji) guards against surrogate / HTML-escape divergence between the snap and middleware. - assertSigningOrigin moved to src/origin.ts so it's directly unit- testable. Loopback set now accepts both "[::1]" (what WHATWG URL actually returns) and bare "::1" defensively. - All sha256 imports switched from @noble/hashes/sha256 (now deprecated upstream) to @noble/hashes/sha2. - stripHexPrefix consolidated in src/hex.ts; two duplicate copies removed from validation.ts and keyDerivation.ts. - Signing-dialog "verified" wording softened: "These fields were used to compute the prepared transaction hash you're about to sign" — cryptographically accurate without overclaiming Canton-level safety. dApp side (transfer flow) - prepareTransfer / prepareAcceptTransfer now require the middleware to return a prepared_transaction envelope and refuse the legacy hash-only response with "Middleware did not return a secure prepared_transaction envelope; update canton-middleware before using snap signing." - SNAP_ID defaults to npm:@chainsafe/canton-snap; VITE_SNAP_ID still overrides for local Flask dev. Documented in .env.example and README. Tests - vitest 65 passing (was 58): +5 assertSigningOrigin cases, +2 canonical-JSON non-ASCII / extra-field cases. - jest 20 passing (was 19): all signHash paths exercise the new prepared-transaction envelope. - mm-snap build clean, eslint clean. * ci(snap): drop release-as 0.2.1 pin The feat! commit in this PR will drive a 0.3.0 bump naturally; keeping release-as would override that to 0.2.1. * revert(snap): drop prepared-tx envelope enforcement; restore hash+metadata signing The prepared-tx envelope required canton-middleware to emit a `prepared_transaction` field that no released middleware build returns. Shipping it now breaks every transfer ("Middleware did not return a secure prepared_transaction envelope"). Reverting to the hash+metadata interface so this PR doesn't break local dev. The envelope check is the right end-state — it removes blind signing — but middleware needs to ship the canonical envelope first. Tracked as two follow-up issues: - canton-middleware: emit `prepared_transaction` envelope - canton-snap: re-introduce envelope enforcement once middleware ships it Both land together in a future PR pair. Kept from the audit pass: - origin allowlist (origin.ts) - strict topology multihash validation (parseTopologyHash) - keyed (origin, keyIndex) fingerprint allowlist - key-index + fingerprint context in every dialog - @noble/hashes/sha2 + secp256k1.utils.isValidPrivateKey rejection sampling - hex.ts consolidation * fix(snap): sha256 the hash before signing in canton_signHash Canton's CantonKeyPair.SignDER always sha256-hashes its input before ECDSA-signing — confirmed in pkg/keys/canton_keys.go:212-215 and mirrored in pkg/transfer's test helpers signTransferHash / signCantonTx (both call kp.SignDER, which internally does sha256.Sum256(message)). The snap was signing the raw 32-byte transaction hash directly, so middleware would receive a signature over H while Canton's verifier checked it against sha256(H) → 'signature verification failed' (403). Fix: compute sha256(hashBytes) and sign that digest. canton_signTopology already follows this pattern (sha256 of the multihash before signing); canton_signHash is now consistent.
1 parent 8c6cb6d commit a839483

21 files changed

Lines changed: 792 additions & 313 deletions

‎README.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ Derives secp256k1 keys from the user's MetaMask seed phrase and signs Canton tra
1010
MetaMask (encrypted vault, holds seed)
1111
│
1212
└─ Canton Snap (sandboxed)
13-
├─ Derives key at m/44'/60'/1'/0/0
14-
├─ Signs with SHA-256 + ECDSA + DER
15-
├─ Shows confirmation dialog
13+
├─ Derives key via snap_getEntropy (salt = "canton-network-key-<index>")
14+
├─ Hashed (SHA-256) to a secp256k1 private key, with rejection sampling
15+
├─ Signs SHA-256 ECDSA DER (low-S, RFC 6979 deterministic k)
16+
├─ Shows confirmation dialog (origin + keyIndex + fingerprint visible)
1617
└─ Returns signature to dApp
1718
```
1819

20+
Keys are **scoped to the snap ID**, not derived from a BIP-44 path. The same MetaMask seed will produce a different Canton identity under `local:http://localhost:4040` vs `npm:@chainsafe/canton-snap`. The published snap is the recoverable identity; local dev keys are independent. There is no migration path between snap IDs — re-register the new identity with Canton if the snap ID changes.
21+
1922
The **Canton dApp** (`packages/dapp`) is the browser frontend. It drives MetaMask + the snap for key operations, and talks to the Canton middleware REST API for registration and transaction flows.
2023

2124
## Snap RPC Methods
@@ -24,18 +27,18 @@ The **Canton dApp** (`packages/dapp`) is the browser frontend. It drives MetaMas
2427
|--------|---------|--------|
2528
| `canton_getPublicKey` | Export compressed pubkey + SPKI DER + fingerprint | Yes |
2629
| `canton_signTopology` | Sign topology hash during registration | Yes |
27-
| `canton_signHash` | Sign a 32-byte hash, return DER signature | Yes |
28-
| `canton_getFingerprint` | Quick fingerprint lookup | No |
30+
| `canton_signHash` | Sign a 32-byte transaction hash with optional metadata for the dialog | Yes |
31+
| `canton_getFingerprint` | Quick fingerprint lookup | First use per origin + key index |
2932

3033
## Project Structure
3134

3235
```
3336
canton-snap/
3437
├── packages/
35-
│ ├── snap/ # MetaMask Snap — pure signing oracle
38+
│ ├── snap/ # MetaMask Snap — Canton transaction signer
3639
│ │ ├── src/
3740
│ │ │ ├── index.ts # onRpcRequest handler
38-
│ │ │ ├── keyDerivation.ts # BIP-44 key derivation from MetaMask seed
41+
│ │ │ ├── keyDerivation.ts # snap_getEntropy key derivation
3942
│ │ │ ├── dialogs.ts # Confirmation dialog builders
4043
│ │ │ ├── types.ts # RPC param/response interfaces
4144
│ │ │ ├── spki.ts # Compressed pubkey → SPKI DER

‎docs/testing-with-middleware.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Open the dApp at **http://localhost:3000** — in the browser profile where Meta
6464

6565
## Architecture
6666

67-
The snap is a pure signing oracle — it never contacts the middleware directly.
67+
The snap signs the 32-byte transaction hash returned by the middleware — it never contacts the middleware directly.
6868

6969
```
7070
Canton dApp (browser)
@@ -134,7 +134,7 @@ npm run test:snap # snap integration tests only (jest + @metamask/snaps-jest
134134

135135
**MetaMask Flask required** — Until the snap is published to npm and reviewed by MetaMask, only MetaMask Flask users can install it.
136136

137-
**Snap not published** — The snap runs only as `local:http://localhost:4040`. Publishing requires an npm release and MetaMask's snap review.
137+
**Local snap mode** — Local development uses `local:http://localhost:4040` and requires MetaMask Flask. Published snap mode uses `npm:@chainsafe/canton-snap` with standard MetaMask.
138138

139139
**Each developer runs their own snap server** — The `local:` snap ID is bound to localhost; teammates cannot share one instance.
140140

@@ -184,5 +184,5 @@ npm run test:snap # snap integration tests only (jest + @metamask/snaps-jest
184184
|---|---|---|
185185
| `canton_getPublicKey` | Yes | Key derivation + SPKI + fingerprint |
186186
| `canton_signTopology` | Yes | Sign Canton topology multihash |
187-
| `canton_signHash` | Yes | Sign 32-byte pre-hashed digest |
188-
| `canton_getFingerprint` | No | Fingerprint lookup only |
187+
| `canton_signHash` | Yes | Sign a 32-byte Canton transaction hash with optional metadata |
188+
| `canton_getFingerprint` | First use per origin + key index | Fingerprint lookup only |

‎packages/dapp/src/hooks/useSnap.ts‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useState, useEffect, useCallback } from "react";
2-
import { sha256, getBytes } from "ethers";
32
import { installSnap, getInstalledSnap, invokeSnap } from "../lib/ethereum";
43

54
export interface SnapPublicKey {
@@ -80,10 +79,7 @@ export function useSnap(): SnapState {
8079

8180
const signHash = useCallback(
8281
async (hash: string, metadata?: SignHashMetadata): Promise<SignHashResult> => {
83-
// Canton returns a multihash from PrepareSubmission. Match Go's keys.SignDER:
84-
// sha256 the raw multihash bytes so the snap signs the correct 32-byte digest.
85-
const digest = sha256(getBytes(hash));
86-
return invokeSnap<SignHashResult>("canton_signHash", { hash: digest, metadata });
82+
return invokeSnap<SignHashResult>("canton_signHash", { hash, metadata });
8783
},
8884
[],
8985
);

‎packages/dapp/src/pages/DashboardBalancesPage.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,10 @@ export function DashboardBalancesPage({
259259
);
260260
setAcceptState((s) => ({ ...s, [cid]: "signing" }));
261261
const { derSignature, fingerprint } = await snap.signHash(prep.transactionHash, {
262-
operation: "accept",
262+
operation: "Accept transfer",
263263
tokenSymbol: offer.symbol ?? offer.instrumentId,
264264
amount: offer.amount,
265+
recipient: offer.receiverPartyId,
265266
sender: offer.senderPartyId,
266267
});
267268
setAcceptState((s) => ({ ...s, [cid]: "executing" }));

‎packages/dapp/src/pages/TransferPage.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ export function TransferPage({ address, activeTab, onTabChange, onDisconnect, ke
358358
try {
359359
setSignPhase("signing");
360360
const { derSignature, fingerprint } = await snap.signHash(prepared.transactionHash, {
361-
operation: "transfer",
361+
operation: "Transfer",
362362
tokenSymbol: selectedToken.symbol,
363363
amount,
364364
recipient,

‎packages/snap/snap.manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"directory": "packages/snap"
99
},
1010
"source": {
11-
"shasum": "HIDNkgcijyLhBFbs5jUjPBMgHEwr1/APbqB5KTpZPq0=",
11+
"shasum": "FKijExPsqFhOTSZYWXzPGhNp2QqB07aJofVrN0lOvIQ=",
1212
"location": {
1313
"npm": {
1414
"filePath": "dist/bundle.js",

‎packages/snap/src/dialogs.ts‎

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,107 @@
11
/**
22
* Confirmation dialog builders for Canton Snap.
33
*
4-
* Each sensitive operation (key export, signing) shows a dialog
5-
* the user must approve before the snap proceeds.
6-
*
7-
* Uses @metamask/snaps-sdk/jsx component factories (snaps-sdk v10+).
4+
* Every dialog surfaces the calling origin, the keyIndex being used,
5+
* and the corresponding Canton fingerprint so the user can verify the
6+
* full context of what they are approving.
87
*/
98

109
import { Box, Heading, Text, Divider, Copyable } from "@metamask/snaps-sdk/jsx";
1110
import type { JSXElement } from "@metamask/snaps-sdk/jsx";
1211
import type { SignHashMetadata } from "./types";
1312

14-
/**
15-
* Dialog for exporting the Canton public key during registration.
16-
*/
17-
export function exportPublicKeyDialog(fingerprint: string) {
13+
function contextLines(origin: string, keyIndex: number, fingerprint: string): JSXElement[] {
14+
return [
15+
Text({ children: `Requested by: ${origin}` }),
16+
Text({ children: `Key index: ${keyIndex}` }),
17+
Text({ children: "Canton fingerprint:" }),
18+
Copyable({ value: fingerprint }),
19+
];
20+
}
21+
22+
export function exportPublicKeyDialog(origin: string, keyIndex: number, fingerprint: string) {
1823
return Box({
1924
children: [
2025
Heading({ children: "Export Canton Public Key" }),
21-
Text({
22-
children: "A dApp is requesting your Canton Network public key for party registration.",
23-
}),
24-
Divider({}),
25-
Text({ children: "Fingerprint:" }),
26-
Copyable({ value: fingerprint }),
26+
...contextLines(origin, keyIndex, fingerprint),
2727
Divider({}),
2828
Text({ children: "This does not expose your private key." }),
2929
],
3030
});
3131
}
3232

33-
/**
34-
* Dialog for signing a Canton transaction (transfer, mint, etc.).
35-
*/
36-
export function signTransactionDialog(hash: string, metadata?: SignHashMetadata) {
37-
const children: JSXElement[] = [Heading({ children: "Sign Canton Transaction" }), Divider({})];
33+
export function signTransactionDialog(
34+
origin: string,
35+
keyIndex: number,
36+
fingerprint: string,
37+
transactionHash: string,
38+
metadata?: SignHashMetadata,
39+
) {
40+
const children: JSXElement[] = [
41+
Heading({ children: "Sign Canton Transaction" }),
42+
...contextLines(origin, keyIndex, fingerprint),
43+
Divider({}),
44+
];
3845

3946
if (metadata) {
40-
children.push(Text({ children: `Operation: ${metadata.operation}` }));
41-
children.push(Text({ children: `Token: ${metadata.tokenSymbol}` }));
42-
children.push(Text({ children: `Amount: ${metadata.amount}` }));
43-
if (metadata.recipient) {
44-
children.push(Text({ children: `To: ${metadata.recipient}` }));
45-
}
46-
if (metadata.sender) {
47-
children.push(Text({ children: `From: ${metadata.sender}` }));
48-
}
49-
children.push(Divider({}));
47+
children.push(
48+
Text({
49+
children:
50+
"Transaction details (reported by the dApp; the snap cannot yet verify these against the hash):",
51+
}),
52+
Text({ children: `Operation: ${metadata.operation}` }),
53+
Text({ children: `Token: ${metadata.tokenSymbol}` }),
54+
Text({ children: `Amount: ${metadata.amount}` }),
55+
);
56+
if (metadata.recipient) children.push(Text({ children: `To: ${metadata.recipient}` }));
57+
if (metadata.sender) children.push(Text({ children: `From: ${metadata.sender}` }));
58+
} else {
59+
children.push(
60+
Text({
61+
children:
62+
"⚠ RAW HASH SIGNING — the dApp did not provide any transaction context. Approve only if you trust this dApp.",
63+
}),
64+
);
5065
}
5166

67+
children.push(Divider({}));
5268
children.push(Text({ children: "Hash to sign:" }));
53-
children.push(Copyable({ value: hash }));
54-
children.push(Text({ children: "Verify the details match your intent before approving." }));
69+
children.push(Copyable({ value: transactionHash }));
5570

5671
return Box({ children });
5772
}
5873

59-
/**
60-
* Dialog for signing topology during Canton party registration.
61-
*/
62-
export function signTopologyDialog(hash: string) {
74+
export function signTopologyDialog(
75+
origin: string,
76+
keyIndex: number,
77+
fingerprint: string,
78+
hash: string,
79+
) {
6380
return Box({
6481
children: [
65-
Heading({ children: "Approve Canton Registration" }),
66-
Text({ children: "Sign the topology transaction to register your Canton Network identity." }),
67-
Text({ children: "This links your MetaMask wallet to a Canton party." }),
82+
Heading({ children: "Sign Canton Topology Transaction" }),
83+
...contextLines(origin, keyIndex, fingerprint),
6884
Divider({}),
85+
Text({
86+
children:
87+
"⚠ Topology transactions can register a new identity, rotate keys, or change party membership. Verify the operation in the dApp before approving.",
88+
}),
6989
Text({ children: "Topology hash:" }),
7090
Copyable({ value: hash }),
7191
],
7292
});
7393
}
94+
95+
export function getFingerprintDialog(origin: string, keyIndex: number, fingerprint: string) {
96+
return Box({
97+
children: [
98+
Heading({ children: "Share Canton Fingerprint" }),
99+
...contextLines(origin, keyIndex, fingerprint),
100+
Divider({}),
101+
Text({
102+
children:
103+
"This dApp wants to read this Canton identity. Approving will let this dApp read it silently from now on for this same key index. Other key indices will still require a fresh prompt.",
104+
}),
105+
],
106+
});
107+
}

‎packages/snap/src/fingerprint.ts‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
* keys.CantonKeyPair.Fingerprint() — used for Canton party identification.
66
*/
77

8-
import { sha256 } from "@noble/hashes/sha256";
8+
import { sha256 } from "@noble/hashes/sha2";
99
import { compressedPubKeyToSPKIDer } from "./spki";
10+
import { bytesToHex } from "@noble/hashes/utils";
1011

1112
/**
1213
* Compute the Canton key fingerprint from SPKI DER bytes.
@@ -52,9 +53,3 @@ export function fingerprintFromCompressedPubKey(compressedPubKey: Uint8Array): s
5253
const spkiDer = compressedPubKeyToSPKIDer(compressedPubKey);
5354
return fingerprintFromSPKI(spkiDer);
5455
}
55-
56-
function bytesToHex(bytes: Uint8Array): string {
57-
return Array.from(bytes)
58-
.map((b) => b.toString(16).padStart(2, "0"))
59-
.join("");
60-
}

‎packages/snap/src/hex.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Hex utilities shared by every input-parsing path in the snap.
3+
*
4+
* The byte-level hex codec is delegated to @noble/hashes' strict
5+
* `hexToBytes` / `bytesToHex`. This file only holds the small
6+
* `stripHexPrefix` helper that was previously duplicated across
7+
* validation.ts and keyDerivation.ts.
8+
*/
9+
10+
export function stripHexPrefix(hex: string): string {
11+
return hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
12+
}

0 commit comments

Comments
 (0)