Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

passing test for gfmulx #48

Merged
merged 14 commits into from
Aug 16, 2024
114 changes: 114 additions & 0 deletions circuits/aes-gcm/gfmulx.circom
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
pragma circom 2.1.9;

// include "circomlib/circuits/gates.circom";
include "helper_functions.circom";

// compute x * `in` over ghash polynomial
// ghash irreducible polynomial x^128 = x^7 + x^2 + x + 1
//
// spec:
// https://tools.ietf.org/html/rfc8452#appendix-A
template ghash_GFMULX() {
signal input in[128];
signal output out[128];
var msb = in[127];

// v = in right-shifted by 1
signal v[128];
v[0] <== 0;
for (var i = 1; i < 128; i++) { v[i] <== in[i-1]; }

// if MSB set, assign irreducible poly bits, otherwise zero
// irreducible_poly has 1s at positions 1, 2, 7, 127
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be
at positions 1, 2, 7, 128

signal irreducible_poly[128];
for (var i = 0; i < 128; i++) {
if (i==0 || i == 1 || i==6 || i==127) {
irreducible_poly[i] <== msb;
} else {
irreducible_poly[i] <== 0;
}
}

component xor = BitwiseXor(128);
xor.a <== v;
xor.b <== irreducible_poly;
out <== xor.out;
}

// compute x * `in` over polyval polynomial
// polyval irreducible polynomial x^128 = x^127 + x^126 + x^121 + 1
//
// spec:
// https://tools.ietf.org/html/rfc8452#appendix-A
//
// rust-crypto reference implementation:
// https://github.com/RustCrypto/universal-hashes/blob/master/polyval/src/mulx.rs#L11
template polyval_GFMULX() {
signal input in[128];
signal output out[128];
// v = in << 1; observe that LE makes this less straightforward
signal v[128];
// if MSB set, assign irreducible poly bits, otherwise zero
signal irreducible_poly[128];
var msb = in[128 - 8];

component left_shift = LeftShiftLE(1);
for (var i = 0; i < 128; i++) {
left_shift.in[i] <== in[i];
}
for (var i = 0; i < 128; i++) {
v[i] <== left_shift.out[i];
}

// irreducible_poly has 1s at positions 1, 121, 126, 127
// 0000 0001... <== encodes 1
// ...1100 0010 <== encodes 121, 126, 127
// ...0100 0010 <== encodes 121, 126
for (var i = 0; i < 128; i++) {
if (i==7 || i == 120 || i==121 || i==126) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be more explicit about LE stuff

irreducible_poly[i] <== msb;
} else {
irreducible_poly[i] <== 0;
}
}

component xor = BitwiseXor(128);
xor.a <== v;
xor.b <== irreducible_poly;
out <== xor.out;
}


// Left shift a 128-bit little-endian array by `shift` bits
//
// example for 16 bit-array shifted by 1 bit:
// in = [h g f e d c b a, p o n m l k j i]
// mid1= [a b c d e f g h, i j k l m n o p] // swap order of bits in each byte
// mid2= [0 a b c d e f g, h i j k l m n o] // shift bits right by 1
// out = [g f e d c b a 0, o n m l k j i h] // swap order of bits in each byte
// TODO(TK 2024-08-15): optimize
template LeftShiftLE(shift) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very readable. Don't optimize until necessary

signal input in[128];
signal output out[128];
signal mid_1[128];
signal mid_2[128];

for (var i = 0; i < 16; i++) {
for (var j = 0; j < 8; j++) {
mid_1[j + 8*i] <== in[7-j + 8*i];
}
}

for (var i = 0; i < shift; i++) {
mid_2[i] <== 0;
}
for (var i = shift; i < 128; i++) {
mid_2[i] <== mid_1[i - shift];
}

for (var i = 0; i < 16; i++) {
for (var j = 0; j < 8; j++) {
out[j + 8*i] <== mid_2[7-j + 8*i];
}
}
}
10 changes: 2 additions & 8 deletions circuits/test/bitreversal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,13 @@ describe("bitreversal", () => {
});

let bit_array = [1,0,0,0,0,0,0,0];
let bit_array_1 = [1,0,0,0,0,0,0,1];
let expected_output = [0,0,0,0,0,0,0,1];
it("should have correct output", async () => {
const witness = await circuit.calculateWitness({ in: bit_array });
const witness = await circuit.expectPass({ in: bit_array}, { out: expected_output });
circuit.expectPass({in: bit_array});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend that tests get back the output and compare that to expected output instead of using expectPass to be consistent with the expressiveness of most testing frameworks.

const expect = ...
let result = await circuit.compute(input, ["out"]);
assert.equal(result, expected);

If not, remove the second circuit.expectPass and remove printing out the witness below.


// I was initially not sure what the first bit was doing in the witness, but it's just the success flag
console.log("witness: from input: [1,0,0,0,0,0,0,0]", witness);
});

it("should fail with wrong input", async () => {
const witness = await circuit.calculateWitness({ in: bit_array_1 });
circuit.expectPass({in: bit_array_1});

console.log("witness: from input: [1,0,0,0,0,0,0,1]", witness);
});
});
68 changes: 66 additions & 2 deletions circuits/test/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,72 @@
import { assert } from "chai";
import { Circomkit, WitnessTester } from "circomkit";
import 'mocha';
import "mocha";

export const circomkit = new Circomkit({
verbose: false,
verbose: false,
});

export { WitnessTester };

export function hexToBitArray(hex: string): number[] {
// Remove '0x' prefix if present and ensure lowercase
hex = hex.replace(/^0x/i, "").toLowerCase();

// Ensure even number of characters
if (hex.length % 2 !== 0) {
hex = "0" + hex;
}

return (
hex
// Split into pairs of characters
.match(/.{2}/g)!
.flatMap((pair) => {
const byte = parseInt(pair, 16);
// map byte to 8-bits. Apologies for the obtuse mapping;
// which cycles through the bits in byte and extracts them one by one.
return Array.from({ length: 8 }, (_, i) => (byte >> (7 - i)) & 1);
})
);
}

export function bitArrayToHex(bits: number[]): string {
// console.log(bits);
if (bits.length % 8 !== 0) {
throw new Error("Input length must be a multiple of 8 bits");
}

return bits
.reduce((acc, bit, index) => {
const byteIndex = Math.floor(index / 8);
const bitPosition = 7 - (index % 8);
acc[byteIndex] = (acc[byteIndex] || 0) | (bit << bitPosition);
return acc;
}, new Array(bits.length / 8).fill(0))
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}

it("tests hexToBitArray", async () => {
let hex = "0F";
let expectedBits = [0, 0, 0, 0, 1, 1, 1, 1];
let result = hexToBitArray(hex);
assert.deepEqual(result, expectedBits);

hex = "1248";
expectedBits = [0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0];
result = hexToBitArray(hex);
assert.deepEqual(result, expectedBits);
});

it("tests bitArrayToHexString", async () => {
let bits = [0, 0, 0, 0, 1, 1, 1, 1];
let expectedHex = "0f";
let result = bitArrayToHex(bits);
assert.equal(result, expectedHex);

bits = [1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1];
expectedHex = "8b09";
result = bitArrayToHex(bits);
assert.equal(result, expectedHex);
});
169 changes: 169 additions & 0 deletions circuits/test/gfmulx/ghash_mulx.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import chai, { assert, expect } from "chai";
import { WitnessTester } from "circomkit";
import { bitArrayToHex, circomkit, hexToBitArray } from "../common";

// Disable truncation of arrays in error messages
chai.config.truncateThreshold = 0;

const mulXTestVectors = [
"00800000000000000000000000000000",
"00400000000000000000000000000000",
"00200000000000000000000000000000",
"00100000000000000000000000000000",
"00080000000000000000000000000000",
"00040000000000000000000000000000",
"00020000000000000000000000000000",
"00010000000000000000000000000000",
"00008000000000000000000000000000",
"00004000000000000000000000000000",
"00002000000000000000000000000000",
"00001000000000000000000000000000",
"00000800000000000000000000000000",
"00000400000000000000000000000000",
"00000200000000000000000000000000",
"00000100000000000000000000000000",
"00000080000000000000000000000000",
"00000040000000000000000000000000",
"00000020000000000000000000000000",
"00000010000000000000000000000000",
"00000008000000000000000000000000",
"00000004000000000000000000000000",
"00000002000000000000000000000000",
"00000001000000000000000000000000",
"00000000800000000000000000000000",
"00000000400000000000000000000000",
"00000000200000000000000000000000",
"00000000100000000000000000000000",
"00000000080000000000000000000000",
"00000000040000000000000000000000",
"00000000020000000000000000000000",
"00000000010000000000000000000000",
"00000000008000000000000000000000",
"00000000004000000000000000000000",
"00000000002000000000000000000000",
"00000000001000000000000000000000",
"00000000000800000000000000000000",
"00000000000400000000000000000000",
"00000000000200000000000000000000",
"00000000000100000000000000000000",
"00000000000080000000000000000000",
"00000000000040000000000000000000",
"00000000000020000000000000000000",
"00000000000010000000000000000000",
"00000000000008000000000000000000",
"00000000000004000000000000000000",
"00000000000002000000000000000000",
"00000000000001000000000000000000",
"00000000000000800000000000000000",
"00000000000000400000000000000000",
"00000000000000200000000000000000",
"00000000000000100000000000000000",
"00000000000000080000000000000000",
"00000000000000040000000000000000",
"00000000000000020000000000000000",
"00000000000000010000000000000000",
"00000000000000008000000000000000",
"00000000000000004000000000000000",
"00000000000000002000000000000000",
"00000000000000001000000000000000",
"00000000000000000800000000000000",
"00000000000000000400000000000000",
"00000000000000000200000000000000",
"00000000000000000100000000000000",
"00000000000000000080000000000000",
"00000000000000000040000000000000",
"00000000000000000020000000000000",
"00000000000000000010000000000000",
"00000000000000000008000000000000",
"00000000000000000004000000000000",
"00000000000000000002000000000000",
"00000000000000000001000000000000",
"00000000000000000000800000000000",
"00000000000000000000400000000000",
"00000000000000000000200000000000",
"00000000000000000000100000000000",
"00000000000000000000080000000000",
"00000000000000000000040000000000",
"00000000000000000000020000000000",
"00000000000000000000010000000000",
"00000000000000000000008000000000",
"00000000000000000000004000000000",
"00000000000000000000002000000000",
"00000000000000000000001000000000",
"00000000000000000000000800000000",
"00000000000000000000000400000000",
"00000000000000000000000200000000",
"00000000000000000000000100000000",
"00000000000000000000000080000000",
"00000000000000000000000040000000",
"00000000000000000000000020000000",
"00000000000000000000000010000000",
"00000000000000000000000008000000",
"00000000000000000000000004000000",
"00000000000000000000000002000000",
"00000000000000000000000001000000",
"00000000000000000000000000800000",
"00000000000000000000000000400000",
"00000000000000000000000000200000",
"00000000000000000000000000100000",
"00000000000000000000000000080000",
"00000000000000000000000000040000",
"00000000000000000000000000020000",
"00000000000000000000000000010000",
"00000000000000000000000000008000",
"00000000000000000000000000004000",
"00000000000000000000000000002000",
"00000000000000000000000000001000",
"00000000000000000000000000000800",
"00000000000000000000000000000400",
"00000000000000000000000000000200",
"00000000000000000000000000000100",
"00000000000000000000000000000080",
"00000000000000000000000000000040",
"00000000000000000000000000000020",
"00000000000000000000000000000010",
"00000000000000000000000000000008",
"00000000000000000000000000000004",
"00000000000000000000000000000002",
"00000000000000000000000000000001",
];

describe("ghash_GFMulX", () => {
let circuit: WitnessTester<["in"], ["out"]>;

describe("ghash GF Mul X test", () => {
before(async () => {
circuit = await circomkit.WitnessTester(`gfmulx`, {
file: "aes-gcm/gfmulx",
template: "ghash_GFMULX",
});
// console.log("#constraints:", await circuit.getConstraintCount());
});

it("test ghash at all bits set", async () => {
let bits = hexToBitArray("01000000000000000000000000000000");
for (let i = 0; i < mulXTestVectors.length; i++) {
const expect = mulXTestVectors[i];
const _res = await circuit.compute({ in: bits }, ["out"]);
const result = bitArrayToHex(
(_res.out as (number | bigint)[]).map((bit) => Number(bit))
);
// console.log("expect: ", expect, "\nresult: ", result);
assert.equal(expect, result);
bits = hexToBitArray(result);
}
});

// ref: https://datatracker.ietf.org/doc/html/rfc8452#appendix-A
it("compute IETF test 2", async () => {
let bits = hexToBitArray("9c98c04df9387ded828175a92ba652d8");
let expect = "4e4c6026fc9c3ef6c140bad495d3296c";
const _res = await circuit.compute({ in: bits }, ["out"]);
const result = bitArrayToHex(
(_res.out as (number | bigint)[]).map((bit) => Number(bit))
);
// console.log("expect: ", expect, "\nresult: ", result);
assert.equal(expect, result);
});
});
});
Loading