@1shotapi/ows-signer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @1shotapi/ows-signer
2
+
3
+ The **OWS Signing Layer** — plain JavaScript, zero npm dependencies, no build step.
4
+
5
+ ## Design
6
+
7
+ Minimal **KMS/HSM** iframe: WebAuthn PRF key custody, curve-based signing, optional export/recovery UI. Chain-specific marshalling lives in `@1shotapi/ows-signer-utils`.
8
+
9
+ ## Deployment and rpId (hybrid)
10
+
11
+ - `rpId` is always **`window.location.hostname`** (the signer document origin).
12
+ - **Canonical CDN / on-chain gateway:** shared passkey namespace for all integrators on that host.
13
+ - **Per-brand rpId:** self-host or reverse-proxy signer HTML on your branding origin.
14
+
15
+ ## Nesting
16
+
17
+ Must be embedded as **iframe inside a Branding Layer iframe** (not directly in the host). Rejects messages unless `window.parent !== window.top`.
18
+
19
+ ## Wire protocol
20
+
21
+ Parent → signer:
22
+
23
+ ```json
24
+ { "v": 1, "kind": "request", "method": "signDigest", "correlationId": "…", "params": { } }
25
+ ```
26
+
27
+ Signer → parent:
28
+
29
+ ```json
30
+ { "v": 1, "kind": "event", "event": "DigestSigned", "correlationId": "…", "data": { } }
31
+ ```
32
+
33
+ Only accepts messages when `event.source === window.parent`. Replies use the parent's `event.origin` as `targetOrigin`.
34
+
35
+ ## Methods
36
+
37
+ | Method | Params | Success events |
38
+ |--------|--------|----------------|
39
+ | `getVersion` | — | `Version` |
40
+ | `createCredential` | `name`, `options?` (`rpName`, `userDisplayName`, `userId`) | `KeyDerived`, `CredentialCreated` |
41
+ | `signDigest` | `digestData`, `scheme`, `credentialId?` | `KeyDerived` (PRF path), `DigestSigned` |
42
+ | `revealPrivateKey` | `credentialId?` | `KeyDerived` (+ DOM display) |
43
+ | `createRecoveryData` | `passwordText`, `buttonText`, `minPasswordLength`, `credentialId?` | `KeyDerived`, `RecoveryDataCreated` |
44
+ | `recoverKey` | `aes256EncryptedPrivateKey`, `passwordText`, `buttonText`, `credentialId?` | DOM display; `RecoverySessionStarted` or re-bind → `RecoverySessionCleared` |
45
+ | `getPublicKey` | `credentialId?`, `challenge?` | `KeyDerived`, `PublicKey`, `ChallengeSigned?` |
46
+ | `clearRecoverySession` | — | `RecoverySessionCleared` |
47
+ | `encryptAES256` | `plaintexts[]`, `credentialId?` | `KeyDerived` (PRF path), `AES256Encrypted` |
48
+ | `decryptAES256` | `ciphertexts[]`, `credentialId?` | `KeyDerived` (PRF path), `AES256Decrypted` |
49
+
50
+ Failure events: `NotAllowed`, `InvalidRequest`.
51
+
52
+ `createCredential` options:
53
+
54
+ - `rpName` — relying party display name in passkey UI (default `"OWS"`); does not affect `rpId`
55
+ - `userDisplayName` — friendly account label (defaults to `name`)
56
+ - `userId` — stable user handle bytes (base64); random if omitted
57
+
58
+ ## Signing schemes
59
+
60
+ `secp256k1-ecdsa`, `secp256k1-ecdsa-recoverable`, `secp256k1-bip340` (not yet implemented), `ed25519`.
61
+
62
+ `digestData` is `0x`-prefixed hex. Hash schemes require 32 bytes.
63
+
64
+ ## PRF labels
65
+
66
+ - `ows-v1/secp256k1`
67
+ - `ows-v1/ed25519` (derived via HKDF from PRF output)
68
+ - `ows-v1/aes256-gcm` (HKDF from the secp256k1 scalar — same material as `signDigest`)
69
+
70
+ ## Recovery envelope
71
+
72
+ `ows1:0x…` — version byte, 16-byte salt, 12-byte IV, AES-GCM-256 ciphertext (PBKDF2-SHA256, 250k iterations).
73
+
74
+ ## AES-256 seal envelope (credentials / general payload)
75
+
76
+ `ows-aes1:0x…` — version byte, 12-byte IV, AES-GCM-256 ciphertext+tag. Key is HKDF-SHA256 of the wallet secp256k1 scalar with info `ows-v1/aes256-gcm` (no PBKDF2). Works under a recovery session without a new WebAuthn ceremony. Batch methods amortize one ceremony across many plaintexts/ciphertexts.
77
+
78
+ ## Recovery session
79
+
80
+ `recoverKey` caches the secp256k1 scalar in memory for subsequent `signDigest` / `encryptAES256` / `decryptAES256` calls without PRF. Cleared via `clearRecoverySession` or successful `credentialId` re-bind after recovery.
81
+
82
+ ## Layout
83
+
84
+ ```
85
+ html/index.html Entry document
86
+ src/main.js Bootstrap + message listener
87
+ src/handlers.js RPC dispatch
88
+ src/webauthn.js Passkey ceremonies
89
+ src/crypto/ PRF, signing, recovery
90
+ src/crypto/vendor/ Vendored @noble/secp256k1 + ed25519 (MIT)
91
+ reference/ 1ShotPay reference (not published)
92
+ ```
93
+
94
+ ## On-chain
95
+
96
+ See `@1shotapi/ows-onchain`.
97
+
98
+ ## Integrity (planned)
99
+
100
+ Client-side confirmation that official signer bytes are loaded (signed manifest + verify-before-execute, optional EIP-8244 pin) is **specced and tabled** — see [docs/trusted-loader-plan.md](docs/trusted-loader-plan.md). Not implemented yet.
101
+
102
+ ## Tests
103
+
104
+ ```bash
105
+ npm test -w @1shotapi/ows-signer
106
+ ```
@@ -0,0 +1,54 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>OWS Custody Signer</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ font-family: system-ui, sans-serif;
11
+ }
12
+ body {
13
+ margin: 0;
14
+ padding: 8px;
15
+ }
16
+ .ows-signer-ui {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: 8px;
20
+ max-width: 100%;
21
+ }
22
+ .ows-key {
23
+ margin: 0;
24
+ padding: 8px;
25
+ border: 1px solid #8884;
26
+ border-radius: 4px;
27
+ font-family: ui-monospace, monospace;
28
+ font-size: 12px;
29
+ word-break: break-all;
30
+ white-space: pre-wrap;
31
+ }
32
+ .ows-password {
33
+ padding: 8px;
34
+ border: 1px solid #8884;
35
+ border-radius: 4px;
36
+ font: inherit;
37
+ }
38
+ .ows-copy,
39
+ .ows-confirm {
40
+ padding: 8px 12px;
41
+ border: 1px solid #8884;
42
+ border-radius: 4px;
43
+ background: transparent;
44
+ font: inherit;
45
+ cursor: pointer;
46
+ align-self: flex-start;
47
+ }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <div id="ows-signer-root"></div>
52
+ <script type="module" src="../src/main.js"></script>
53
+ </body>
54
+ </html>
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@1shotapi/ows-signer",
3
+ "version": "0.2.0",
4
+ "description": "OWS Signing Layer — plain JavaScript WebAuthn PRF key custody with curve-based signing (on-chain via EIP-8244)",
5
+ "type": "module",
6
+ "files": [
7
+ "src",
8
+ "html",
9
+ "README.md"
10
+ ],
11
+ "exports": {
12
+ "./html": "./html/index.html",
13
+ "./src/*": "./src/*"
14
+ },
15
+ "scripts": {
16
+ "test": "node --test test/*.test.js"
17
+ },
18
+ "keywords": [
19
+ "ows",
20
+ "open-wallet-standard",
21
+ "webauthn",
22
+ "prf",
23
+ "passkey",
24
+ "signer",
25
+ "custody",
26
+ "secp256k1"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/1Shot-API/open-wallet.git",
31
+ "directory": "packages/ows-signer"
32
+ },
33
+ "license": "MIT",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
@@ -0,0 +1,30 @@
1
+ /** @typedef {'secp256k1-ecdsa' | 'secp256k1-ecdsa-recoverable' | 'secp256k1-bip340' | 'ed25519'} SignScheme */
2
+
3
+ export const API_VERSION = 1;
4
+ export const SIGNER_VERSION = "0.1.0";
5
+
6
+ export const PRF_LABEL_SECP256K1 = new TextEncoder().encode("ows-v1/secp256k1");
7
+ export const PRF_LABEL_ED25519 = new TextEncoder().encode("ows-v1/ed25519");
8
+ /** HKDF info for AES-256-GCM — IKM is the secp256k1 scalar (`signDigest` material). */
9
+ export const PRF_LABEL_AES256 = new TextEncoder().encode("ows-v1/aes256-gcm");
10
+
11
+ /** @type {SignScheme[]} */
12
+ export const SIGN_SCHEMES = [
13
+ "secp256k1-ecdsa",
14
+ "secp256k1-ecdsa-recoverable",
15
+ "secp256k1-bip340",
16
+ "ed25519",
17
+ ];
18
+
19
+ export const METHODS = [
20
+ "getVersion",
21
+ "createCredential",
22
+ "signDigest",
23
+ "revealPrivateKey",
24
+ "createRecoveryData",
25
+ "recoverKey",
26
+ "getPublicKey",
27
+ "clearRecoverySession",
28
+ "encryptAES256",
29
+ "decryptAES256",
30
+ ];
@@ -0,0 +1,115 @@
1
+ import { PRF_LABEL_AES256 } from "../constants.js";
2
+ import { parse0xHex, to0xHex } from "../hex.js";
3
+ import { hkdfExpand } from "./prf.js";
4
+
5
+ /** Envelope prefix — distinct from recovery `ows1:` (PBKDF2). */
6
+ export const AES256_ENVELOPE_PREFIX = "ows-aes1:";
7
+ const AES256_VERSION = 1;
8
+ const IV_LENGTH = 12;
9
+
10
+ /**
11
+ * Derive an AES-256-GCM CryptoKey from the wallet secp256k1 scalar
12
+ * (same material `signDigest` uses), via HKDF domain separation.
13
+ *
14
+ * @param {Uint8Array} secp256k1PrivateKey
15
+ * @returns {Promise<CryptoKey>}
16
+ */
17
+ export async function deriveAes256KeyFromSecp256k1(secp256k1PrivateKey) {
18
+ const raw = await hkdfExpand(
19
+ secp256k1PrivateKey.buffer.slice(
20
+ secp256k1PrivateKey.byteOffset,
21
+ secp256k1PrivateKey.byteOffset + secp256k1PrivateKey.byteLength,
22
+ ),
23
+ PRF_LABEL_AES256,
24
+ 32,
25
+ );
26
+ try {
27
+ return await crypto.subtle.importKey(
28
+ "raw",
29
+ raw,
30
+ { name: "AES-GCM", length: 256 },
31
+ false,
32
+ ["encrypt", "decrypt"],
33
+ );
34
+ } finally {
35
+ raw.fill(0);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Encrypt a UTF-8 plaintext. Envelope: `ows-aes1:0x` ‖ version ‖ IV ‖ ciphertext+tag.
41
+ *
42
+ * @param {string} plaintext
43
+ * @param {CryptoKey} aesKey
44
+ * @returns {Promise<string>}
45
+ */
46
+ export async function encryptAes256String(plaintext, aesKey) {
47
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
48
+ const encoded = new TextEncoder().encode(plaintext);
49
+ const ciphertext = new Uint8Array(
50
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, encoded),
51
+ );
52
+ const payload = new Uint8Array(1 + IV_LENGTH + ciphertext.length);
53
+ payload[0] = AES256_VERSION;
54
+ payload.set(iv, 1);
55
+ payload.set(ciphertext, 1 + IV_LENGTH);
56
+ return `${AES256_ENVELOPE_PREFIX}${to0xHex(payload)}`;
57
+ }
58
+
59
+ /**
60
+ * Decrypt an `ows-aes1:` envelope to a UTF-8 string.
61
+ *
62
+ * @param {string} envelope
63
+ * @param {CryptoKey} aesKey
64
+ * @returns {Promise<string>}
65
+ */
66
+ export async function decryptAes256String(envelope, aesKey) {
67
+ if (!envelope.startsWith(AES256_ENVELOPE_PREFIX)) {
68
+ throw new Error("invalidAes256Envelope");
69
+ }
70
+ const bytes = parse0xHex(envelope.slice(AES256_ENVELOPE_PREFIX.length));
71
+ if (bytes.length < 1 + IV_LENGTH + 16) {
72
+ throw new Error("invalidAes256Envelope");
73
+ }
74
+ if (bytes[0] !== AES256_VERSION) {
75
+ throw new Error("unsupportedAes256Version");
76
+ }
77
+ const iv = bytes.slice(1, 1 + IV_LENGTH);
78
+ const ciphertext = bytes.slice(1 + IV_LENGTH);
79
+ try {
80
+ const plain = new Uint8Array(
81
+ await crypto.subtle.decrypt({ name: "AES-GCM", iv }, aesKey, ciphertext),
82
+ );
83
+ return new TextDecoder().decode(plain);
84
+ } catch {
85
+ throw new Error("decryptionFailed");
86
+ }
87
+ }
88
+
89
+ /**
90
+ * @param {string[]} plaintexts
91
+ * @param {Uint8Array} secp256k1PrivateKey
92
+ * @returns {Promise<string[]>}
93
+ */
94
+ export async function encryptAes256Batch(plaintexts, secp256k1PrivateKey) {
95
+ const aesKey = await deriveAes256KeyFromSecp256k1(secp256k1PrivateKey);
96
+ const out = [];
97
+ for (const plaintext of plaintexts) {
98
+ out.push(await encryptAes256String(plaintext, aesKey));
99
+ }
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * @param {string[]} envelopes
105
+ * @param {Uint8Array} secp256k1PrivateKey
106
+ * @returns {Promise<string[]>}
107
+ */
108
+ export async function decryptAes256Batch(envelopes, secp256k1PrivateKey) {
109
+ const aesKey = await deriveAes256KeyFromSecp256k1(secp256k1PrivateKey);
110
+ const out = [];
111
+ for (const envelope of envelopes) {
112
+ out.push(await decryptAes256String(envelope, aesKey));
113
+ }
114
+ return out;
115
+ }
@@ -0,0 +1,119 @@
1
+ import { PRF_LABEL_ED25519, PRF_LABEL_SECP256K1 } from "../constants.js";
2
+ import { getPublicKeyAsync as edGetPublicKeyAsync } from "./vendor/noble-ed25519.js";
3
+ import { getPublicKey as secpGetPublicKey } from "./vendor/noble-secp256k1.js";
4
+
5
+ const SECP256K1_N = BigInt(
6
+ "0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141",
7
+ );
8
+
9
+ /**
10
+ * @param {unknown} prfOutput
11
+ * @returns {ArrayBuffer | null}
12
+ */
13
+ export function normalizePrfOutputToArrayBuffer(prfOutput) {
14
+ if (prfOutput instanceof ArrayBuffer) return prfOutput;
15
+ if (prfOutput instanceof Uint8Array) {
16
+ return prfOutput.buffer.slice(
17
+ prfOutput.byteOffset,
18
+ prfOutput.byteOffset + prfOutput.byteLength,
19
+ );
20
+ }
21
+ if (ArrayBuffer.isView(prfOutput)) {
22
+ const view = /** @type {ArrayBufferView} */ (prfOutput);
23
+ return view.buffer.slice(
24
+ view.byteOffset,
25
+ view.byteOffset + view.byteLength,
26
+ );
27
+ }
28
+ if (Array.isArray(prfOutput)) {
29
+ return new Uint8Array(prfOutput).buffer;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * @param {ArrayBuffer} prfBuffer
36
+ * @param {Uint8Array} infoLabel
37
+ * @param {number} length
38
+ * @returns {Promise<Uint8Array>}
39
+ */
40
+ export async function hkdfExpand(prfBuffer, infoLabel, length) {
41
+ const ikm = new Uint8Array(prfBuffer);
42
+ const baseKey = await crypto.subtle.importKey("raw", ikm, "HKDF", false, [
43
+ "deriveBits",
44
+ ]);
45
+ const salt = new Uint8Array(32);
46
+ const bits = await crypto.subtle.deriveBits(
47
+ {
48
+ name: "HKDF",
49
+ hash: "SHA-256",
50
+ salt,
51
+ info: infoLabel,
52
+ },
53
+ baseKey,
54
+ length * 8,
55
+ );
56
+ return new Uint8Array(bits);
57
+ }
58
+
59
+ /**
60
+ * @param {ArrayBuffer} prfBuffer
61
+ * @param {Uint8Array} infoLabel
62
+ * @returns {Promise<Uint8Array>}
63
+ */
64
+ export async function deriveScalarFromPrf(prfBuffer, infoLabel) {
65
+ for (let counter = 0; counter < 255; counter++) {
66
+ const info = new Uint8Array(infoLabel.length + 1);
67
+ info.set(infoLabel);
68
+ info[infoLabel.length] = counter;
69
+ const candidate = await hkdfExpand(prfBuffer, info, 32);
70
+ const scalar = bytesToBigInt(candidate);
71
+ if (scalar > 0n && scalar < SECP256K1_N) {
72
+ return candidate;
73
+ }
74
+ }
75
+ throw new Error("Failed to derive valid secp256k1 scalar from PRF");
76
+ }
77
+
78
+ /**
79
+ * @param {ArrayBuffer} prfBuffer
80
+ * @returns {Promise<Uint8Array>}
81
+ */
82
+ export async function deriveEd25519SeedFromPrf(prfBuffer) {
83
+ return hkdfExpand(prfBuffer, PRF_LABEL_ED25519, 32);
84
+ }
85
+
86
+ /**
87
+ * @param {PublicKeyCredential} credential
88
+ * @returns {Promise<{ secp256k1PrivateKey: Uint8Array, secp256k1PublicKey: Uint8Array, ed25519PublicKey: Uint8Array }>}
89
+ */
90
+ export async function deriveKeysFromCredential(credential) {
91
+ const rawPrf = /** @type {{ prf?: { results?: { first?: unknown } } }} */ (
92
+ credential.getClientExtensionResults()
93
+ ).prf?.results?.first;
94
+ const prfBuffer = normalizePrfOutputToArrayBuffer(rawPrf);
95
+ if (!prfBuffer) {
96
+ throw new Error("Passkey does not support PRF or PRF output missing");
97
+ }
98
+
99
+ const secp256k1PrivateKey = await deriveScalarFromPrf(
100
+ prfBuffer,
101
+ PRF_LABEL_SECP256K1,
102
+ );
103
+ const ed25519Seed = await deriveEd25519SeedFromPrf(prfBuffer);
104
+ // Uncompressed (0x04 ‖ X ‖ Y) — required by viem `publicKeyToAddress`.
105
+ const secp256k1PublicKey = secpGetPublicKey(secp256k1PrivateKey, false);
106
+ const ed25519PublicKey = await edGetPublicKeyAsync(ed25519Seed);
107
+
108
+ return { secp256k1PrivateKey, secp256k1PublicKey, ed25519PublicKey };
109
+ }
110
+
111
+ /**
112
+ * @param {Uint8Array} bytes
113
+ * @returns {bigint}
114
+ */
115
+ function bytesToBigInt(bytes) {
116
+ let result = 0n;
117
+ for (const b of bytes) result = (result << 8n) + BigInt(b);
118
+ return result;
119
+ }
@@ -0,0 +1,81 @@
1
+ import { parse0xHex, to0xHex } from "../hex.js";
2
+
3
+ const RECOVERY_VERSION = 1;
4
+ const PBKDF2_ITERATIONS = 250_000;
5
+
6
+ /**
7
+ * @param {string} passphrase
8
+ * @param {Uint8Array} salt
9
+ * @returns {Promise<CryptoKey>}
10
+ */
11
+ async function deriveAesKey(passphrase, salt) {
12
+ const enc = new TextEncoder();
13
+ const keyMaterial = await crypto.subtle.importKey(
14
+ "raw",
15
+ enc.encode(passphrase),
16
+ "PBKDF2",
17
+ false,
18
+ ["deriveKey"],
19
+ );
20
+ return crypto.subtle.deriveKey(
21
+ {
22
+ name: "PBKDF2",
23
+ salt,
24
+ iterations: PBKDF2_ITERATIONS,
25
+ hash: "SHA-256",
26
+ },
27
+ keyMaterial,
28
+ { name: "AES-GCM", length: 256 },
29
+ false,
30
+ ["encrypt", "decrypt"],
31
+ );
32
+ }
33
+
34
+ /**
35
+ * @param {Uint8Array} privateKey
36
+ * @param {string} passphrase
37
+ * @returns {Promise<string>}
38
+ */
39
+ export async function encryptPrivateKey(privateKey, passphrase) {
40
+ const salt = crypto.getRandomValues(new Uint8Array(16));
41
+ const iv = crypto.getRandomValues(new Uint8Array(12));
42
+ const aesKey = await deriveAesKey(passphrase, salt);
43
+ const ciphertext = new Uint8Array(
44
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aesKey, privateKey),
45
+ );
46
+ const payload = new Uint8Array(
47
+ 1 + salt.length + iv.length + ciphertext.length,
48
+ );
49
+ payload[0] = RECOVERY_VERSION;
50
+ payload.set(salt, 1);
51
+ payload.set(iv, 1 + salt.length);
52
+ payload.set(ciphertext, 1 + salt.length + iv.length);
53
+ return `ows1:${to0xHex(payload)}`;
54
+ }
55
+
56
+ /**
57
+ * @param {string} envelope
58
+ * @returns {Promise<Uint8Array>}
59
+ */
60
+ export async function decryptPrivateKey(envelope, passphrase) {
61
+ if (!envelope.startsWith("ows1:")) {
62
+ throw new Error("invalidRecoveryEnvelope");
63
+ }
64
+ const bytes = parse0xHex(envelope.slice(5));
65
+ if (bytes[0] !== RECOVERY_VERSION) {
66
+ throw new Error("unsupportedRecoveryVersion");
67
+ }
68
+ const salt = bytes.slice(1, 17);
69
+ const iv = bytes.slice(17, 29);
70
+ const ciphertext = bytes.slice(29);
71
+ const aesKey = await deriveAesKey(passphrase, salt);
72
+ try {
73
+ const plain = new Uint8Array(
74
+ await crypto.subtle.decrypt({ name: "AES-GCM", iv }, aesKey, ciphertext),
75
+ );
76
+ if (plain.length !== 32) throw new Error("invalidDecryptedKey");
77
+ return plain;
78
+ } catch {
79
+ throw new Error("decryptionFailed");
80
+ }
81
+ }
@@ -0,0 +1,59 @@
1
+ import { to0xHex } from "../hex.js";
2
+ import { signAsync as edSignAsync } from "./vendor/noble-ed25519.js";
3
+ import { signAsync as secpSignAsync } from "./vendor/noble-secp256k1.js";
4
+
5
+ /**
6
+ * @param {import('../constants.js').SignScheme} scheme
7
+ * @param {Uint8Array} digest
8
+ * @param {Uint8Array} secp256k1PrivateKey
9
+ * @param {Uint8Array} ed25519Seed
10
+ * @returns {Promise<`0x${string}`>}
11
+ */
12
+ export async function signWithScheme(
13
+ scheme,
14
+ digest,
15
+ secp256k1PrivateKey,
16
+ ed25519Seed,
17
+ ) {
18
+ switch (scheme) {
19
+ case "secp256k1-ecdsa": {
20
+ const sig = await secpSignAsync(digest, secp256k1PrivateKey, {
21
+ lowS: true,
22
+ });
23
+ return to0xHex(sig.toCompactRawBytes());
24
+ }
25
+ case "secp256k1-ecdsa-recoverable": {
26
+ const sig = await secpSignAsync(digest, secp256k1PrivateKey, {
27
+ lowS: true,
28
+ });
29
+ const compact = sig.toCompactRawBytes();
30
+ const recovery = sig.recovery ?? 0;
31
+ const out = new Uint8Array(65);
32
+ out.set(compact, 0);
33
+ out[64] = recovery;
34
+ return to0xHex(out);
35
+ }
36
+ case "secp256k1-bip340":
37
+ throw new Error("schemeNotImplemented");
38
+ case "ed25519": {
39
+ const sig = await edSignAsync(digest, ed25519Seed);
40
+ return to0xHex(sig);
41
+ }
42
+ default:
43
+ throw new Error("unknownScheme");
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {import('../constants.js').SignScheme} scheme
49
+ * @param {Uint8Array} payload
50
+ */
51
+ export function validateSignPayload(scheme, payload) {
52
+ if (scheme === "ed25519") {
53
+ if (payload.length === 0) throw new Error("invalidPayload");
54
+ return;
55
+ }
56
+ if (payload.length !== 32) {
57
+ throw new Error("digestMustBe32Bytes");
58
+ }
59
+ }