Skip to content

Commit bfc120b

Browse files
fix: chacha circuit (#74)
* add correct padding check for AES * add ciphertext padding check for chacha * update package version * better check * pass plaintext as bytes * fix test * remove length * revert aes * remove aes * refactor: use bytes for chacha input --------- Co-authored-by: Colin Roberts <colin@autoparallel.xyz>
1 parent e7c4ec9 commit bfc120b

7 files changed

Lines changed: 121 additions & 64 deletions

File tree

builds/target_1024b/chacha20_nivc_1024.circom

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ pragma circom 2.1.9;
22

33
include "../../circuits/chacha20/nivc/chacha20_nivc.circom";
44

5-
component main { public [step_in] } = ChaCha20_NIVC(256);
5+
component main { public [step_in] } = ChaCha20_NIVC(1024);

builds/target_512b/chacha20_nivc_512b.circom

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ pragma circom 2.1.9;
22

33
include "../../circuits/chacha20/nivc/chacha20_nivc.circom";
44

5-
component main { public [step_in] } = ChaCha20_NIVC(128);
5+
component main { public [step_in] } = ChaCha20_NIVC(512);

circuits/chacha20/nivc/chacha20_nivc.circom

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ include "../../utils/array.circom";
2222
// +---+---+---+---+
2323
// | # | N | N | N |
2424
// +---+---+---+---+
25-
// paramaterized by n which is the number of 32-bit words to encrypt
26-
template ChaCha20_NIVC(N) {
25+
// paramaterized by `DATA_BYTES` which is the plaintext length in bytes
26+
template ChaCha20_NIVC(DATA_BYTES) {
2727
// key => 8 32-bit words = 32 bytes
2828
signal input key[8][32];
2929
// nonce => 3 32-bit words = 12 bytes
@@ -33,13 +33,23 @@ template ChaCha20_NIVC(N) {
3333

3434
// the below can be both ciphertext or plaintext depending on the direction
3535
// in => N 32-bit words => N 4 byte words
36-
signal input plainText[N][32];
36+
signal input plainText[DATA_BYTES];
3737
// out => N 32-bit words => N 4 byte words
38-
signal input cipherText[N][32];
38+
signal input cipherText[DATA_BYTES];
3939

4040
signal input step_in[1];
4141
signal output step_out[1];
4242

43+
signal plaintextBits[DATA_BYTES / 4][32];
44+
component toBits[DATA_BYTES / 4];
45+
for (var i = 0 ; i < DATA_BYTES / 4 ; i++) {
46+
toBits[i] = fromWords32ToLittleEndian();
47+
for (var j = 0 ; j < 4 ; j++) {
48+
toBits[i].words[j] <== plainText[i*4 + j];
49+
}
50+
plaintextBits[i] <== toBits[i].data;
51+
}
52+
4353
var tmp[16][32] = [
4454
[
4555
// constant 0x61707865
@@ -88,24 +98,24 @@ template ChaCha20_NIVC(N) {
8898

8999
// do the ChaCha20 rounds
90100
// rounds opperates on 4 words at a time
91-
component rounds[N/16];
92-
component xors[N];
93-
component counter_adder[N/16 - 1];
101+
component rounds[DATA_BYTES / 64];
102+
component xors[DATA_BYTES];
103+
component counter_adder[DATA_BYTES / 64 - 1];
94104

95-
signal computedCipherText[N][32];
105+
signal computedCipherText[DATA_BYTES / 4][32];
96106

97-
for(i = 0; i < N/16; i++) {
107+
for(i = 0; i < DATA_BYTES / 64; i++) {
98108
rounds[i] = Round();
99109
rounds[i].in <== tmp;
100110
// XOR block with input
101111
for(j = 0; j < 16; j++) {
102112
xors[i*16 + j] = XorBits(32);
103-
xors[i*16 + j].a <== plainText[i*16 + j];
113+
xors[i*16 + j].a <== plaintextBits[i*16 + j];
104114
xors[i*16 + j].b <== rounds[i].out[j];
105115
computedCipherText[i*16 + j] <== xors[i*16 + j].out;
106116
}
107117

108-
if(i < N/16 - 1) {
118+
if(i < DATA_BYTES / 64 - 1) {
109119
counter_adder[i] = AddBits(32);
110120
counter_adder[i].a <== tmp[12];
111121
counter_adder[i].b <== one;
@@ -115,25 +125,21 @@ template ChaCha20_NIVC(N) {
115125
}
116126
}
117127

118-
signal ciphertext_equal_check[N][32];
119-
for(var i = 0 ; i < N; i++) {
120-
for(var j = 0 ; j < 32 ; j++) {
121-
ciphertext_equal_check[i][j] <== IsEqual()([computedCipherText[i][j], cipherText[i][j]]);
122-
ciphertext_equal_check[i][j] === 1;
123-
}
124-
}
125-
126-
component toBytes[N];
127-
signal bigEndianPlaintext[N*4];
128-
for(var i = 0 ; i < N; i++) {
129-
toBytes[i] = fromLittleEndianToWords32();
130-
for(var j = 0 ; j < 32 ; j++) {
131-
toBytes[i].data[j] <== plainText[i][j];
128+
component toCiphertextBytes[DATA_BYTES / 4];
129+
signal bigEndianCiphertext[DATA_BYTES];
130+
for (var i = 0 ; i < DATA_BYTES / 4 ; i++) {
131+
toCiphertextBytes[i] = fromLittleEndianToWords32();
132+
for (var j = 0 ; j < 32 ; j++) {
133+
toCiphertextBytes[i].data[j] <== computedCipherText[i][j];
132134
}
133-
for(var j = 0; j < 4; j++) {
134-
bigEndianPlaintext[i*4 + j] <== toBytes[i].words[j];
135+
for (var j = 0 ; j < 4 ; j++) {
136+
bigEndianCiphertext[i*4 + j] <== toCiphertextBytes[i].words[j];
135137
}
136138
}
137-
signal data_hash <== DataHasher(N*4)(bigEndianPlaintext);
139+
140+
signal paddedCiphertextCheck <== IsEqualArrayPaddedLHS(DATA_BYTES)([cipherText, bigEndianCiphertext]);
141+
paddedCiphertextCheck === 1;
142+
143+
signal data_hash <== DataHasher(DATA_BYTES)(plainText);
138144
step_out[0] <== data_hash;
139145
}

circuits/test/chacha20/chacha20-nivc.test.ts

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,41 @@
11
import { WitnessTester } from "circomkit";
2-
import { circomkit, toUint32Array, uintArray32ToBits } from "../common";
2+
import { circomkit, toByte, toUint32Array, uintArray32ToBits } from "../common";
33
import { DataHasher } from "../common/poseidon";
44
import { assert } from "chai";
55

66

77
describe("chacha20-nivc", () => {
8+
let circuit: WitnessTester<["key", "nonce", "counter", "plainText", "cipherText", "step_in"], ["step_out"]>;
89
describe("16 block test", () => {
9-
let circuit: WitnessTester<["key", "nonce", "counter", "plainText", "cipherText", "step_in"], ["step_out"]>;
1010
it("should perform encryption", async () => {
1111
circuit = await circomkit.WitnessTester(`ChaCha20`, {
1212
file: "chacha20/nivc/chacha20_nivc",
1313
template: "ChaCha20_NIVC",
14-
params: [16] // number of 32-bit words in the key, 32 * 16 = 512 bits
14+
params: [64] // number of bytes for plaintext
1515
});
1616
// Test case from RCF https://www.rfc-editor.org/rfc/rfc7539.html#section-2.4.2
17-
// the input encoding here is not the most intuitive. inputs are serialized as little endian.
17+
// the input encoding here is not the most intuitive. inputs are serialized as little endian.
1818
// i.e. "e4e7f110" is serialized as "10 f1 e7 e4". So the way i am reading in inputs is
19-
// to ensure that every 32 bit word is byte reversed before being turned into bits.
19+
// to ensure that every 32 bit word is byte reversed before being turned into bits.
2020
// i think this should be easy when we compute witness in rust.
2121
let keyBytes = [
22-
0x00, 0x01, 0x02, 0x03,
23-
0x04, 0x05, 0x06, 0x07,
24-
0x08, 0x09, 0x0a, 0x0b,
25-
0x0c, 0x0d, 0x0e, 0x0f,
26-
0x10, 0x11, 0x12, 0x13,
27-
0x14, 0x15, 0x16, 0x17,
28-
0x18, 0x19, 0x1a, 0x1b,
29-
0x1c, 0x1d, 0x1e, 0x1f
30-
];
22+
0x00, 0x01, 0x02, 0x03,
23+
0x04, 0x05, 0x06, 0x07,
24+
0x08, 0x09, 0x0a, 0x0b,
25+
0x0c, 0x0d, 0x0e, 0x0f,
26+
0x10, 0x11, 0x12, 0x13,
27+
0x14, 0x15, 0x16, 0x17,
28+
0x18, 0x19, 0x1a, 0x1b,
29+
0x1c, 0x1d, 0x1e, 0x1f
30+
];
3131

3232
let nonceBytes =
3333
[
3434
0x00, 0x00, 0x00, 0x00,
3535
0x00, 0x00, 0x00, 0x4a,
3636
0x00, 0x00, 0x00, 0x00
3737
];
38-
let plaintextBytes =
38+
let plaintextBytes =
3939
[
4040
0x4c, 0x61, 0x64, 0x69, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x47, 0x65, 0x6e, 0x74, 0x6c,
4141
0x65, 0x6d, 0x65, 0x6e, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x61, 0x73,
@@ -48,21 +48,75 @@ describe("chacha20-nivc", () => {
4848
0xe9, 0x7e, 0x7a, 0xec, 0x1d, 0x43, 0x60, 0xc2, 0x0a, 0x27, 0xaf, 0xcc, 0xfd, 0x9f, 0xae, 0x0b,
4949
0xf9, 0x1b, 0x65, 0xc5, 0x52, 0x47, 0x33, 0xab, 0x8f, 0x59, 0x3d, 0xab, 0xcd, 0x62, 0xb3, 0x57,
5050
0x16, 0x39, 0xd6, 0x24, 0xe6, 0x51, 0x52, 0xab, 0x8f, 0x53, 0x0c, 0x35, 0x9f, 0x08, 0x61, 0xd8
51-
];
52-
const ciphertextBits = toInput(Buffer.from(ciphertextBytes))
53-
const plaintextBits = toInput(Buffer.from(plaintextBytes))
54-
const counterBits = uintArray32ToBits([1])[0]
55-
let w = await circuit.compute({
56-
key: toInput(Buffer.from(keyBytes)),
57-
nonce: toInput(Buffer.from(nonceBytes)),
58-
counter: counterBits,
59-
cipherText: ciphertextBits,
60-
plainText: plaintextBits,
51+
];
52+
const counterBits = uintArray32ToBits([1])[0]
53+
let w = await circuit.compute({
54+
key: toInput(Buffer.from(keyBytes)),
55+
nonce: toInput(Buffer.from(nonceBytes)),
56+
counter: counterBits,
57+
cipherText: ciphertextBytes,
58+
plainText: plaintextBytes,
6159
step_in: 0
62-
}, (["step_out"]));
60+
}, (["step_out"]));
6361
assert.deepEqual(w.step_out, DataHasher(plaintextBytes));
6462
});
6563
});
64+
65+
describe("padded plaintext", () => {
66+
it("should perform encryption", async () => {
67+
circuit = await circomkit.WitnessTester(`ChaCha20`, {
68+
file: "chacha20/nivc/chacha20_nivc",
69+
template: "ChaCha20_NIVC",
70+
params: [128] // number of bytes in plaintext
71+
});
72+
// Test case from RCF https://www.rfc-editor.org/rfc/rfc7539.html#section-2.4.2
73+
// the input encoding here is not the most intuitive. inputs are serialized as little endian.
74+
// i.e. "e4e7f110" is serialized as "10 f1 e7 e4". So the way i am reading in inputs is
75+
// to ensure that every 32 bit word is byte reversed before being turned into bits.
76+
// i think this should be easy when we compute witness in rust.
77+
let keyBytes = [
78+
0x00, 0x01, 0x02, 0x03,
79+
0x04, 0x05, 0x06, 0x07,
80+
0x08, 0x09, 0x0a, 0x0b,
81+
0x0c, 0x0d, 0x0e, 0x0f,
82+
0x10, 0x11, 0x12, 0x13,
83+
0x14, 0x15, 0x16, 0x17,
84+
0x18, 0x19, 0x1a, 0x1b,
85+
0x1c, 0x1d, 0x1e, 0x1f
86+
];
87+
88+
let nonceBytes =
89+
[
90+
0x00, 0x00, 0x00, 0x00,
91+
0x00, 0x00, 0x00, 0x4a,
92+
0x00, 0x00, 0x00, 0x00
93+
];
94+
let plaintextBytes =
95+
toByte("Ladies and Gentlemen of the class of '99: If I could offer you only one tip ");
96+
97+
let ciphertextBytes =
98+
[
99+
0x6e, 0x2e, 0x35, 0x9a, 0x25, 0x68, 0xf9, 0x80, 0x41, 0xba, 0x07, 0x28, 0xdd, 0x0d, 0x69, 0x81,
100+
0xe9, 0x7e, 0x7a, 0xec, 0x1d, 0x43, 0x60, 0xc2, 0x0a, 0x27, 0xaf, 0xcc, 0xfd, 0x9f, 0xae, 0x0b,
101+
0xf9, 0x1b, 0x65, 0xc5, 0x52, 0x47, 0x33, 0xab, 0x8f, 0x59, 0x3d, 0xab, 0xcd, 0x62, 0xb3, 0x57,
102+
0x16, 0x39, 0xd6, 0x24, 0xe6, 0x51, 0x52, 0xab, 0x8f, 0x53, 0x0c, 0x35, 0x9f, 0x08, 0x61, 0xd8,
103+
0x07, 0xca, 0x0d, 0xbf, 0x50, 0x0d, 0x6a, 0x61, 0x56, 0xa3, 0x8e, 0x08
104+
];
105+
let totalLength = 128;
106+
let paddedPlaintextBytes = plaintextBytes.concat(Array(totalLength - plaintextBytes.length).fill(0));
107+
let paddedCiphertextBytes = ciphertextBytes.concat(Array(totalLength - ciphertextBytes.length).fill(0));
108+
const counterBits = uintArray32ToBits([1])[0]
109+
let w = await circuit.compute({
110+
key: toInput(Buffer.from(keyBytes)),
111+
nonce: toInput(Buffer.from(nonceBytes)),
112+
counter: counterBits,
113+
cipherText: paddedCiphertextBytes,
114+
plainText: paddedPlaintextBytes,
115+
step_in: 0
116+
}, (["step_out"]));
117+
assert.deepEqual(w.step_out, DataHasher(paddedPlaintextBytes));
118+
});
119+
});
66120
});
67121

68122

circuits/test/full/full.test.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { assert } from "chai";
22
import { circomkit, WitnessTester, toByte, uintArray32ToBits, toUint32Array } from "../common";
33
import { DataHasher } from "../common/poseidon";
44
import { toInput } from "../chacha20/chacha20-nivc.test";
5-
import { buffer } from "stream/consumers";
65

76
// HTTP/1.1 200 OK
87
// content-type: application/json; charset=utf-8
@@ -355,7 +354,7 @@ describe("NIVC_FULL_CHACHA", async () => {
355354
chacha20Circuit = await circomkit.WitnessTester("CHACHA20", {
356355
file: "chacha20/nivc/chacha20_nivc",
357356
template: "ChaCha20_NIVC",
358-
params: [80] // 80 * 32 = 2560 bits / 8 = 320 bytes
357+
params: [320]
359358
});
360359
console.log("#constraints (CHACHA20):", await chacha20Circuit.getConstraintCount());
361360

@@ -392,11 +391,9 @@ describe("NIVC_FULL_CHACHA", async () => {
392391
const init_nivc_input = 0;
393392
// Run ChaCha20
394393
const counterBits = uintArray32ToBits([1])[0]
395-
const ptIn = toInput(Buffer.from(http_response_plaintext));
396-
const ctIn = toInput(Buffer.from(chacha20_http_response_ciphertext));
397394
const keyIn = toInput(Buffer.from(Array(32).fill(0)));
398395
const nonceIn = toInput(Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00]));
399-
let chacha20 = await chacha20Circuit.compute({ key: keyIn, nonce: nonceIn, counter: counterBits, plainText: ptIn, cipherText: ctIn, step_in: init_nivc_input }, ["step_out"]);
396+
let chacha20 = await chacha20Circuit.compute({ key: keyIn, nonce: nonceIn, counter: counterBits, plainText: http_response_plaintext, cipherText: chacha20_http_response_ciphertext, step_in: init_nivc_input }, ["step_out"]);
400397
console.log("ChaCha20 `step_out`:", chacha20.step_out);
401398
assert.deepEqual(http_response_hash, chacha20.step_out);
402399

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "web-prover-circuits",
33
"description": "ZK Circuits for WebProofs",
4-
"version": "0.5.9",
4+
"version": "0.5.10",
55
"license": "Apache-2.0",
66
"repository": {
77
"type": "git",

0 commit comments

Comments
 (0)