From 1fccd37834c3adf7c455c5df9229b1a48744dd5c Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Wed, 23 Sep 2026 13:19:45 -0700 Subject: [PATCH] Use custom abort handler. --- asconfig.json | 7 ++- src/assembly/abort.ts | 50 +++++++++++++++++ src/assembly/errors.ts | 49 ++++++++++++++--- src/assembly/index.ts | 54 +++++++++---------- src/assembly/tests.ts | 34 +++++++----- src/lib/errors.ts | 118 +++++++++++++++++++++++++++++++++++++++++ src/lib/sign.ts | 7 +-- src/lib/wasm.ts | 32 ++++++++--- test/node.mjs | 2 +- 9 files changed, 290 insertions(+), 63 deletions(-) create mode 100644 src/assembly/abort.ts create mode 100644 src/lib/errors.ts diff --git a/asconfig.json b/asconfig.json index fa1b917..480f0cb 100644 --- a/asconfig.json +++ b/asconfig.json @@ -21,6 +21,9 @@ "disable": [ "mutable-globals" ], - "noColors": true + "noColors": true, + "use": [ + "abort=src/assembly/abort/abort" + ] } -} +} \ No newline at end of file diff --git a/src/assembly/abort.ts b/src/assembly/abort.ts new file mode 100644 index 0000000..dd9a3da --- /dev/null +++ b/src/assembly/abort.ts @@ -0,0 +1,50 @@ +//! SPDX-FileCopyrightText: 2026 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { ERROR_UNKNOWN, } from './errors' + +/** + * The module's only import, installed as AssemblyScript's `abort` by + * `use: ["abort=src/assembly/abort/abort"]` in `asconfig.json`. + * + * It takes four integers and no pointers, so the host never reads wasm memory + * to report an error. The previous handler decoded a UTF-16 string out of + * linear memory, which is the pointer arithmetic this replaces. + */ +@external('env', 'abort') +declare function env_abort (code: i32, line: i32, a: i32, b: i32): void + +let errorCode: i32 = ERROR_UNKNOWN +let detailA: i32 = 0 +let detailB: i32 = 0 + +/** + * Throws, reporting `code` and up to two context integers to the host. + * + * **Must stay `@inline`.** AssemblyScript fills in the line number at the + * `throw`, so inlining is what makes the host report the caller's line rather + * than this file's. + */ +//@ts-expect-error +@inline +export function raise (code: i32, a: i32 = 0, b: i32 = 0): void { + errorCode = code + detailA = a + detailB = b + throw new Error() +} + +/** + * Reads the pending code, resets it, and hands it to the host. Resetting + * *before* the call matters: `env_abort` throws, so nothing after it runs, and + * a stale code would otherwise be attributed to the next uncoded abort. + */ +export function abort (message: string | null, fileName: string | null, line: u32, col: u32): void { + const code = errorCode + const a = detailA + const b = detailB + errorCode = ERROR_UNKNOWN + detailA = 0 + detailB = 0 + env_abort(code, i32(line), a, b) +} diff --git a/src/assembly/errors.ts b/src/assembly/errors.ts index c318d9e..ed4e6e5 100644 --- a/src/assembly/errors.ts +++ b/src/assembly/errors.ts @@ -1,9 +1,46 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -export const errors = [ - '', - 'Invalid public key', - 'Invalid message length', - 'Invalid block count' -] \ No newline at end of file +/** + * Error codes reported to the host through `env.abort`. These values are a + * public contract: `src/lib/errors.ts` turns them back into message text, and + * they are re-exported as wasm globals so the host reads the numbers from the + * module rather than restating them. + * + * `ERROR_UNKNOWN` is the resting value. An abort that reports it came from a + * site that never called `raise` — the stub allocator running out of heap, or + * a `StaticArray` constructor rejecting its size. Both are init-time. + */ +export const ERROR_UNKNOWN: i32 = 0 +export const ERROR_INVALID_PUBLIC_KEY: i32 = 1 +export const ERROR_INVALID_MESSAGE_LENGTH: i32 = 2 +export const ERROR_INVALID_BLOCK_COUNT: i32 = 3 +/** + * Raised by the byte accessors. `a` is the buffer id below, `b` the offending + * index or value. The buffer id is what identifies the site: `line` cannot, + * because AssemblyScript fixes it at the `throw` inside `raise`, so every + * coded abort reports that one line. `line` is only meaningful for + * `ERROR_UNKNOWN`, where it points into the AssemblyScript runtime. + */ +export const ERROR_INDEX_OUT_OF_RANGE: i32 = 4 +export const ERROR_BYTE_OUT_OF_RANGE: i32 = 5 + +export const BUFFER_INPUT_MSG: i32 = 0 +export const BUFFER_INPUT_PRV: i32 = 1 +export const BUFFER_INPUT_PUB: i32 = 2 +export const BUFFER_INPUT_SIG: i32 = 3 +export const BUFFER_OUTPUT_DERIVE: i32 = 4 +export const BUFFER_OUTPUT_SIGN: i32 = 5 +export const BUFFER_OUTPUT_VERIFY: i32 = 6 + +/** + * `equalbytes` start-function guard. `a` is the vector index (0 `zeros`, + * 1 `ones`, 2 `spread`); `b` encodes the corruption the comparison failed to + * detect, and is 0 for `SELFTEST_XPASS`. + * + * - `SELFTEST_XFAIL_ONE`: `b` is `(position << 8) | mask` + * - `SELFTEST_XFAIL_PAIR`: `b` is `(position << 8) | position`, both `^= 0x80` + */ +export const ERROR_SELFTEST_XPASS: i32 = 6 +export const ERROR_SELFTEST_XFAIL_ONE: i32 = 7 +export const ERROR_SELFTEST_XFAIL_PAIR: i32 = 8 diff --git a/src/assembly/index.ts b/src/assembly/index.ts index d300801..a51f863 100644 --- a/src/assembly/index.ts +++ b/src/assembly/index.ts @@ -5,9 +5,13 @@ import { BLOCKHASH_BYTELENGTH, KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, MAX_VERIF import { crypto_derive, } from './crypto_derive' import { crypto_sign, } from './crypto_sign' import { crypto_verify_decodepubkey, crypto_verify_donna, crypto_verify_sodium, } from './crypto_verify' +import { BUFFER_INPUT_MSG, BUFFER_INPUT_PRV, BUFFER_INPUT_PUB, BUFFER_INPUT_SIG, BUFFER_OUTPUT_DERIVE, BUFFER_OUTPUT_SIGN, BUFFER_OUTPUT_VERIFY, ERROR_INDEX_OUT_OF_RANGE, ERROR_BYTE_OUT_OF_RANGE, ERROR_INVALID_BLOCK_COUNT, ERROR_INVALID_MESSAGE_LENGTH, ERROR_INVALID_PUBLIC_KEY, ERROR_SELFTEST_XFAIL_ONE, ERROR_SELFTEST_XFAIL_PAIR, ERROR_SELFTEST_XPASS, ERROR_UNKNOWN, } from './errors' +import { raise, } from './abort' import './tests' export { BLOCKHASH_BYTELENGTH, KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, MAX_VERIFY_BLOCKS, MAX_VERIFY_BLOCKS_BYTELENGTH, SIGNATURE_BYTELENGTH } +export { BUFFER_INPUT_MSG, BUFFER_INPUT_PRV, BUFFER_INPUT_PUB, BUFFER_INPUT_SIG, BUFFER_OUTPUT_DERIVE, BUFFER_OUTPUT_SIGN, BUFFER_OUTPUT_VERIFY } +export { ERROR_INDEX_OUT_OF_RANGE, ERROR_BYTE_OUT_OF_RANGE, ERROR_INVALID_BLOCK_COUNT, ERROR_INVALID_MESSAGE_LENGTH, ERROR_INVALID_PUBLIC_KEY, ERROR_SELFTEST_XFAIL_ONE, ERROR_SELFTEST_XFAIL_PAIR, ERROR_SELFTEST_XPASS, ERROR_UNKNOWN } // Static I/O buffers const INPUT_MSG = new StaticArray(MAX_MESSAGE_BYTELENGTH) @@ -31,57 +35,53 @@ export function clearMemory (): void { } /** Sets a byte in the static message input buffer. */ -export function setInputMsgByte (index: i32, v0: i32): i32 { +export function setInputMsgByte (index: i32, v0: i32): void { if (index < 0 || MAX_MESSAGE_BYTELENGTH <= index) { - return -1 + raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_MSG, index) } if (v0 < 0 || 0xff < v0) { - return -2 + raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_MSG, v0) } INPUT_MSG[index] = u8(v0) - return 0 } /** Sets a byte in the static private key input buffer (32 bytes). */ -export function setInputPrvByte (index: i32, value: i32): i32 { +export function setInputPrvByte (index: i32, value: i32): void { if (index < 0 || KEY_BYTELENGTH <= index) { - return -1 + raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_PRV, index) } if (value < 0 || 0xff < value) { - return -2 + raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_PRV, value) } INPUT_PRV[index] = u8(value) - return 0 } /** Sets a byte in the static public key input buffer (32 bytes). */ -export function setInputPubByte (index: i32, value: i32): i32 { +export function setInputPubByte (index: i32, value: i32): void { if (index < 0 || KEY_BYTELENGTH <= index) { - return -1 + raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_PUB, index) } if (value < 0 || 0xff < value) { - return -2 + raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_PUB, value) } INPUT_PUB[index] = u8(value) - return 0 } /** Sets a byte in the static signature input buffer (64 bytes). */ -export function setInputSigByte (index: i32, value: i32): i32 { +export function setInputSigByte (index: i32, value: i32): void { if (index < 0 || SIGNATURE_BYTELENGTH <= index) { - return -1 + raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_INPUT_SIG, index) } if (value < 0 || 0xff < value) { - return -2 + raise(ERROR_BYTE_OUT_OF_RANGE, BUFFER_INPUT_SIG, value) } INPUT_SIG[index] = u8(value) - return 0 } /** Gets a byte from the derive() static output buffer (32 bytes). */ export function getOutputDeriveByte (index: i32): i32 { if (index < 0 || KEY_BYTELENGTH <= index) { - return -1 + raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_OUTPUT_DERIVE, index) } return OUTPUT_DERIVE[index] } @@ -89,7 +89,7 @@ export function getOutputDeriveByte (index: i32): i32 { /** Gets a byte from the sign() static output buffer (64 bytes). */ export function getOutputSignByte (index: i32): i32 { if (index < 0 || SIGNATURE_BYTELENGTH <= index) { - return -1 + raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_OUTPUT_SIGN, index) } return OUTPUT_SIGN[index] } @@ -97,7 +97,7 @@ export function getOutputSignByte (index: i32): i32 { /** Gets a byte from the verify() static output buffer (64 bytes). */ export function getOutputVerifyByte (index: i32): i32 { if (index < 0 || MAX_VERIFY_BLOCKS <= index) { - return -1 + raise(ERROR_INDEX_OUT_OF_RANGE, BUFFER_OUTPUT_VERIFY, index) } return OUTPUT_VERIFY[index] } @@ -149,7 +149,7 @@ export function derive (): void { * from the message and written to the output buffer. * @param {i32} mlen Byte length of message to be signed, up to 65536 */ -export function sign (mlen: i32): i32 { +export function sign (mlen: i32): void { // Clear output buffer so errors do not retain stale prior data OUTPUT_SIGN.fill(0) @@ -163,7 +163,7 @@ export function sign (mlen: i32): i32 { INPUT_PRV.fill(0) INPUT_PUB.fill(0) INPUT_MSG.fill(0) - return -2 + raise(ERROR_INVALID_MESSAGE_LENGTH, mlen, MAX_MESSAGE_BYTELENGTH) } // Copy input buffers to local parameters, then clear input buffer @@ -181,8 +181,8 @@ export function sign (mlen: i32): i32 { // Clear signature output buffer if signing failed if (result != 0) { OUTPUT_SIGN.fill(0) + raise(ERROR_INVALID_PUBLIC_KEY) } - return result } /** @@ -191,7 +191,7 @@ export function sign (mlen: i32): i32 { * @param {i32} mlen Byte length of message that was signed, up to 65536 * @returns {boolean} True if message was signed by public key's private key */ -export function verify (mlen: i32): i32 { +export function verify (mlen: i32): void { // Clear output buffer so errors do not retain stale prior data OUTPUT_VERIFY.fill(0) @@ -203,7 +203,7 @@ export function verify (mlen: i32): i32 { if (mlen < 0 || mlen > MAX_MESSAGE_BYTELENGTH) { INPUT_MSG.fill(0) INPUT_PUB.fill(0) - return -2 + raise(ERROR_INVALID_MESSAGE_LENGTH, mlen, MAX_MESSAGE_BYTELENGTH) } // Copy input buffer to local parameters, then clear input buffer @@ -219,7 +219,6 @@ export function verify (mlen: i32): i32 { INPUT_MSG.fill(0) pub.fill(0) sig.fill(0) - return 0 } /** @@ -239,7 +238,7 @@ export function verify (mlen: i32): i32 { * to the output buffer in the same order they were received. * @param {i32} count Number of signatures to verify, up to 64 */ -export function verify_blocks (count: i32): i32 { +export function verify_blocks (count: i32): void { // Clear output buffer so errors do not retain stale prior data OUTPUT_VERIFY.fill(0) @@ -252,7 +251,7 @@ export function verify_blocks (count: i32): i32 { if (count < 1 || MAX_VERIFY_BLOCKS < count) { INPUT_MSG.fill(0) INPUT_PUB.fill(0) - return -3 + raise(ERROR_INVALID_BLOCK_COUNT, count, MAX_VERIFY_BLOCKS) } // Copy public key input buffer to local parameter, then clear input buffer @@ -278,5 +277,4 @@ export function verify_blocks (count: i32): i32 { h.fill(0) pub.fill(0) sig.fill(0) - return 0 } diff --git a/src/assembly/tests.ts b/src/assembly/tests.ts index 34691bc..ed4d512 100644 --- a/src/assembly/tests.ts +++ b/src/assembly/tests.ts @@ -1,25 +1,34 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later +import { raise, } from './abort' import { KEY_BYTELENGTH } from './constants' +import { ERROR_SELFTEST_XFAIL_ONE, ERROR_SELFTEST_XFAIL_PAIR, ERROR_SELFTEST_XPASS, } from './errors' import { equalbytes } from './utils' /** * Tests run on initialization to ensure build is correct. + * + * Failures report a code and two integers rather than a message: building the + * message here would pull String#join and the whole itoa path into the module + * for a path that runs once and then traps. The host reconstructs the text + * from `vector` and `detail` — see `src/lib/errors.ts`. */ -function err (fname: string, vector: string, expected: string, actual: string): void { - throw new Error(`${fname} build error from "${vector}": expected ${expected}; actual ${actual}`) +function xpass_equalbytes (vector: i32, expected: StaticArray, actual: StaticArray): void { + if (!equalbytes(expected, actual, KEY_BYTELENGTH)) { + raise(ERROR_SELFTEST_XPASS, vector, 0) + } } -function xpass_equalbytes (vector: string, expected: StaticArray, actual: StaticArray): void { - if (!equalbytes(expected, actual, KEY_BYTELENGTH)) { - err('equalbytes', vector, `pass [${expected}]`, `fail [${actual}]`) +function xfail_equalbytes_one (vector: i32, position: i32, mask: i32, expected: StaticArray, actual: StaticArray): void { + if (equalbytes(expected, actual, KEY_BYTELENGTH)) { + raise(ERROR_SELFTEST_XFAIL_ONE, vector, (position << 8) | mask) } } -function xfail_equalbytes (vector: string, expected: StaticArray, actual: StaticArray): void { +function xfail_equalbytes_pair (vector: i32, i: i32, j: i32, expected: StaticArray, actual: StaticArray): void { if (equalbytes(expected, actual, KEY_BYTELENGTH)) { - err('equalbytes', vector, `fail [${expected}]`, `pass [${actual}]`) + raise(ERROR_SELFTEST_XFAIL_PAIR, vector, (i << 8) | j) } } @@ -37,23 +46,22 @@ for (let i = 0; i < KEY_BYTELENGTH; i++) { spread[i] = u8(i * 37 + 11) } -const vectors = ['zeros', 'ones', 'spread',] +// Vector index is the error detail: 0 zeros, 1 ones, 2 spread const expecteds = [zeros, ones, spread,] const actual = new StaticArray(KEY_BYTELENGTH) -for (let v = 0; v < vectors.length; v++) { - const vector = vectors[v] +for (let v = 0; v < expecteds.length; v++) { const expected = expecteds[v] memory.copy(changetype(actual), changetype(expected), KEY_BYTELENGTH) - xpass_equalbytes(vector, expected, actual) + xpass_equalbytes(v, expected, actual) for (let i = 0; i < KEY_BYTELENGTH; i++) { // verify any individual bit difference fails for (let j = 1; j < 256; j++) { actual[i] ^= u8(j) - xfail_equalbytes(vector, expected, actual) + xfail_equalbytes_one(v, i, j, expected, actual) actual[i] = expected[i] } @@ -62,7 +70,7 @@ for (let v = 0; v < vectors.length; v++) { for (let j = i + 1; j < KEY_BYTELENGTH; j++) { actual[i] ^= 128 actual[j] ^= 128 - xfail_equalbytes(vector, expected, actual) + xfail_equalbytes_pair(v, i, j, expected, actual) actual[i] = expected[i] actual[j] = expected[j] } diff --git a/src/lib/errors.ts b/src/lib/errors.ts new file mode 100644 index 0000000..9ebeb1a --- /dev/null +++ b/src/lib/errors.ts @@ -0,0 +1,118 @@ +//! SPDX-FileCopyrightText: 2026 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { Nano25519WasmError } from './primordials' + +/** + * Message text for the codes in `src/assembly/errors.ts`. The numbers are the + * contract between the two files; `wasm.ts` asserts after instantiation that + * every `ERROR_*` global the module exports still matches the key used here, + * so the two cannot drift silently. + * + * The table is keyed by number rather than read from the module because the + * handler has to exist *before* instantiation: the `equalbytes` guard runs in + * the start function, so it can abort while `exports` is still undefined. + */ +export const ERROR_UNKNOWN = 0 +export const ERROR_INVALID_PUBLIC_KEY = 1 +export const ERROR_INVALID_MESSAGE_LENGTH = 2 +export const ERROR_INVALID_BLOCK_COUNT = 3 +export const ERROR_INDEX_OUT_OF_RANGE = 4 +export const ERROR_BYTE_OUT_OF_RANGE = 5 +export const ERROR_SELFTEST_XPASS = 6 +export const ERROR_SELFTEST_XFAIL_ONE = 7 +export const ERROR_SELFTEST_XFAIL_PAIR = 8 + +const VECTORS = ['zeros', 'ones', 'spread'] + +/** Buffer ids from `src/assembly/errors.ts`, in value order. */ +const BUFFERS = ['INPUT_MSG', 'INPUT_PRV', 'INPUT_PUB', 'INPUT_SIG', 'OUTPUT_DERIVE', 'OUTPUT_SIGN', 'OUTPUT_VERIFY'] + +/** + * Source file per code. `env.abort` no longer receives a file pointer, so the + * code carries that instead — which holds only while **each code is raised + * from exactly one file**. Reusing a code across files makes this lie. + */ +const FILES: Record = { + [ERROR_INVALID_PUBLIC_KEY]: 'src/assembly/index.ts', + [ERROR_INVALID_MESSAGE_LENGTH]: 'src/assembly/index.ts', + [ERROR_INVALID_BLOCK_COUNT]: 'src/assembly/index.ts', + [ERROR_INDEX_OUT_OF_RANGE]: 'src/assembly/index.ts', + [ERROR_BYTE_OUT_OF_RANGE]: 'src/assembly/index.ts', + [ERROR_SELFTEST_XPASS]: 'src/assembly/tests.ts', + [ERROR_SELFTEST_XFAIL_ONE]: 'src/assembly/tests.ts', + [ERROR_SELFTEST_XFAIL_PAIR]: 'src/assembly/tests.ts' +} + +/** + * Rebuilds a self-test vector. These are the same three definitions as + * `src/assembly/tests.ts`; keeping them here is what lets the host report the + * failing bytes without the module ever formatting a string. + */ +function vector (index: number, length: number): number[] { + const bytes: number[] = [] + for (let i = 0; i < length; i++) { + bytes[i] = index === 0 ? 0 : index === 1 ? 255 : (i * 37 + 11) & 0xff + } + return bytes +} + +function selftest (code: number, v: number, detail: number, length: number): string { + const name = VECTORS[v] ?? `vector ${v}` + const expected = vector(v, length) + const actual = vector(v, length) + if (code === ERROR_SELFTEST_XFAIL_ONE) { + const position = (detail >> 8) & 0xff + const mask = detail & 0xff + actual[position] ^= mask + return `equalbytes build error from "${name}": expected fail [${expected}]; actual pass [${actual}] (byte ${position} ^ 0x${mask.toString(16).padStart(2, '0')})` + } + if (code === ERROR_SELFTEST_XFAIL_PAIR) { + const i = (detail >> 8) & 0xff + const j = detail & 0xff + actual[i] ^= 0x80 + actual[j] ^= 0x80 + return `equalbytes build error from "${name}": expected fail [${expected}]; actual pass [${actual}] (bytes ${i} and ${j} ^ 0x80)` + } + return `equalbytes build error from "${name}": expected pass [${expected}]; actual fail` +} + +/** Turns the four integers `env.abort` reports into a message. */ +export function describe (code: number, line: number, a: number, b: number): string { + let text: string + switch (code) { + case ERROR_INVALID_PUBLIC_KEY: + text = 'Invalid public key' + break + case ERROR_INVALID_MESSAGE_LENGTH: + text = `Invalid message length: ${a} is not in [0, ${b}]` + break + case ERROR_INVALID_BLOCK_COUNT: + text = `Invalid block count: ${a} is not in [1, ${b}]` + break + case ERROR_INDEX_OUT_OF_RANGE: + text = `Buffer index out of range for ${BUFFERS[a] ?? `buffer ${a}`}: ${b}` + break + case ERROR_BYTE_OUT_OF_RANGE: + text = `Byte value out of range for ${BUFFERS[a] ?? `buffer ${a}`}: ${b} is not in [0, 255]` + break + case ERROR_SELFTEST_XPASS: + case ERROR_SELFTEST_XFAIL_ONE: + case ERROR_SELFTEST_XFAIL_PAIR: + text = selftest(code, a, b, 32) + break + default: + text = `Unrecognized wasm abort (code ${code}, a ${a}, b ${b})` + } + // `line` only locates an ERROR_UNKNOWN abort. Every coded site reports the + // `throw` inside `raise`, because AssemblyScript fixes the line before + // inlining — so for coded errors the code and its details are the location. + return code === ERROR_UNKNOWN + ? `${text}, row ${line}` + : `${text}, ${FILES[code] ?? 'unknown file'}` +} + +/** The module's only import. Takes four integers and never reads wasm memory. */ +export function abort (code: number, line: number, a: number, b: number): never { + throw new Nano25519WasmError(describe(code, line, a, b)) +} diff --git a/src/lib/sign.ts b/src/lib/sign.ts index b9e5a04..b5d069b 100644 --- a/src/lib/sign.ts +++ b/src/lib/sign.ts @@ -1,7 +1,7 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later -import { allocate, byteLength, bytesToHex, copy, fill, isBytes, Nano25519WasmError, normalize } from './primordials' +import { allocate, byteLength, bytesToHex, copy, fill, isBytes, normalize } from './primordials' import { clearMemory, constants, MemoryBuffers, Mutex, sign as wasm_sign } from './wasm' const { KEY_BYTELENGTH, MAX_MESSAGE_BYTELENGTH, SIGNATURE_BYTELENGTH, } = constants @@ -25,10 +25,7 @@ export function sign (msg: unknown, prv: unknown, pub: unknown, out?: unknown): for (let i = 0; i < KEY_BYTELENGTH; i++) { MemoryBuffers.INPUT_PUB(i)(publicKey[i]) } - const result = wasm_sign(message.byteLength) - if (result !== 0) { - throw new Nano25519WasmError('Invalid public key') - } + wasm_sign(message.byteLength) for (let i = 0; i < SIGNATURE_BYTELENGTH; i++) { signature[i] = MemoryBuffers.OUTPUT_SIGN(i) } diff --git a/src/lib/wasm.ts b/src/lib/wasm.ts index cb62d4b..7fd6c7c 100644 --- a/src/lib/wasm.ts +++ b/src/lib/wasm.ts @@ -4,17 +4,18 @@ //@ts-expect-error import nano25519_wasm from '../../build/nano25519.wasm' import { Nano25519TypeError, copy } from './primordials' +import * as errors from './errors' type Exports = WebAssembly.Instance['exports'] & { clearMemory: () => void derive: () => void - sign: (mlen: number) => number - verify: (mlen: number) => number - verify_blocks: (count: number) => number - setInputMsgByte: (index: number, value: number) => number - setInputPrvByte: (index: number, value: number) => number - setInputPubByte: (index: number, value: number) => number - setInputSigByte: (index: number, value: number) => number + sign: (mlen: number) => void + verify: (mlen: number) => void + verify_blocks: (count: number) => void + setInputMsgByte: (index: number, value: number) => void + setInputPrvByte: (index: number, value: number) => void + setInputPubByte: (index: number, value: number) => void + setInputSigByte: (index: number, value: number) => void getOutputDeriveByte: (index: number) => number getOutputSignByte: (index: number) => number getOutputVerifyByte: (index: number) => number @@ -37,9 +38,24 @@ export const Mutex = { const wasm = copy(nano25519_wasm) const module = new WebAssembly.Module(wasm) -const instance = new WebAssembly.Instance(module, { env: { abort: () => { } } }) +const instance = new WebAssembly.Instance(module, { env: { abort: errors.abort } }) const exports = instance.exports as Exports +/** + * The error codes are a contract between `src/assembly/errors.ts` and + * `src/lib/errors.ts`. The host table cannot be read from the module, because + * the handler has to exist before instantiation, so it is checked against the + * module here instead. A mismatch means one side was renumbered alone. + */ +for (const name of Object.keys(errors)) { + if (!name.startsWith('ERROR_')) continue + const global = exports[name] as WebAssembly.Global | undefined + if (global == null) throw new Nano25519TypeError(`Missing wasm error code ${name}`) + if (global.value !== (errors as Record)[name]) { + throw new Nano25519TypeError(`Error code ${name} is ${global.value} in wasm, ${(errors as Record)[name]} in host`) + } +} + const BLOCKHASH_BYTELENGTH = exports.BLOCKHASH_BYTELENGTH.value const KEY_BYTELENGTH = exports.KEY_BYTELENGTH.value const SIGNATURE_BYTELENGTH = exports.SIGNATURE_BYTELENGTH.value diff --git a/test/node.mjs b/test/node.mjs index cc82c92..e22a5a9 100644 --- a/test/node.mjs +++ b/test/node.mjs @@ -164,7 +164,7 @@ try { result = false } catch (err) { if (err != null && typeof err === 'object' && 'message' in err && typeof err.message === 'string') { - result = err.constructor.name === 'Nano25519WasmError' && err.message === 'Invalid public key' + result = err.constructor.name === 'Nano25519WasmError' && err.message === 'Invalid public key, src/assembly/index.ts' } else { result = false } -- 2.52.0