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: 2 additions & 0 deletions src/core/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const CAPABILITY_STATUS = {
keyAttestation: 'experimental',
/** RAILGUN signing (BabyJubjub EdDSA) via the custom app. */
railgunSigning: 'experimental',
/** CLEAR_SIGN transact review protocol (INS 0x11) — new in firmware 1.6.1. */
clearSign: 'experimental',
/** Ethereum tx / message / EIP-712 signing. */
ethereumSigning: 'experimental',
/** FROST / MPC threshold signing — NOT implemented on current firmware. */
Expand Down
39 changes: 39 additions & 0 deletions src/core/signers/railgun-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
buildGetPublicKey,
buildSignHash,
buildGetViewingKey,
buildGetViewingPublicKey,
buildGetRailgunAddress,
buildRailgunEip7702Bip32Path,
buildRailgunEthereumBip32Path,
buildGetEthereumPublicKey,
Expand All @@ -35,6 +37,8 @@ import {
parseSignResponse,
parsePublicKeyResponse,
parseViewingKeyResponse,
parseViewingPublicKeyResponse,
parseRailgunAddressResponse,
extractEchoedHash,
} from '../../validation/apdu-response.js';
import { validateSignature } from '../../validation/signature.js';
Expand Down Expand Up @@ -89,6 +93,9 @@ function defaultCapabilities(): RailgunAppCapabilities {
eip7702Authorization: false,
ethereumTxHash: false,
ethereumSigning: [],
viewingPublicKey: false,
railgunAddress: false,
railgunClearSign: false,
};
}

Expand Down Expand Up @@ -199,6 +206,38 @@ export class RailgunSigner {
return deriveRailgunWalletArtifacts(spendingPublicKey, viewingPrivateKey);
}

/**
* Get the compressed Ed25519 viewing *public* key from the device (INS 0x10).
* Display/verify accessor — requires an on-device confirmation and does NOT
* export the viewing secret. Wallet loading still uses `getWalletArtifacts()`.
* @returns 32 raw bytes (compressed Ed25519 point).
*/
async getViewingPublicKey(): Promise<Uint8Array> {
this.requireCapability(
(capabilities) => capabilities.viewingPublicKey,
'RAILGUN app does not advertise viewing-public-key retrieval support.',
);
const response = await this.transport.send(buildGetViewingPublicKey(this.account, this.profile));
validateApduResponse(response);
return parseViewingPublicKeyResponse(response.data);
}

/**
* Derive and display the canonical `0zk1…` address on the device (INS 0x14).
* Device-confirmed cross-check of the host-derived address — requires an
* on-device confirmation.
* @returns the 127-character `0zk1…` address string.
*/
async getRailgunAddress(): Promise<string> {
this.requireCapability(
(capabilities) => capabilities.railgunAddress,
'RAILGUN app does not advertise RAILGUN-address derivation support.',
);
const response = await this.transport.send(buildGetRailgunAddress(this.account, this.profile));
validateApduResponse(response);
return parseRailgunAddressResponse(response.data);
}

private async getEthereumPublicKeyAtPath(request: RailgunEthereumPreloadRequest, display: boolean): Promise<Uint8Array> {
this.requireCapability(
(capabilities) => capabilities.ethereumAddress,
Expand Down
21 changes: 21 additions & 0 deletions src/core/transport/apdu-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export type RailgunAppCapabilities = {
readonly eip7702Authorization: boolean;
readonly ethereumTxHash: boolean;
readonly ethereumSigning: readonly EthereumSignCapability[];
/** Device can return the compressed Ed25519 viewing *public* key (INS 0x10). */
readonly viewingPublicKey: boolean;
/** Device can derive + display the canonical `0zk1…` address (INS 0x14). */
readonly railgunAddress: boolean;
/** Device supports the stateful CLEAR_SIGN transact review protocol (INS 0x11). */
readonly railgunClearSign: boolean;
};

/**
Expand All @@ -64,6 +70,15 @@ export type ApduProfile = {
readonly sign: ApduSignDef;
/** Get viewing private key. Response: privkey(32B). Optional — not all apps support this. */
readonly getViewingKey?: ApduCommandDef;
/** Get compressed Ed25519 viewing public key (INS 0x10). Response: pubkey(32B). */
readonly getViewingPublicKey?: ApduCommandDef;
/** Derive + display the canonical `0zk1…` address (INS 0x14). Response: 127 ASCII bytes. */
readonly getRailgunAddress?: ApduCommandDef;
/**
* CLEAR_SIGN transact review protocol (INS 0x11). Stateful: P1 selects the
* sub-command and the response length varies per sub-command; the quoted
* `responseLength` is the single-tx FINALIZE length (129B). */
readonly clearSign?: ApduCommandDef;
/** Get secp256k1 Ethereum public key. Response: uncompressed pubkey(65B). */
readonly getEthereumPublicKey?: ApduCommandDef;
/** Sign an EIP-7702 authorization. Response: yParity(1B) + r(32B) + s(32B). */
Expand All @@ -88,11 +103,17 @@ export const RAILGUN_PROFILE: ApduProfile = {
eip7702Authorization: true,
ethereumTxHash: true,
ethereumSigning: ['blind', 'clear'],
viewingPublicKey: true,
railgunAddress: true,
railgunClearSign: true,
},
commands: {
getPublicKey: { ins: 0x01, responseLength: 64 },
sign: { ins: 0x12, responseLength: 129, hasPrefix: true, echoesHash: true },
getViewingKey: { ins: 0x13, responseLength: 32 },
getViewingPublicKey: { ins: 0x10, responseLength: 32 },
getRailgunAddress: { ins: 0x14, responseLength: 127 },
clearSign: { ins: 0x11, responseLength: 129 },
getEthereumPublicKey: { ins: 0x07, responseLength: 65 },
signEip7702Authorization: { ins: 0x08, responseLength: 65 },
signEthereumTxHash: { ins: 0x09, responseLength: 65 },
Expand Down
58 changes: 58 additions & 0 deletions src/core/transport/apdu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ export const RailgunAppINS = {
SIGN_HASH: 0x12,
/** Get viewing private key. Data: account(4B BE). Response: privkey(32B). */
GET_VIEWING_KEY: 0x13,
/** Get compressed Ed25519 viewing public key. Data: account(4B BE). Response: pubkey(32B). P1=0x01. */
GET_VIEWING_PUBLIC_KEY: 0x10,
/** CLEAR_SIGN transact review protocol. Stateful; P1 selects the sub-command, P2=0x00. */
CLEAR_SIGN: 0x11,
/** Derive + display the canonical `0zk1…` address. Data: account(4B BE). Response: 127 ASCII. P1=0x01. */
GET_RAILGUN_ADDRESS: 0x14,

// ─── Ethereum / EIP-7702 — matched to current embedded app demo ──────
// EIP-7702 (SIGN_EIP7702_AUTHORIZATION) is UNDER DEVELOPMENT — see CAPABILITY_STATUS.
Expand Down Expand Up @@ -285,6 +291,56 @@ export function buildGetViewingKey(
};
}

/**
* Build GET_VIEWING_PUBLIC_KEY APDU (VIEWING_PUBKEY, INS 0x10).
* Returns the compressed Ed25519 viewing *public* key — 32 bytes.
*
* P1 is `0x01` (display + confirm): the device shows the account index and
* pubkey hex and returns the key only on Approve (Reject → `0x6985`). This is a
* display/verify accessor — it does NOT export the viewing secret. Wallet-artifact
* derivation still uses `buildGetViewingKey` (the private key, INS 0x13).
* @param account - Account index (default 0).
* @param profile - APDU profile (default RAILGUN_PROFILE).
*/
export function buildGetViewingPublicKey(
account = 0,
profile: ApduProfile = RAILGUN_PROFILE,
): ApduCommand {
const command = requiredCommand(profile.commands.getViewingPublicKey, profile, 'getViewingPublicKey');
return {
cla: profile.cla,
ins: command.ins,
p1: 0x01,
p2: 0,
data: encodeAccountIndex(account),
};
}

/**
* Build GET_RAILGUN_ADDRESS APDU (RAILGUN_ADDRESS, INS 0x14).
* Derives and displays the canonical `0zk1…` address — 127 ASCII bytes.
*
* P1 is `0x01` (display + confirm; always required in prod): the device shows the
* same `0zk1…` string for out-of-band comparison and returns it only on Approve
* (Reject → `0x6985`). This is a device-confirmed cross-check of the address the
* host already derives in `wallet-artifacts.ts`; it does not replace it.
* @param account - Account index (default 0).
* @param profile - APDU profile (default RAILGUN_PROFILE).
*/
export function buildGetRailgunAddress(
account = 0,
profile: ApduProfile = RAILGUN_PROFILE,
): ApduCommand {
const command = requiredCommand(profile.commands.getRailgunAddress, profile, 'getRailgunAddress');
return {
cla: profile.cla,
ins: command.ins,
p1: 0x01,
p2: 0,
data: encodeAccountIndex(account),
};
}

export function buildGetEthereumPublicKey(
request: number | RailgunEthereumPathRequest = 0,
display = false,
Expand Down Expand Up @@ -523,4 +579,6 @@ export function buildMpcReset(): ApduCommand {
export const SIGN_RESPONSE_LENGTH = RAILGUN_PROFILE.commands.sign.responseLength;
export const PUBLIC_KEY_RESPONSE_LENGTH = RAILGUN_PROFILE.commands.getPublicKey.responseLength;
export const VIEWING_KEY_RESPONSE_LENGTH = RAILGUN_PROFILE.commands.getViewingKey!.responseLength;
export const VIEWING_PUBLIC_KEY_RESPONSE_LENGTH = RAILGUN_PROFILE.commands.getViewingPublicKey!.responseLength;
export const RAILGUN_ADDRESS_RESPONSE_LENGTH = RAILGUN_PROFILE.commands.getRailgunAddress!.responseLength;
export const COMMITMENTS_RESPONSE_LENGTH = 128; // hiding.x(32) + hiding.y(32) + binding.x(32) + binding.y(32)
Loading
Loading