Skip to content

Commit

Permalink
clients/js: more munacl tests
Browse files Browse the repository at this point in the history
  • Loading branch information
CedarMist committed Sep 24, 2024
1 parent 3a17970 commit 9e2f495
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 82 deletions.
49 changes: 24 additions & 25 deletions clients/js/src/calldatapublickey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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 } from './munacl.js';
import { ed25519_verify_raw_cofactorless } from './munacl.js';
import { sha512_256 } from '@noble/hashes/sha512';

/**
Expand All @@ -15,6 +15,10 @@ import { sha512_256 } from '@noble/hashes/sha512';
*/
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
Expand Down Expand Up @@ -57,8 +61,7 @@ export interface CachedCallDataPublicKey extends CallDataPublicKey {
}

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 {}
Expand All @@ -79,20 +82,16 @@ 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)];
}

const UINT64_MIN = BigInt(0);
const UINT64_MAX = BigInt(1) << BigInt(64);

function u64tobytes(x: bigint): Uint8Array {
if (x < UINT64_MIN || x > UINT64_MAX) {
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);
const view = new DataView(buffer);
view.setBigUint64(0, x, false); // false for big-endian
new DataView(buffer).setBigUint64(0, y, false); // false for big-endian
return new Uint8Array(buffer);
}

Expand All @@ -105,7 +104,7 @@ export function verifyRuntimePublicKey(
cdpk: CallDataPublicKey,
runtime_id: Uint8Array,
key_pair_id: Uint8Array,
) {
): boolean {
let body = new Uint8Array([
...cdpk.public_key.key,
...cdpk.public_key.checksum,
Expand All @@ -114,20 +113,20 @@ export function verifyRuntimePublicKey(
]);

if (cdpk.epoch !== undefined) {
body = new Uint8Array([...body, ...u64tobytes(BigInt(cdpk.epoch))]);
body = new Uint8Array([...body, ...u64tobytes(cdpk.epoch)]);
}

const expiration = cdpk.public_key.expiration;
if (expiration !== undefined) {
body = new Uint8Array([...body, ...u64tobytes(BigInt(expiration))]);
body = new Uint8Array([...body, ...u64tobytes(expiration)]);
}

const ctx = sha512_256.create();
ctx.update(PUBLIC_KEY_SIGNATURE_CONTEXT);
ctx.update(body);
const digest = ctx.digest();
const digest = sha512_256.create()
.update(PUBLIC_KEY_SIGNATURE_CONTEXT)
.update(body)
.digest();

return ed25519_verify_raw(cdpk.public_key.signature, signerPk, digest);
return ed25519_verify_raw_cofactorless(cdpk.public_key.signature, signerPk, digest);
}

/**
Expand Down Expand Up @@ -165,17 +164,17 @@ 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}`);
}

// TODO: validate response?
// TODO: validate resp_cbor?

return {
...cborDecode(resp_cbor),
...cborDecode(resp_cbor) as CallDataPublicKey,
chainId,
fetched: new Date(),
} as CachedCallDataPublicKey;
};
}

/**
Expand Down Expand Up @@ -216,7 +215,7 @@ export class KeyFetcher {
return X25519DeoxysII.ephemeral(public_key.key, epoch);
}

public cipherSync() {
public cipherSync(): X25519DeoxysII {
if (!this.pubkey) {
throw new Error('No cached pubkey!');
}
Expand Down
122 changes: 115 additions & 7 deletions clients/js/src/munacl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ const I = gf([
0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83,
]);

const _8 = new Uint8Array(32);
_8[0] = 8;

const _9 = new Uint8Array(32);
_9[0] = 9;
const _121665 = gf([0xdb41, 1]);
Expand Down Expand Up @@ -169,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];
}
Expand Down Expand Up @@ -807,14 +811,18 @@ function par25519(a: Float64Array) {
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) {
function unpackneg(r: Float64Array[], p: Uint8Array, dontnegate?:boolean) {
const t = gf(),
chk = gf(),
num = gf(),
Expand Down Expand Up @@ -850,7 +858,9 @@ function unpackneg(r: Float64Array[], p: Uint8Array) {
M(chk, chk, den);
if (neq25519(chk, num)) return -1;

if (par25519(r[0]) === p[31] >> 7) Z(r[0], gf0, r[0]);
if( ! dontnegate ) {
if (par25519(r[0]) === p[31] >> 7) Z(r[0], gf0, r[0]);
}

M(r[3], r[0], r[1]);
return 0;
Expand Down Expand Up @@ -1210,12 +1220,11 @@ function ed25519_is_small_order(p: Uint8Array): boolean {
return false;
}

/// Verify signature without applying domain separation.
export function ed25519_verify_raw(
function _ed25519_verify_raw_common(
signature: Uint8Array,
publicKey: Uint8Array,
msg: Uint8Array,
): boolean {
) {
if (ed25519_is_small_order(publicKey)) {
return false; // Small order A
}
Expand All @@ -1224,11 +1233,12 @@ export function ed25519_verify_raw(
const S_bits = signature.subarray(32, 64);

if (ed25519_is_small_order(R_bits)) {
console.log('Small order R', hexlify(R_bits));
return false; // Small order R
}

if (!ed25519_is_valid_scalar(S_bits)) {
console.log('S is not minimal (reject malleability)');
console.log('S is not minimal (reject malleability)', hexlify(S_bits));
return false; // S is not minimal (reject malleability)
}

Expand All @@ -1237,7 +1247,7 @@ export function ed25519_verify_raw(
// Decompress A (PublicKey)
const negA = [gf(), gf(), gf(), gf()];
if (unpackneg(negA, publicKey) !== 0) {
console.log('Point decompression failed, pubkey (A)', hexlify(publicKey));
console.log('Decompress A (PublicKey) failed!', hexlify(publicKey));
return false; // Point decompression failed
}

Expand All @@ -1254,6 +1264,22 @@ export function ed25519_verify_raw(
// 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);

Expand All @@ -1263,6 +1289,38 @@ export function ed25519_verify_raw(
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 {
Expand Down Expand Up @@ -1296,3 +1354,53 @@ export function boxKeyPairFromSecretKey(secretKey: Uint8Array): BoxKeyPair {
secretKey: new Uint8Array(secretKey),
};
}

/**
* https://eprint.iacr.org/2008/013.pdf
*
* The birational maps are:
*
* (u, v) = ((1+y)/(1-y), sqrt(-486664)*u/x)
* (x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1))
*
* @param pk Curve25519 public key
* @returns Ed25519 public key
*/
export function curve25519_to_ed25519(pk: Uint8Array) {
const z = new Uint8Array(32),
u = gf(),
a = gf(),
b = gf();

unpack25519(u, pk);

A(a, u, gf1); // a = u+1
Z(b, u, gf1); // b = u-1
inv25519(a, a); // a = 1/(u+1)
M(a, a, b); // a = a * b

pack25519(z, a);
return z;
}

export function ed25519_to_curve25519(pk: Uint8Array) {
const z = new Uint8Array(32)
const q = [gf(), gf(), gf(), gf()]
const a = gf()
const b = gf()

if (unpackneg(q, pk)) {
return null
}

const y = q[1]

A(a, gf1, y)
Z(b, gf1, y)
inv25519(b, b)
M(a, a, b)

pack25519(z, a)

return z
}
Loading

0 comments on commit 9e2f495

Please sign in to comment.