From ef0dcbc5542f21f2dfa7e2208d66683511657957 Mon Sep 17 00:00:00 2001 From: CedarMist <134699267+CedarMist@users.noreply.github.com> Date: Sat, 26 Oct 2024 21:17:39 +0200 Subject: [PATCH] calldata public key verification using new subcalls --- clients/js/src/calldatapublickey.ts | 121 ++-- clients/js/src/cipher.ts | 12 +- clients/js/src/munacl.ts | 640 ++++++++++++++++++++++ clients/js/test/calldatapublickey.spec.ts | 112 +++- clients/js/test/munacl.spec.ts | 342 ++++++++++++ runtime/Cargo.lock | 19 +- runtime/Cargo.toml | 4 +- runtime/Makefile | 8 +- 8 files changed, 1189 insertions(+), 69 deletions(-) diff --git a/clients/js/src/calldatapublickey.ts b/clients/js/src/calldatapublickey.ts index 6d599025..ca197275 100644 --- a/clients/js/src/calldatapublickey.ts +++ b/clients/js/src/calldatapublickey.ts @@ -5,6 +5,8 @@ import { fromQuantity, getBytes } from './ethersutils.js'; import type { EIP2696_EthereumProvider } from './provider.js'; import { SUBCALL_ADDR, CALLDATAPUBLICKEY_CALLDATA } from './constants.js'; import { Cipher, X25519DeoxysII } from './cipher.js'; +import { ed25519_verify_raw_cofactorless } from './munacl.js'; +import { sha512_256 } from '@noble/hashes/sha512'; /** * calldata public keys are cached for this amount of time @@ -13,51 +15,58 @@ import { Cipher, X25519DeoxysII } from './cipher.js'; */ const DEFAULT_PUBKEY_CACHE_EXPIRATION_MS = 60 * 5 * 1000; +const BIGINT_ZERO = BigInt(0); +const UINT64_MAX = BigInt(1) << BigInt(64); +const BIGINT_EIGHT = BigInt(8); + // ----------------------------------------------------------------------------- // Fetch calldata public key // Well use provider when possible, and fallback to HTTP(S)? requests // e.g. MetaMask doesn't allow the oasis_callDataPublicKey JSON-RPC method -export type RawCallDataPublicKeyResponseResult = { - key: string; - checksum: string; - signature: string; - epoch: number; -}; - -export type RawCallDataPublicKeyResponse = { - result: RawCallDataPublicKeyResponseResult; -}; - export class FetchError extends Error { public constructor(message: string, public readonly response?: unknown) { super(message); } } -export interface CallDataPublicKey { - // PublicKey is the requested public key. +export interface SignedPubicKey { + /// PublicKey is the requested public key. key: Uint8Array; - // Checksum is the checksum of the key manager state. + /// Checksum is the checksum of the key manager state. checksum: Uint8Array; - // Signature is the Sign(sk, (key || checksum)) from the key manager. + /// Sign(sk, (key || checksum || runtime id || key pair id || epoch || expiration epoch)) from the key manager. signature: Uint8Array; - // Epoch is the epoch of the ephemeral runtime key. - epoch: number; + /// At which epoch does this key become invalid + expiration?: bigint; +} + +export interface CallDataPublicKey { + /// Signed public key + public_key: SignedPubicKey; + + /// Epoch is the epoch of the ephemeral runtime key. + epoch?: bigint; + + /// Which runtime is the ephemeral public key for + runtime_id: Uint8Array; + + key_pair_id: Uint8Array; +} - // Which chain ID is this key for? +export interface CachedCallDataPublicKey extends CallDataPublicKey { + /// Which chain ID is this key for? chainId: number; - // When was the key fetched + /// When was the key fetched fetched: Date; } function parseBigIntFromByteArray(bytes: Uint8Array): bigint { - const eight = BigInt(8); - return bytes.reduce((acc, byte) => (acc << eight) | BigInt(byte), BigInt(0)); + return bytes.reduce((acc, byte) => (acc << BIGINT_EIGHT) | BigInt(byte), BIGINT_ZERO); } class AbiDecodeError extends Error {} @@ -78,8 +87,49 @@ function parseAbiEncodedUintBytes(bytes: Uint8Array): [bigint, Uint8Array] { if (bytes.length < offset + 32 + data_length) { throw new AbiDecodeError('too short, data'); } - const data = bytes.slice(offset + 32, offset + 32 + data_length); - return [status, data]; + return [status, bytes.slice(offset + 32, offset + 32 + data_length)]; +} + +function u64tobytes(x: bigint|number): Uint8Array { + const y = BigInt(x); + if (y < BIGINT_ZERO || y > UINT64_MAX) { + throw new Error('Value out of range for uint64'); + } + const buffer = new ArrayBuffer(8); + new DataView(buffer).setBigUint64(0, y, false); // false for big-endian + return new Uint8Array(buffer); +} + +const PUBLIC_KEY_SIGNATURE_CONTEXT = new TextEncoder().encode( + 'oasis-core/keymanager: pk signature', +); + +export function verifyRuntimePublicKey( + signerPk: Uint8Array, + cdpk: CallDataPublicKey, +): boolean { + let body = new Uint8Array([ + ...cdpk.public_key.key, + ...cdpk.public_key.checksum, + ...cdpk.runtime_id, + ...cdpk.key_pair_id, + ]); + + if (cdpk.epoch !== undefined) { + body = new Uint8Array([...body, ...u64tobytes(cdpk.epoch)]); + } + + const expiration = cdpk.public_key.expiration; + if (expiration !== undefined) { + body = new Uint8Array([...body, ...u64tobytes(expiration)]); + } + + const digest = sha512_256.create() + .update(PUBLIC_KEY_SIGNATURE_CONTEXT) + .update(body) + .digest(); + + return ed25519_verify_raw_cofactorless(cdpk.public_key.signature, signerPk, digest); } /** @@ -91,7 +141,7 @@ function parseAbiEncodedUintBytes(bytes: Uint8Array): [bigint, Uint8Array] { */ export async function fetchRuntimePublicKey(args: { upstream: EIP2696_EthereumProvider; -}) { +}): Promise { let chainId: number | undefined = undefined; const { upstream } = args; @@ -117,27 +167,24 @@ export async function fetchRuntimePublicKey(args: { // NOTE: to avoid pulling-in a full ABI decoder dependency, slice it manually const [resp_status, resp_cbor] = parseAbiEncodedUintBytes(resp_bytes); - if (resp_status !== BigInt(0)) { + if (resp_status !== BIGINT_ZERO) { throw new Error(`fetchRuntimePublicKey - invalid status: ${resp_status}`); } - const response = cborDecode(resp_cbor); + // TODO: validate resp_cbor? return { - key: response.public_key.key, - checksum: response.public_key.checksum, - signature: response.public_key.signature, - epoch: response.epoch, + ...cborDecode(resp_cbor) as CallDataPublicKey, chainId, fetched: new Date(), - } as CallDataPublicKey; + }; } /** * Retrieves calldata public keys from RPC provider */ export class KeyFetcher { - public pubkey?: CallDataPublicKey; + public pubkey?: CachedCallDataPublicKey; constructor( readonly timeoutMilliseconds: number = DEFAULT_PUBKEY_CACHE_EXPIRATION_MS, @@ -167,15 +214,15 @@ export class KeyFetcher { } public async cipher(upstream: EIP2696_EthereumProvider): Promise { - const { key, epoch } = await this.fetch(upstream); - return X25519DeoxysII.ephemeral(key, epoch); + const { public_key, epoch } = await this.fetch(upstream); + return X25519DeoxysII.ephemeral(public_key.key, epoch); } - public cipherSync() { + public cipherSync(): X25519DeoxysII { if (!this.pubkey) { throw new Error('No cached pubkey!'); } - const { key, epoch } = this.pubkey; - return X25519DeoxysII.ephemeral(key, epoch); + const { public_key, epoch } = this.pubkey; + return X25519DeoxysII.ephemeral(public_key.key, epoch); } } diff --git a/clients/js/src/cipher.ts b/clients/js/src/cipher.ts index bffd072d..cdaf9f17 100644 --- a/clients/js/src/cipher.ts +++ b/clients/js/src/cipher.ts @@ -58,7 +58,7 @@ export type Envelope = { pk: Uint8Array; nonce: Uint8Array; data: Uint8Array; - epoch?: number; + epoch?: bigint; }; }; @@ -80,7 +80,7 @@ function formatFailure(fail: CallFailure): string { export abstract class Cipher { public abstract kind: CipherKind; public abstract publicKey: Uint8Array; - public abstract epoch?: number; + public abstract epoch?: bigint; public abstract encrypt(plaintext: Uint8Array): { ciphertext: Uint8Array; @@ -206,13 +206,13 @@ export abstract class Cipher { export class X25519DeoxysII extends Cipher { public override readonly kind = CipherKind.X25519DeoxysII; public override readonly publicKey: Uint8Array; - public override readonly epoch: number | undefined; + public override readonly epoch: bigint | undefined; private cipher: deoxysii.AEAD; private key: Uint8Array; // Stored for curious users. /** Creates a new cipher using an ephemeral keypair stored in memory. */ - static ephemeral(peerPublicKey: BytesLike, epoch?: number): X25519DeoxysII { + static ephemeral(peerPublicKey: BytesLike, epoch?: bigint): X25519DeoxysII { const keypair = boxKeyPairFromSecretKey( randomBytes(crypto_box_SECRETKEYBYTES), ); @@ -222,7 +222,7 @@ export class X25519DeoxysII extends Cipher { static fromSecretKey( secretKey: BytesLike, peerPublicKey: BytesLike, - epoch?: number, + epoch?: bigint, ): X25519DeoxysII { const keypair = boxKeyPairFromSecretKey(getBytes(secretKey)); return new X25519DeoxysII(keypair, getBytes(peerPublicKey), epoch); @@ -231,7 +231,7 @@ export class X25519DeoxysII extends Cipher { public constructor( keypair: BoxKeyPair, peerPublicKey: Uint8Array, - epoch?: number, + epoch?: bigint, ) { super(); diff --git a/clients/js/src/munacl.ts b/clients/js/src/munacl.ts index cce9cf83..6226b233 100644 --- a/clients/js/src/munacl.ts +++ b/clients/js/src/munacl.ts @@ -1,8 +1,40 @@ // SPDX-License-Identifier: Unlicense +/* +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to +*/ + +// See: https://www.rfc-editor.org/rfc/rfc8032 +// See: https://eprint.iacr.org/2020/1244.pdf for Ed25519 // Minimum necessary functions extracted and made TypeScript compatible from: // https://github.com/dchest/tweetnacl-js/blob/fecde6ecf0eb81e31d54ca0509531ab1b825f490/nacl-fast.js +import { sha512 } from '@noble/hashes/sha512'; +import { hexlify } from './ethersutils.js'; + function gf(init?: number[]): Float64Array { const r = new Float64Array(16); if (init) { @@ -15,6 +47,60 @@ export const crypto_box_SECRETKEYBYTES = 32 as const; export const crypto_box_PUBLICKEYBYTES = 32 as const; export const crypto_scalarmult_BYTES = 32 as const; export const crypto_scalarmult_SCALARBYTES = 32 as const; +export const crypto_sign_BYTES = 64 as const; +export const crypto_sign_PUBLICKEYBYTES = 32 as const; + +const gf0 = gf(); +const gf1 = gf([1]); + +/** + * D: Edwards curve constant + * -121665/121666 over the field + * Used in the Edwards curve equation: -x^2 + y^2 = 1 + dx^2y^2 + */ +const D = gf([ + 0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, + 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203, +]); + +/** + * D2: 2 * D + * Used for optimizing certain calculations + */ +const D2 = gf([ + 0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, + 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406, +]); + +/** + * X: x-coordinate of the base point + * The base point is a generator of the main subgroup + */ +const X = gf([ + 0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, + 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169, +]); + +/** + * Y: y-coordinate of the base point + * 4/5 in the field + */ +const Y = gf([ + 0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, + 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, +]); + +/** + * I: sqrt(-1) in the field + * Used in various calculations + */ +const I = gf([ + 0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, + 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83, +]); + +const _8 = new Uint8Array(32); +_8[0] = 8; const _9 = new Uint8Array(32); _9[0] = 9; @@ -86,6 +172,7 @@ function A(o: Float64Array, a: Float64Array, b: Float64Array) { for (let i = 0; i < 16; i++) o[i] = a[i] + b[i]; } +/// Subtract field elements, o = a - b function Z(o: Float64Array, a: Float64Array, b: Float64Array) { for (let i = 0; i < 16; i++) o[i] = a[i] - b[i]; } @@ -619,6 +706,559 @@ function crypto_scalarmult_base(q: Uint8Array, n: Uint8Array) { return crypto_scalarmult(q, n, _9); } +/** + * Copies elements from one Float64Array to another, truncating to integers. + * Used in operations related to the curve25519 field. + * + * @param r Destination Float64Array (length 16) + * @param a Source Float64Array (length 16) + * + * Note: The `|0` operation truncates each float to a 32-bit integer. + * This ensures all values in `r` are integers, which is + * important for certain field arithmetic operations. + */ +function set25519(r: Float64Array, a: Float64Array) { + let i; + for (i = 0; i < 16; i++) r[i] = a[i] | 0; +} + +/** + * Computes (2^252 - 3) power in the finite field. + * This is a key operation for Ed25519 signature scheme. + * + * @param o Output Float64Array (length 16) + * @param i Input Float64Array (length 16) + * + * Details: + * - Implements the exponentiation i^(2^252 - 3) (mod p) + * - Uses a square-and-multiply algorithm + * - S(c, c) squares the value + * - M(c, c, i) multiplies by the original input + * - The result is used in computing inverses in the field + */ +function pow2523(o: Float64Array, i: Float64Array) { + const c = gf(); + let a; + for (a = 0; a < 16; a++) c[a] = i[a]; + for (a = 250; a >= 0; a--) { + S(c, c); + if (a !== 1) M(c, c, i); + } + for (a = 0; a < 16; a++) o[a] = c[a]; +} + +/** + * Compares n bytes of two arrays in constant time. + * @param x First Uint8Array + * @param xi Starting index in x + * @param y Second Uint8Array + * @param yi Starting index in y + * @param n Number of bytes to compare + * @returns 0 if sections are identical, -1 otherwise + */ +function vn(x: Uint8Array, xi: number, y: Uint8Array, yi: number, n: number) { + let i, + d = 0; + for (i = 0; i < n; i++) d |= x[xi + i] ^ y[yi + i]; + return (1 & ((d - 1) >>> 8)) - 1; +} + +/** + * Compares 32 bytes of two arrays in constant time. + * @param x First Uint8Array + * @param xi Starting index in x + * @param y Second Uint8Array + * @param yi Starting index in y + * @returns 0 if 32-byte sections are identical, -1 otherwise + */ +function crypto_verify_32( + x: Uint8Array, + xi: number, + y: Uint8Array, + yi: number, +) { + return vn(x, xi, y, yi, 32); +} + +/** + * Checks if two field elements are not equal. + * + * @param a First field element (Float64Array) + * @param b Second field element (Float64Array) + * @returns 0 if equal, -1 if not equal + * + * Note: Operates in constant time to prevent timing attacks. + */ +function neq25519(a: Float64Array, b: Float64Array) { + const c = new Uint8Array(32), + d = new Uint8Array(32); + pack25519(c, a); + pack25519(d, b); + return crypto_verify_32(c, 0, d, 0); +} + +/** + * Computes the parity of a field element. + * + * @param a Field element (Float64Array) + * @returns 0 if even, 1 if odd + * + * Note: Used in point compression/decompression. + */ +function par25519(a: Float64Array) { + const d = new Uint8Array(32); + pack25519(d, a); + return d[0] & 1; +} + +function unpack(r: Float64Array[], p: Uint8Array) { + return unpackneg(r, p, true); +} + +/** + * Unpacks a compressed Edwards point, then negates it + * + * @param r Output array of 4 Float64Arrays representing the point (X:Y:Z:T) + * @param p Input compressed point (32-byte Uint8Array) + * @returns 0 on success, -1 if point is invalid + */ +function unpackneg(r: Float64Array[], p: Uint8Array, dontnegate?:boolean) { + const t = gf(), + chk = gf(), + num = gf(), + den = gf(), + den2 = gf(), + den4 = gf(), + den6 = gf(); + + set25519(r[2], gf1); + unpack25519(r[1], p); + S(num, r[1]); + M(den, num, D); + Z(num, num, r[2]); + A(den, r[2], den); + + S(den2, den); + S(den4, den2); + M(den6, den4, den2); + M(t, den6, num); + M(t, t, den); + + pow2523(t, t); + M(t, t, num); + M(t, t, den); + M(t, t, den); + M(r[0], t, den); + + S(chk, r[0]); + M(chk, chk, den); + if (neq25519(chk, num)) M(r[0], r[0], I); + + S(chk, r[0]); + M(chk, chk, den); + if (neq25519(chk, num)) return -1; + + if( ! dontnegate ) { + if (par25519(r[0]) === p[31] >> 7) Z(r[0], gf0, r[0]); + } + + M(r[3], r[0], r[1]); + return 0; +} + +/** + * Conditionally swaps two sets of field elements in constant time. + * + * @param p First array of Float64Array (length 4) + * @param q Second array of Float64Array (length 4) + * @param b Condition for swapping (0 or 1) + * + * Operation: + * - Applies sel25519 to each pair of corresponding elements in p and q + * - If b is 1, elements are swapped; if b is 0, no change occurs + * - Operates in constant time to prevent timing attacks + * + * Security: + * - The constant-time nature of this operation is critical for + * preventing side-channel attacks in cryptographic implementations. + */ +function cswap(p: Float64Array[], q: Float64Array[], b: number) { + let i; + for (i = 0; i < 4; i++) { + sel25519(p[i], q[i], b); + } +} + +/** + * Adds two points on the Edwards curve. + * + * @param p First point (input/output), array of 4 Float64Arrays + * @param q Second point (input), array of 4 Float64Arrays + * + * Note: Uses extended coordinates (X:Y:Z:T) for efficient computation + */ +function add(p: Float64Array[], q: Float64Array[]) { + const a = gf(), + b = gf(), + c = gf(), + d = gf(), + e = gf(), + f = gf(), + g = gf(), + h = gf(), + t = gf(); + + Z(a, p[1], p[0]); + Z(t, q[1], q[0]); + M(a, a, t); + A(b, p[0], p[1]); + A(t, q[0], q[1]); + M(b, b, t); + M(c, p[3], q[3]); + M(c, c, D2); + M(d, p[2], q[2]); + A(d, d, d); + Z(e, b, a); + Z(f, d, c); + A(g, d, c); + A(h, b, a); + + M(p[0], e, f); + M(p[1], h, g); + M(p[2], g, f); + M(p[3], e, h); +} + +/** + * Performs scalar multiplication: p = s * q + * + * @param p Result point (output), array of 4 Float64Arrays + * @param q Base point (input), array of 4 Float64Arrays + * @param s Scalar (input), 32-byte Uint8Array + * + * Algorithm: + * - Implements the Montgomery ladder for constant-time operation + * - Uses conditional swaps (cswap) to prevent timing attacks + */ +function scalarmult(p: Float64Array[], q: Float64Array[], s: Uint8Array) { + set25519(p[0], gf0); + set25519(p[1], gf1); + set25519(p[2], gf1); + set25519(p[3], gf0); + for (let i = 255; i >= 0; --i) { + const b = (s[(i / 8) | 0] >> (i & 7)) & 1; + cswap(p, q, b); + add(q, p); + add(p, p); + cswap(p, q, b); + } +} + +/** + * Computes s * B, where B is the curve's base point + * + * @param p Result point (output), array of 4 Float64Arrays + * @param s Scalar (input), 32-byte Uint8Array + * + * Operation: + * - Initializes q with the curve's base point (X, Y) + * - Calls scalarmult(p, q, s) + */ +function scalarbase(p: Float64Array[], s: Uint8Array) { + const q = [gf(), gf(), gf(), gf()]; + set25519(q[0], X); + set25519(q[1], Y); + set25519(q[2], gf1); + M(q[3], X, Y); + scalarmult(p, q, s); +} + +/** + * L: The order of the main subgroup of the Ed25519 curve + * Represented as a little-endian 256-bit number + * L = 2^252 + 27742317777372353535851937790883648493 + */ +const L = new Float64Array([ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, + 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10, +]); + +/** + * Performs modular reduction of a 512-bit number modulo L + * + * @param r Output Uint8Array (64 bytes) for the reduced result + * @param x Input Float64Array (64 elements) representing the number to reduce + * + * Algorithm: + * 1. Reduces higher 256 bits using schoolbook division + * 2. Reduces lower 256 bits + * 3. Handles final carry and normalization + * + * Notes: + * - Uses 256-bit arithmetic to avoid BigInt dependencies + * - Implements optimized reduction for the specific prime L + * - Critical for maintaining correct range in Ed25519 operations + */ +function modL(r: Uint8Array, x: Float64Array) { + let carry, i, j, k; + for (i = 63; i >= 32; --i) { + carry = 0; + for (j = i - 32, k = i - 12; j < k; ++j) { + x[j] += carry - 16 * x[i] * L[j - (i - 32)]; + carry = Math.floor((x[j] + 128) / 256); + x[j] -= carry * 256; + } + x[j] += carry; + x[i] = 0; + } + carry = 0; + for (j = 0; j < 32; j++) { + x[j] += carry - (x[31] >> 4) * L[j]; + carry = x[j] >> 8; + x[j] &= 255; + } + for (j = 0; j < 32; j++) x[j] -= carry * L[j]; + for (i = 0; i < 32; i++) { + x[i + 1] += x[i] >> 8; + r[i] = x[i] & 255; + } +} + +/** + * Reduces a 64-byte number modulo L. + * L is the order of the main subgroup used in Ed25519. + * + * @param r Uint8Array (length 64) to be reduced in place + * + * **Operation:** + * 1. Copies input to a Float64Array for precision + * 2. Zeros out the original input array + * 3. Performs modular reduction using modL function + * + * **Note:** + * - Using Float64Array allows for higher precision in intermediate calculations + * - The result is stored back in the original array r + * + * This function is crucial for ensuring that scalar values in + * Ed25519 operations remain within the appropriate range. + */ +function reduce(r: Uint8Array) { + const x = new Float64Array(64); + for (let i = 0; i < 64; i++) x[i] = r[i]; + for (let i = 0; i < 64; i++) r[i] = 0; + modL(r, x); +} + +/** + * Encodes a point on the Edwards curve to a 32-byte array. + * + * @param r Output Uint8Array (32 bytes) for the encoded point + * @param p Input array of Float64Array representing the point (x, y, z, t) + * + * Operation: + * 1. Converts the point from projective to affine coordinates + * 2. Encodes the y-coordinate + * 3. Stores the sign of x in the most significant bit of the last byte + */ +function pack(r: Uint8Array, p: Float64Array[]) { + const tx = gf(), + ty = gf(), + zi = gf(); + inv25519(zi, p[2]); + M(tx, p[0], zi); // Calculate affine x and y coordinates + M(ty, p[1], zi); + pack25519(r, ty); // Encode y-coordinate + r[31] ^= par25519(tx) << 7; // set sign bit +} + +/// Check if s < L, per RFC 8032 +/// https://www.rfc-editor.org/rfc/rfc8032 +export function ed25519_is_valid_scalar(s: Uint8Array): boolean { + // Check if scalar s is less than L + for (let i = 31; i >= 0; i--) { + if (s[i] < L[i]) return true; + if (s[i] > L[i]) return false; + } + // The scalar is equal to the order of the curve. + return false; +} + +/// The 8 elements of the Ed25519 torsion subgroup as Uint8Arrays +const ED25519_TORSION_SUBGROUP = [ + // 0100000000000000000000000000000000000000000000000000000000000000 + // (0,1), order 1, neutral element + new Uint8Array([ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + + // c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a + // order 8 + new Uint8Array([ + 199, 23, 106, 112, 61, 77, 216, 79, 186, 60, 11, 118, 13, 16, 103, 15, 42, + 32, 83, 250, 44, 57, 204, 198, 78, 199, 253, 119, 146, 172, 3, 122, + ]), + + // 0000000000000000000000000000000000000000000000000000000000000080 + // order 4 + new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 128, + ]), + + // 26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05 + // order 8 + new Uint8Array([ + 38, 232, 149, 143, 194, 178, 39, 176, 69, 195, 244, 137, 242, 239, 152, 240, + 213, 223, 172, 5, 211, 198, 51, 57, 177, 56, 2, 136, 109, 83, 252, 5, + ]), + + // ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f + // order 2 + new Uint8Array([ + 236, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 127, + ]), + + // 26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85 + // order 8 + new Uint8Array([ + 38, 232, 149, 143, 194, 178, 39, 176, 69, 195, 244, 137, 242, 239, 152, 240, + 213, 223, 172, 5, 211, 198, 51, 57, 177, 56, 2, 136, 109, 83, 252, 133, + ]), + + // 0000000000000000000000000000000000000000000000000000000000000000 + // order 4 + new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ]), + + // c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa + // order 8 + new Uint8Array([ + 199, 23, 106, 112, 61, 77, 216, 79, 186, 60, 11, 118, 13, 16, 103, 15, 42, + 32, 83, 250, 44, 57, 204, 198, 78, 199, 253, 119, 146, 172, 3, 250, + ]), +] as const; + +/** + * Checks if a given point is a small order point on the Ed25519 curve. + * + * @param p - A Uint8Array of 32 bytes representing a compressed Ed25519 point. + * @returns true if the point is of small order (in the torsion subgroup), false otherwise. + * + * This function is critical for security in Ed25519 operations, particularly + * in signature verification and key exchange protocols. + */ +function ed25519_is_small_order(p: Uint8Array): boolean { + for (const q of ED25519_TORSION_SUBGROUP) { + if (crypto_verify_32(q, 0, p, 0) === 0) { + return true; + } + } + return false; +} + +function _ed25519_verify_raw_common( + signature: Uint8Array, + publicKey: Uint8Array, + msg: Uint8Array, +) { + if (ed25519_is_small_order(publicKey)) { + return false; // Small order A + } + + const R_bits = signature.subarray(0, 32); + const S_bits = signature.subarray(32, 64); + + if (ed25519_is_small_order(R_bits)) { + return false; // Small order R + } + + if (!ed25519_is_valid_scalar(S_bits)) { + return false; // S is not minimal (reject malleability) + } + + // TODO: verify A and R are canonical point encodings + + // Decompress A (PublicKey) + const negA = [gf(), gf(), gf(), gf()]; + if (unpackneg(negA, publicKey) !== 0) { + return false; // Decompress A (PublicKey) failed + } + + // k = H(R,A,m) + const k = sha512(new Uint8Array([...R_bits, ...publicKey, ...msg])); + reduce(k); + + const sB = [gf(), gf(), gf(), gf()]; + const kA = [gf(), gf(), gf(), gf()]; + + // sB = G^s + scalarbase(sB, S_bits); + + // kA = -A^k + scalarmult(kA, negA, k); + + return {kA, sB, k}; +} + +/// Verify signature without applying domain separation. +export function ed25519_verify_raw_cofactorless( + signature: Uint8Array, + publicKey: Uint8Array, + msg: Uint8Array, +): boolean { + const result = _ed25519_verify_raw_common(signature, publicKey, msg); + if( result === false ) { + return false; + } + + const {sB, kA, k} = result; + + // sB = G^s - A^k + add(sB, kA); + + pack(k, sB); + + // R == G^s - A^k + return crypto_verify_32(signature, 0, k, 0) === 0; +} + +/// Verify signature without applying domain separation. +export function ed25519_verify_raw_cofactored( + signature: Uint8Array, + publicKey: Uint8Array, + msg: Uint8Array, +): boolean { + const result = _ed25519_verify_raw_common(signature, publicKey, msg); + if( result === false ) { + return false; + } + + const {sB, kA, k} = result; + + scalarmult(sB, sB, _8); + + // kA = 8 * -A^k + scalarmult(kA, kA, _8); + + // R = 8*r + const R = [gf(), gf(), gf(), gf()]; + unpack(R, signature); + scalarmult(R, R, _8); + + // Check the cofactored group equation ([8][S]B = [8]R - [8][k]A) + add(R, kA); + pack(k, R); + const j = new Uint8Array(32); + pack(j, sB); + + return crypto_verify_32(j, 0, k, 0) === 0; +} + export class MuNaclError extends Error {} export interface BoxKeyPair { diff --git a/clients/js/test/calldatapublickey.spec.ts b/clients/js/test/calldatapublickey.spec.ts index ae0f7f6a..3c11c097 100644 --- a/clients/js/test/calldatapublickey.spec.ts +++ b/clients/js/test/calldatapublickey.spec.ts @@ -5,21 +5,80 @@ import { fetchRuntimePublicKey, NETWORKS, KeyFetcher, + CallDataPublicKey, + getBytes, + verifyRuntimePublicKey, } from '@oasisprotocol/sapphire-paratime'; +import { encode as cborEncode, decode as cborDecode } from 'cborg'; import { MockEIP1193Provider, MockNonRuntimePublicKeyProvider } from './utils'; +import { AbiCoder } from 'ethers'; +import { JsonRpcProvider } from 'ethers'; +import { SUBCALL_ADDR } from '../src/constants'; describe('fetchRuntimePublicKey', () => { afterEach(() => { jest.clearAllMocks(); }); + it('Verify runtime public key without epoch', async () => { + const x: CallDataPublicKey = { + public_key: { + key: getBytes( + '0xaa371525c094be908740c729844dae670ba8688076b7f73f2a56477b7225b96f', + ), + checksum: new Uint8Array(32).fill(2), + expiration: undefined, + signature: getBytes( + '0x401034412210ba402f040c3d7753be3080769ef6ae53a7e8dd017fc144f6797be632a6dab834a771554535074b75da0da5c93f3bdf01c7209520b65406c4fb0f', + ), + }, + epoch: undefined, + runtime_id: new Uint8Array(32).fill(3), + key_pair_id: new Uint8Array(32).fill(4) + }; + const signer = getBytes( + '0xb8f0cae2ea75374e6ffc8d597e76743613828b7b17a3d890eff358486b2bbf2a', + ); + + expect(verifyRuntimePublicKey(signer, x)).toEqual( + true, + ); + }); + + it('Verify runtime public key with epoch', async () => { + const cdpk: CallDataPublicKey = { + public_key: { + key: getBytes( + '0xaa371525c094be908740c729844dae670ba8688076b7f73f2a56477b7225b96f', + ), + checksum: new Uint8Array(32).fill(2), + expiration: 0x14n, + signature: getBytes( + '0x6e37fcbff7f9b46b4873b314000e21c71f1f40acd97be6d2d0894f0111865ba13a13be5779313401a017b6f2cb669e980beb5d04f53a2aac1b2ca58b4f76400e', + ), + }, + epoch: 0xan, + runtime_id: new Uint8Array(32).fill(3), + key_pair_id: new Uint8Array(32).fill(4) + }; + const signer = getBytes( + '0xb8f0cae2ea75374e6ffc8d597e76743613828b7b17a3d890eff358486b2bbf2a', + ); + + expect(verifyRuntimePublicKey(signer, cdpk)).toEqual( + true, + ); + }); + /// Verifies call data public key fetching works it('mock provider', async () => { const upstream = new MockEIP1193Provider(NETWORKS.localnet.chainId); - const pk = await fetchRuntimePublicKey({ upstream }); - expect(hexlify(pk.key)).toEqual(hexlify(upstream.calldatapublickey)); + const cdpk = await fetchRuntimePublicKey({ upstream }); + expect(hexlify(cdpk.public_key.key)).toEqual( + hexlify(upstream.calldatapublickey), + ); }); // The mock provider rejects oasis_callDataPublicKey calls @@ -29,9 +88,11 @@ describe('fetchRuntimePublicKey', () => { const upstream = new MockNonRuntimePublicKeyProvider( NETWORKS.localnet.chainId, ); - const pk = await fetchRuntimePublicKey({ upstream }); + const cdpk = await fetchRuntimePublicKey({ upstream }); // This will have retrieved the key from testnet or mainnet - expect(pk.key).not.toEqual(new Uint8Array(Buffer.alloc(32, 8))); + expect(cdpk.public_key.key).not.toEqual( + new Uint8Array(Buffer.alloc(32, 8)), + ); }); // Verifies that we can differentiate between testnet & mainnet @@ -53,7 +114,7 @@ describe('fetchRuntimePublicKey', () => { expect(pkMainnet.chainId).toEqual(upstreamMainnet.chainId); expect(pkTestnet.chainId).toEqual(upstreamTestnet.chainId); - expect(pkMainnet.key).not.toBe(pkTestnet.key); + expect(pkMainnet.public_key.key).not.toBe(pkTestnet.public_key.key); expect(pkMainnet).not.toBe(pkTestnet); }); @@ -66,13 +127,48 @@ describe('fetchRuntimePublicKey', () => { const upstream = new MockEIP1193Provider(NETWORKS.localnet.chainId); const oldPk = upstream.calldatapublickey; const pk = await k.fetch(upstream); - expect(hexlify(pk.key)).toBe(hexlify(upstream.calldatapublickey)); + expect(hexlify(pk.public_key.key)).toBe( + hexlify(upstream.calldatapublickey), + ); expect(k.cipherSync()).toBe; // Then, after cycling the key, the fetcher should return the cached key upstream.__cycleKey(); const pk2 = await k.fetch(upstream); - expect(hexlify(pk2.key)).toBe(hexlify(oldPk)); - expect(hexlify(pk2.key)).not.toBe(hexlify(upstream.calldatapublickey)); + expect(hexlify(pk2.public_key.key)).toBe(hexlify(oldPk)); + expect(hexlify(pk2.public_key.key)).not.toBe( + hexlify(upstream.calldatapublickey), + ); + }); + + it('Validate core.KeyManagerPublicKey + core.CallDataPublicKey', async () => { + const rpc = new JsonRpcProvider('http://localhost:8545'); + const coder = AbiCoder.defaultAbiCoder(); + + // Retrieve KeyManager info required to verify calldata public key + const kmpk_result = coder.decode(['uint','bytes'], await rpc.call({ + to: SUBCALL_ADDR, + data: coder.encode(['string', 'bytes'], ['core.KeyManagerPublicKey', cborEncode(null)]) + })); + expect(kmpk_result[0]).toEqual(0n); + const kmpk = cborDecode(getBytes(kmpk_result[1])) as Uint8Array; + expect(kmpk.length).toEqual(32); + + // Retrieve the calldata public key + const cdpk_result = coder.decode(['uint','bytes'], await rpc.call({ + to: SUBCALL_ADDR, + data: coder.encode(['string', 'bytes'], ['core.CallDataPublicKey', cborEncode(null)]) + })); + expect(cdpk_result[0]).toEqual(0n); + const cdpk = cborDecode(getBytes(cdpk_result[1])) as CallDataPublicKey; + + // Validate the calldata public key signature + const cdpkIsValid = verifyRuntimePublicKey(kmpk, cdpk); + expect(cdpkIsValid).toEqual(true); + + // Changing any of the params will invalidate the signature + kmpk[1] = 0x3; + const cdpkIsInvalid = verifyRuntimePublicKey(kmpk, cdpk); + expect(cdpkIsInvalid).toEqual(false); }); }); diff --git a/clients/js/test/munacl.spec.ts b/clients/js/test/munacl.spec.ts index eb9ca990..e7a6b036 100644 --- a/clients/js/test/munacl.spec.ts +++ b/clients/js/test/munacl.spec.ts @@ -7,6 +7,10 @@ import { naclScalarMultBase, boxKeyPairFromSecretKey, crypto_box_SECRETKEYBYTES, + ed25519_verify_raw_cofactorless, + getBytes, + ed25519_is_valid_scalar, + ed25519_verify_raw_cofactored, } from '@oasisprotocol/sapphire-paratime'; describe('munacl', () => { @@ -56,4 +60,342 @@ describe('munacl', () => { boxKeyPairFromSecretKey(new Uint8Array(crypto_box_SECRETKEYBYTES - 1)), ).toThrow(MuNaclError); }); + + it('is_in_scalar_field', () => { + // L - 2^0 + expect( + ed25519_is_valid_scalar( + new Uint8Array([ + 0xec, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, + 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + ]), + ), + ).toStrictEqual(true); + + // L - 2^64 + expect( + ed25519_is_valid_scalar( + new Uint8Array([ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd5, 0x9c, 0xf7, + 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + ]), + ), + ).toStrictEqual(true); + + // L - 2^192 + expect( + ed25519_is_valid_scalar( + new Uint8Array([ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd5, 0x9c, 0xf7, + 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, + ]), + ), + ).toStrictEqual(true); + + // L + expect( + ed25519_is_valid_scalar( + new Uint8Array([ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, + 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + ]), + ), + ).toStrictEqual(false); + + // L + 2^0 + expect( + ed25519_is_valid_scalar( + new Uint8Array([ + 0xef, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, + 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + ]), + ), + ).toStrictEqual(false); + + // L + 2^64 + expect( + ed25519_is_valid_scalar( + new Uint8Array([ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd7, 0x9c, 0xf7, + 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + ]), + ), + ).toStrictEqual(false); + + // L + 2^128 + expect( + ed25519_is_valid_scalar( + new Uint8Array([ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, + 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + ]), + ), + ).toStrictEqual(false); + + // L + 2^192 + expect( + ed25519_is_valid_scalar( + new Uint8Array([ + 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, + 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + ]), + ), + ).toStrictEqual(false); + + // Scalar from the go runtime's test case. + expect( + ed25519_is_valid_scalar( + new Uint8Array([ + 0x67, 0x65, 0x4b, 0xce, 0x38, 0x32, 0xc2, 0xd7, 0x6f, 0x8f, 0x6f, + 0x5d, 0xaf, 0xc0, 0x8d, 0x93, 0x39, 0xd4, 0xee, 0xf6, 0x76, 0x57, + 0x33, 0x36, 0xa5, 0xc5, 0x1e, 0xb6, 0xf9, 0x46, 0xb3, 0x1d, + ]), + ), + ).toStrictEqual(false); + }); + + describe('ed25519', () => { + it('Test Cases', () => { + const cases = [ + { + message: '0x', + pub_key: + '0xd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a', + signature: + '0xe5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b', + }, + + { + message: '0x72', + pub_key: + '0x3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c', + signature: + '0x92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00', + }, + + { + message: '0xaf82', + pub_key: + '0xfc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025', + signature: + '0x6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a', + }, + ]; + + for (const c of cases) { + const sig = getBytes(c.signature); + const pbk = getBytes(c.pub_key); + const msg = getBytes(c.message); + const x = ed25519_verify_raw_cofactorless(sig, pbk, msg); + const z = ed25519_verify_raw_cofactored(sig, pbk, msg); + /* + const outMsg = new Uint8Array(msg.length + 64); + const y = crypto_sign_open( + outMsg, + new Uint8Array([...sig, ...msg]), + msg.length + 64, + pbk, + ); + expect(y).toStrictEqual(msg.length); + */ + expect(x).toStrictEqual(true); + expect(z).toStrictEqual(true); + } + }); + + it('speccheck', () => { + const cases = [ + { // #0: canonical S, small R, small A + message: + '8c93255d71dcab10e8f379c26200f3c7bd5f09d9bc3068d3ef4edeb4853022b6', + pub_key: + 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa', + signature: + 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a0000000000000000000000000000000000000000000000000000000000000000', + expected_with_cofactored: false, + expected_with_cofactorless: false, + }, + { // #1: canonical S, mixed R, small A + message: + '9bd9f44f4dcc75bd531b56b2cd280b0bb38fc1cd6d1230e14861d861de092e79', + pub_key: + 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa', + signature: + 'f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43a5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04', + expected_with_cofactored: false, + expected_with_cofactorless: false, + }, + { // #2: canonical S, small R, mixed A + message: + 'aebf3f2601a0c8c5d39cc7d8911642f740b78168218da8471772b35f9d35b9ab', + pub_key: + 'f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43', + signature: + 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa8c4bd45aecaca5b24fb97bc10ac27ac8751a7dfe1baff8b953ec9f5833ca260e', + expected_with_cofactored: false, + expected_with_cofactorless: false, + }, + { // #3-4: canonical S, mixed R, mixed A + message: + '9bd9f44f4dcc75bd531b56b2cd280b0bb38fc1cd6d1230e14861d861de092e79', + pub_key: + 'cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d', + signature: + '9046a64750444938de19f227bb80485e92b83fdb4b6506c160484c016cc1852f87909e14428a7a1d62e9f22f3d3ad7802db02eb2e688b6c52fcd6648a98bd009', + expected_with_cofactored: true, + expected_with_cofactorless: true, + }, + { // 4 + message: + 'e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec4011eaccd55b53f56c', + pub_key: + 'cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d', + signature: + '160a1cb0dc9c0258cd0a7d23e94d8fa878bcb1925f2c64246b2dee1796bed5125ec6bc982a269b723e0668e540911a9a6a58921d6925e434ab10aa7940551a09', + expected_with_cofactored: true, + expected_with_cofactorless: false, + }, + { // #5 Prereduce scalar which fails cofactorless + message: + 'e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec4011eaccd55b53f56c', + pub_key: + 'cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d', + signature: + '21122a84e0b5fca4052f5b1235c80a537878b38f3142356b2c2384ebad4668b7e40bc836dac0f71076f9abe3a53f9c03c1ceeeddb658d0030494ace586687405', + expected_with_cofactored: true, + expected_with_cofactorless: false, + }, + { // #6 Large S + message: + '85e241a07d148b41e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec40', + pub_key: + '442aad9f089ad9e14647b1ef9099a1ff4798d78589e66f28eca69c11f582a623', + signature: + 'e96f66be976d82e60150baecff9906684aebb1ef181f67a7189ac78ea23b6c0e547f7690a0e2ddcd04d87dbc3490dc19b3b3052f7ff0538cb68afb369ba3a514', + expected_with_cofactored: false, + expected_with_cofactorless: false, + }, + { // #7 Large S beyond the high bit checks (i.e. non-canonical representation) + message: + '85e241a07d148b41e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec40', + pub_key: + '442aad9f089ad9e14647b1ef9099a1ff4798d78589e66f28eca69c11f582a623', + signature: + '8ce5b96c8f26d0ab6c47958c9e68b937104cd36e13c33566acd2fe8d38aa19427e71f98a473474f2f13f06f97c20d58cc3f54b8bd0d272f42b695dd7e89a8c22', + expected_with_cofactored: false, + expected_with_cofactorless: false, + }, + { // #8-9 Non canonical R + message: + '9bedc267423725d473888631ebf45988bad3db83851ee85c85e241a07d148b41', + pub_key: + 'f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43', + signature: + 'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03be9678ac102edcd92b0210bb34d7428d12ffc5df5f37e359941266a4e35f0f', + expected_with_cofactored: true, + expected_with_cofactorless: false, + }, + { // 9 + message: + '9bedc267423725d473888631ebf45988bad3db83851ee85c85e241a07d148b41', + pub_key: + 'f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43', + signature: + 'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffca8c5b64cd208982aa38d4936621a4775aa233aa0505711d8fdcfdaa943d4908', + expected_with_cofactored: true, + expected_with_cofactorless: false, + }, + { // #10-11 Non canonical A + message: + 'e96b7021eb39c1a163b6da4e3093dcd3f21387da4cc4572be588fafae23c155b', + pub_key: + 'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + signature: + 'a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04', + expected_with_cofactored: true, + expected_with_cofactorless: false, + }, + + { // 11 + message: + '39a591f5321bbe07fd5a23dc2f39d025d74526615746727ceefd6e82ae65c06f', + pub_key: + 'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + signature: + 'a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04', + expected_with_cofactored: true, + expected_with_cofactorless: true, + }, + ] as const; + + let i = 0; + for (const c of cases) { + const sig = getBytes(`0x${c.signature}`); + const pbk = getBytes(`0x${c.pub_key}`); + const msg = getBytes(`0x${c.message}`); + const cofactorless = ed25519_verify_raw_cofactorless(sig, pbk, msg); + const cofactored = ed25519_verify_raw_cofactored(sig, pbk, msg); + expect(cofactorless).toStrictEqual(c.expected_with_cofactorless); + expect(cofactored).toStrictEqual(c.expected_with_cofactored); + i += 1; + } + }); + + it('Case 1 - small order A', () => { + const pbk = getBytes( + '0xc7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa', + ); + const msg = getBytes( + '0x9bd9f44f4dcc75bd531b56b2cd280b0bb38fc1cd6d1230e14861d861de092e79', + ); + const sig = getBytes( + '0xf7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43a5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04', + ); + // small order A not rejected + expect(ed25519_verify_raw_cofactored(sig, pbk, msg)).toStrictEqual(false); + expect(ed25519_verify_raw_cofactorless(sig, pbk, msg)).toStrictEqual(false); + }); + + it('Case 2 - reject small order R', () => { + const pbk = getBytes( + '0xf7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43', + ); + const msg = getBytes( + '0xaebf3f2601a0c8c5d39cc7d8911642f740b78168218da8471772b35f9d35b9ab', + ); + const sig = getBytes( + '0xc7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa8c4bd45aecaca5b24fb97bc10ac27ac8751a7dfe1baff8b953ec9f5833ca260e', + ); + // small order R not rejected + expect(ed25519_verify_raw_cofactored(sig, pbk, msg)).toStrictEqual(false); + expect(ed25519_verify_raw_cofactorless(sig, pbk, msg)).toStrictEqual(false); + }); + + // Vector 4 is made to pass cofactored and fail in cofactorless verification, this vector + // is the main indicator of what type of verification is used in the implementation + // + // RFC 8032 [18] allows optionality between using a permissive verification + // equation (cofactored) and a more strict verification equation (cofactorless) + it('Case 4 - cofactored verification', () => { + const pbk = getBytes( + '0xcdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d', + ); + const msg = getBytes( + '0xe47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec4011eaccd55b53f56c', + ); + const sig = getBytes( + '0x160a1cb0dc9c0258cd0a7d23e94d8fa878bcb1925f2c64246b2dee1796bed5125ec6bc982a269b723e0668e540911a9a6a58921d6925e434ab10aa7940551a09', + ); + expect(ed25519_verify_raw_cofactorless(sig, pbk, msg)).toStrictEqual(false); + expect(ed25519_verify_raw_cofactored(sig, pbk, msg)).toStrictEqual(true); + }); + }); }); diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index 27c8d4df..89f1a21f 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -1525,15 +1525,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" @@ -2149,7 +2140,7 @@ dependencies = [ [[package]] name = "oasis-runtime-sdk" version = "0.10.0" -source = "git+https://github.com/oasisprotocol/oasis-sdk?tag=runtime-sdk/v0.10.0#8444b42637892c14052e53124363362adc297bda" +source = "git+https://github.com/oasisprotocol/oasis-sdk?branch=CedarMist/GetPublicKey#50658ab2990383e20c70068964115143ab729468" dependencies = [ "anyhow", "async-trait", @@ -2190,7 +2181,7 @@ dependencies = [ [[package]] name = "oasis-runtime-sdk-evm" version = "0.6.0" -source = "git+https://github.com/oasisprotocol/oasis-sdk?tag=runtime-sdk/v0.10.0#8444b42637892c14052e53124363362adc297bda" +source = "git+https://github.com/oasisprotocol/oasis-sdk?branch=CedarMist/GetPublicKey#50658ab2990383e20c70068964115143ab729468" dependencies = [ "anyhow", "base64 0.22.1", @@ -2222,7 +2213,7 @@ dependencies = [ [[package]] name = "oasis-runtime-sdk-macros" version = "0.3.0" -source = "git+https://github.com/oasisprotocol/oasis-sdk?tag=runtime-sdk/v0.10.0#8444b42637892c14052e53124363362adc297bda" +source = "git+https://github.com/oasisprotocol/oasis-sdk?branch=CedarMist/GetPublicKey#50658ab2990383e20c70068964115143ab729468" dependencies = [ "darling 0.20.10", "proc-macro2 1.0.89", @@ -2575,7 +2566,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools", "proc-macro2 1.0.89", "quote 1.0.37", "syn 2.0.85", @@ -2588,7 +2579,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools", "proc-macro2 1.0.89", "quote 1.0.37", "syn 2.0.85", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 6d63d110..42a6ceab 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -21,8 +21,8 @@ debug = false keymanager = { git = "https://github.com/oasisprotocol/keymanager-paratime", tag = "v0.5.0-testnet" } # SDK. -module-evm = { git = "https://github.com/oasisprotocol/oasis-sdk", tag = "runtime-sdk/v0.10.0", package = "oasis-runtime-sdk-evm" } -oasis-runtime-sdk = { git = "https://github.com/oasisprotocol/oasis-sdk", tag = "runtime-sdk/v0.10.0" } +module-evm = { git = "https://github.com/oasisprotocol/oasis-sdk", branch = "CedarMist/GetPublicKey", package = "oasis-runtime-sdk-evm" } +oasis-runtime-sdk = { git = "https://github.com/oasisprotocol/oasis-sdk", branch = "CedarMist/GetPublicKey" } # Third party. once_cell = "1.8.0" diff --git a/runtime/Makefile b/runtime/Makefile index c123de94..2a62a1b7 100644 --- a/runtime/Makefile +++ b/runtime/Makefile @@ -1,5 +1,6 @@ ROOT_DIR := $(dir $(realpath $(lastword $(MAKEFILE_LIST)))) -SAPPHIRE_DEV_DOCKER=ghcr.io/oasisprotocol/sapphire-localnet:latest +#SAPPHIRE_DEV_DOCKER=ghcr.io/oasisprotocol/sapphire-localnet:latest +SAPPHIRE_DEV_DOCKER=ghcr.io/oasisprotocol/sapphire-localnet:local all: @@ -9,8 +10,11 @@ build-debug: pull: docker pull $(SAPPHIRE_DEV_DOCKER) +# Enable debugging in the container +DOCKER_EXTRA_ARGS = # -e OASIS_NODE_LOG_LEVEL=debug + debug: build-debug - docker run --rm -ti -p8545:8545 -p8546:8546 -v $(ROOT_DIR)/target/debug/sapphire-paratime:/runtime.elf $(SAPPHIRE_DEV_DOCKER) -test-mnemonic -n 4 + docker run --rm -ti -p8545:8545 -p8546:8546 -v $(ROOT_DIR)/target/debug/sapphire-paratime:/ronl.elf $(DOCKER_EXTRA_ARGS) $(SAPPHIRE_DEV_DOCKER) -test-mnemonic -n 4 clean: cargo clean