Skip to content

Commit bc071c1

Browse files
authored
feat: multiple plaintext authentication (#91)
* add multi-packet ciphertext support * chore: update package version * feat: add polynomial digest with counter * add plaintext_index in authentication and support chaining * add file generation check * add complete test case * add circuit diagram to readme * update version * update version
1 parent 5e720b6 commit bc071c1

18 files changed

Lines changed: 531 additions & 122 deletions

.github/workflows/artifacts.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ jobs:
9595
run: |
9696
make debug # Show what will be processed
9797
make build # Build the circuits
98+
make check # Check all circuits are built
9899
99100
- name: Build and run parameter generator
100101
run: |

Makefile

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ CIRCOM_FILES := $(wildcard $(addsuffix /*_*b.circom,$(TARGET_DIRS)))
77
# Extract target sizes (e.g., "512b", "1024b") from directory names
88
TARGET_SIZES := $(patsubst builds/target_%,%,$(TARGET_DIRS))
99

10+
1011
# Create artifacts directories
1112
$(shell mkdir -p $(addsuffix /artifacts,$(TARGET_DIRS)))
1213

1314
# Default target
1415
.PHONY: all clean
15-
all: build params
16+
all: build check params
1617

1718
# Build target
1819
.PHONY: build
@@ -34,6 +35,22 @@ params:
3435
cargo +nightly run --release -- "$$target_dir/artifacts" "$${size}b" "5" || exit 1; \
3536
done
3637

38+
.PHONY: check
39+
check:
40+
@echo "Checking that all .bin artifacts exist..."
41+
@set -e; \
42+
for circuit in $(CIRCOM_FILES); do \
43+
f1="$$(dirname $${circuit})/artifacts/$$(basename $${circuit} .circom).bin"; \
44+
f2="$$(dirname $${circuit})/artifacts/$$(basename $${circuit} .circom).r1cs"; \
45+
if [ ! -f "$${f1}" ] || [ ! -f "$${f2}" ]; then \
46+
echo "ERROR: Missing artifact '$${f1}', '$${f2}"; \
47+
exit 1; \
48+
else \
49+
echo "OK: $${f1}, $${f2}"; \
50+
fi; \
51+
done
52+
@echo "All artifacts present!"
53+
3754
# Clean target
3855
clean:
3956
rm -rf $(addsuffix /artifacts,$(TARGET_DIRS))

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313

1414
## Overview
1515

16-
`web-prover-circuits` is a project focused on implementing parsers and extractors/selective-disclosure for various data formats inside zero-knowledge circuits.
17-
Specifically, these are designed to be used in an NIVC folding scheme.
16+
`web-prover-circuits` is a project focused on implementing parsers and extractors/selective-disclosure for various data formats inside zero-knowledge circuits.
17+
Specifically, these are designed to be used in an NIVC folding scheme.
1818
Currently, our program layout looks like this:
19-
![v0.7.0](docs/images/v0.7.0.png)
19+
![v0.7.5](docs/images/v0.7.5.png)
2020

2121
## Repository Structure
2222

circuits/chacha20/authentication.circom

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ template PlaintextAuthentication(DATA_BYTES) {
3636
// in => N 32-bit words => N 4 byte words
3737
signal input plaintext[DATA_BYTES];
3838

39+
signal input ciphertext_digest;
40+
signal input plaintext_index_counter;
41+
3942
// step_in should be the ciphertext digest + the HTTP digests + JSON seq digest
4043
signal input step_in[1];
4144

@@ -142,15 +145,13 @@ template PlaintextAuthentication(DATA_BYTES) {
142145
}
143146
}
144147

145-
signal ciphertext_digest <== DataHasher(DATA_BYTES)(bigEndianCiphertext);
146-
147148
signal zeroed_plaintext[DATA_BYTES];
148149
for(var i = 0 ; i < DATA_BYTES ; i++) {
149150
// Sets any padding bytes to zero (which are presumably at the end) so they don't accum into the poly hash
150151
zeroed_plaintext[i] <== (1 - isPadding[i]) * plaintext[i];
151152
}
152-
signal plaintext_digest <== PolynomialDigest(DATA_BYTES)(zeroed_plaintext, ciphertext_digest);
153-
signal plaintext_digest_hashed <== Poseidon(1)([plaintext_digest]);
154-
// TODO: I'm not sure we need to subtract the CT digest
155-
step_out[0] <== step_in[0] - ciphertext_digest + plaintext_digest_hashed;
153+
signal part_ciphertext_digest <== DataHasher(DATA_BYTES)(bigEndianCiphertext);
154+
signal plaintext_digest <== PolynomialDigestWithCounter(DATA_BYTES)(zeroed_plaintext, ciphertext_digest, plaintext_index_counter);
155+
156+
step_out[0] <== step_in[0] - part_ciphertext_digest + plaintext_digest;
156157
}

circuits/http/verification.circom

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
pragma circom 2.1.9;
22

33
include "machine.circom";
4-
// TODO: we don't need this if we do a poly digest of the plaintext in authentication circuit
54
include "../utils/hash.circom";
65

76
template HTTPVerification(DATA_BYTES, MAX_NUMBER_OF_HEADERS) {
@@ -17,9 +16,10 @@ template HTTPVerification(DATA_BYTES, MAX_NUMBER_OF_HEADERS) {
1716
isPadding[i] <== IsEqual()([data[i], -1]);
1817
zeroed_data[i] <== (1 - isPadding[i]) * data[i];
1918
}
20-
signal data_digest <== PolynomialDigest(DATA_BYTES)(zeroed_data, ciphertext_digest);
19+
signal pt_digest <== PolynomialDigest(DATA_BYTES)(zeroed_data, ciphertext_digest);
2120

22-
signal input main_digests[MAX_NUMBER_OF_HEADERS + 1]; // Contains digests of start line and all intended headers (up to `MAX_NUMBER_OF_HEADERS`)
21+
// Contains digests of start line and all intended headers (up to `MAX_NUMBER_OF_HEADERS`)
22+
signal input main_digests[MAX_NUMBER_OF_HEADERS + 1];
2323
signal not_contained[MAX_NUMBER_OF_HEADERS + 1];
2424
var num_to_match = MAX_NUMBER_OF_HEADERS + 1;
2525
for(var i = 0 ; i < MAX_NUMBER_OF_HEADERS + 1 ; i++) {
@@ -106,9 +106,8 @@ template HTTPVerification(DATA_BYTES, MAX_NUMBER_OF_HEADERS) {
106106
State[DATA_BYTES - 1].next_parsing_body === 1;
107107
State[DATA_BYTES - 1].next_line_status === 0;
108108

109-
// TODO: Need to subtract all the header digests here and also wrap them in poseidon. We can use the ones from the input to make this cheaper since they're verified in this circuit!
109+
// subtract all the header digests here and also wrap them in poseidon.
110110
signal body_digest_hashed <== Poseidon(1)([body_digest[DATA_BYTES - 1]]);
111-
signal data_digest_hashed <== Poseidon(1)([data_digest]);
112111
signal option_hash[MAX_NUMBER_OF_HEADERS + 1];
113112
signal main_digests_hashed[MAX_NUMBER_OF_HEADERS + 1];
114113
var accumulated_main_digests_hashed = 0;
@@ -118,5 +117,5 @@ template HTTPVerification(DATA_BYTES, MAX_NUMBER_OF_HEADERS) {
118117
accumulated_main_digests_hashed += main_digests_hashed[i];
119118
}
120119

121-
step_out[0] <== step_in[0] + body_digest_hashed - accumulated_main_digests_hashed - data_digest_hashed; // TODO: data_digest is really plaintext_digest from before, consider changing names
120+
step_out[0] <== step_in[0] + body_digest_hashed - accumulated_main_digests_hashed - pt_digest;
122121
}

circuits/json/extraction.circom

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ include "hash_machine.circom";
66
template JSONExtraction(DATA_BYTES, MAX_STACK_HEIGHT) {
77
signal input data[DATA_BYTES];
88
signal input ciphertext_digest;
9-
signal input sequence_digest;
9+
signal input sequence_digest;
1010
signal input value_digest;
1111

1212
signal input step_in[1];
@@ -23,7 +23,7 @@ template JSONExtraction(DATA_BYTES, MAX_STACK_HEIGHT) {
2323
}
2424
signal intermediate_digest[DATA_BYTES][3 * MAX_STACK_HEIGHT];
2525
signal state_digest[DATA_BYTES];
26-
26+
2727
// Debugging
2828
// for(var i = 0; i<MAX_STACK_HEIGHT; i++) {
2929
// log("State[", 0, "].next_stack[", i,"] = [",State[0].next_stack[i][0], "][", State[0].next_stack[i][1],"]" );
@@ -73,8 +73,8 @@ template JSONExtraction(DATA_BYTES, MAX_STACK_HEIGHT) {
7373
}
7474
state_digest[data_idx] <== accumulator;
7575
sequence_is_matched[data_idx] <== IsEqual()([state_digest[data_idx], sequence_digest]);
76-
77-
// Now check for if the value digest appears
76+
77+
// Now check for if the value digest appears
7878
var value_digest_in_stack = 0;
7979
for(var i = 0 ; i < MAX_STACK_HEIGHT ; i++) {
8080
// A single value can be present only, and it is on index 1, so we can just accum

circuits/test/chacha20/authentication.test.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import { WitnessTester } from "circomkit";
22
import { circomkit, PolynomialDigest, toByte, toUint32Array, uintArray32ToBits, modAdd } from "../common";
33
import { DataHasher } from "../common/poseidon";
44
import { assert } from "chai";
5-
import { poseidon1 } from "poseidon-lite";
65

76
describe("Plaintext Authentication", () => {
8-
let circuit: WitnessTester<["key", "nonce", "counter", "plaintext", "step_in"], ["step_out"]>;
7+
let circuit: WitnessTester<["key", "nonce", "counter", "plaintext", "plaintext_index_counter", "ciphertext_digest", "step_in"], ["step_out"]>;
98
describe("16 block test", () => {
109
it("should perform encryption", async () => {
1110
circuit = await circomkit.WitnessTester(`PlaintextAuthentication`, {
@@ -49,18 +48,21 @@ describe("Plaintext Authentication", () => {
4948
0xf9, 0x1b, 0x65, 0xc5, 0x52, 0x47, 0x33, 0xab, 0x8f, 0x59, 0x3d, 0xab, 0xcd, 0x62, 0xb3, 0x57,
5049
0x16, 0x39, 0xd6, 0x24, 0xe6, 0x51, 0x52, 0xab, 0x8f, 0x53, 0x0c, 0x35, 0x9f, 0x08, 0x61, 0xd8
5150
];
52-
const counterBits = uintArray32ToBits([1])[0]
51+
const counterBits = uintArray32ToBits([1])[0];
52+
let ciphertext_digest = DataHasher(ciphertextBytes);
5353
let w = await circuit.compute({
5454
key: toInput(Buffer.from(keyBytes)),
5555
nonce: toInput(Buffer.from(nonceBytes)),
5656
counter: counterBits,
5757
plaintext: plaintextBytes,
58+
plaintext_index_counter: 0,
59+
ciphertext_digest: ciphertext_digest,
5860
step_in: 0
5961
}, (["step_out"]));
62+
6063
// Output
61-
let ciphertext_digest = DataHasher(ciphertextBytes);
62-
let plaintext_digest_hashed = poseidon1([PolynomialDigest(plaintextBytes, ciphertext_digest)]);
63-
let output = modAdd(plaintext_digest_hashed - ciphertext_digest, BigInt(0));
64+
let plaintext_digest = PolynomialDigest(plaintextBytes, ciphertext_digest, BigInt(0));
65+
let output = modAdd(plaintext_digest - ciphertext_digest, BigInt(0));
6466
assert.deepEqual(w.step_out, output);
6567
});
6668
});
@@ -107,16 +109,19 @@ describe("Plaintext Authentication", () => {
107109
];
108110
let totalLength = 128;
109111
let paddedPlaintextBytes = plaintextBytes.concat(Array(totalLength - plaintextBytes.length).fill(-1));
110-
const counterBits = uintArray32ToBits([1])[0]
112+
const counterBits = uintArray32ToBits([1])[0];
113+
let ciphertext_digest = DataHasher(ciphertextBytes);
111114
let w = await circuit.compute({
112115
key: toInput(Buffer.from(keyBytes)),
113116
nonce: toInput(Buffer.from(nonceBytes)),
114117
counter: counterBits,
115118
plaintext: paddedPlaintextBytes,
116-
step_in: 0
119+
step_in: 0,
120+
plaintext_index_counter: 0,
121+
ciphertext_digest: ciphertext_digest,
117122
}, (["step_out"]));
118-
let ciphertext_digest = DataHasher(ciphertextBytes);
119-
let plaintext_digest = poseidon1([PolynomialDigest(plaintextBytes, ciphertext_digest)]);
123+
124+
let plaintext_digest = PolynomialDigest(plaintextBytes, ciphertext_digest, BigInt(0));
120125
let output = modAdd(plaintext_digest - ciphertext_digest, BigInt(0));
121126
assert.deepEqual(w.step_out, output);
122127
});

circuits/test/common/index.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,13 @@ export function bytesToBigInt(bytes: number[] | Uint8Array): bigint {
238238
}
239239

240240
const prime = BigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617");
241-
export function PolynomialDigest(coeffs: number[], input: bigint): bigint {
241+
export function PolynomialDigest(coeffs: number[], input: bigint, counter: bigint): bigint {
242242
let result = BigInt(0);
243+
// input ** counter
243244
let power = BigInt(1);
245+
for (let i = 0; i < counter; i++) {
246+
power = (power * input) % prime;
247+
}
244248

245249
for (let i = 0; i < coeffs.length; i++) {
246250
result = (result + BigInt(coeffs[i]) * power) % prime;
@@ -306,6 +310,10 @@ export const http_response_ciphertext = [
306310
220, 67, 16, 26,
307311
];
308312

313+
export const http_response_ciphertext_dup = [
314+
66, 0, 57, 150, 208, 144, 184, 250, 244, 106, 253, 118, 105, 7, 189, 139, 78, 36, 126, 180, 79, 153, 22, 237, 62, 182, 186, 218, 239, 75, 35, 97, 231, 115, 106, 144, 4, 226, 80, 116, 121, 35, 136, 75, 89, 30, 78, 124, 59, 165, 121, 235, 65, 63, 174, 154, 143, 75, 78, 33, 20, 38, 21, 133, 42, 97, 147, 38, 195, 192, 90, 33, 165, 244, 196, 97, 167, 218, 2, 114, 7, 50, 34, 109, 211, 202, 30, 101, 196, 146, 61, 67, 166, 66, 255, 90, 38, 74, 162, 187, 173, 9, 149, 98, 16, 65, 79, 186, 61, 110, 193, 228, 163, 82, 238, 26, 30, 105, 206, 69, 2, 102, 238, 165, 47, 159, 39, 5, 197, 150, 0, 69, 51, 234, 132, 22, 219, 250, 22, 69, 111, 87, 123, 211, 13, 88, 46, 215, 6, 12, 107, 65, 69, 9, 235, 217, 180, 167, 132, 204
315+
];
316+
309317
export const http_start_line = [72, 84, 84, 80, 47, 49, 46, 49, 32, 50, 48, 48, 32, 79, 75];
310318

311319
export const http_header_0 = [
@@ -421,7 +429,7 @@ interface ManifestResponse {
421429
};
422430
}
423431

424-
interface Manifest {
432+
export interface Manifest {
425433
response: ManifestResponse;
426434
}
427435

@@ -441,22 +449,28 @@ function headersToBytes(headers: Record<string, string[]>): number[][] {
441449

442450
export function InitialDigest(
443451
manifest: Manifest,
444-
ciphertext: number[],
452+
ciphertexts: number[][],
445453
maxStackHeight: number
446454
): [bigint, bigint] {
455+
let ciphertextDigests: bigint[] = [];
447456
// Create a digest of the ciphertext itself
448-
const ciphertextDigest = DataHasher(ciphertext);
457+
ciphertexts.forEach(ciphertext => {
458+
const ciphertextDigest = DataHasher(ciphertext);
459+
ciphertextDigests.push(ciphertextDigest);
460+
});
461+
462+
let ciphertextDigest = ciphertextDigests.reduce((a, b) => a + b, BigInt(0));
449463

450464
// Digest the start line using the ciphertext_digest as a random input
451465
const startLineBytes = strToBytes(
452466
`${manifest.response.version} ${manifest.response.status} ${manifest.response.message}`
453467
);
454-
const startLineDigest = PolynomialDigest(startLineBytes, ciphertextDigest);
468+
const startLineDigest = PolynomialDigest(startLineBytes, ciphertextDigest, BigInt(0));
455469

456470
// Digest all the headers
457471
const headerBytes = headersToBytes(manifest.response.headers);
458472
const headersDigest = headerBytes.map(bytes =>
459-
PolynomialDigest(bytes, ciphertextDigest)
473+
PolynomialDigest(bytes, ciphertextDigest, BigInt(0))
460474
);
461475

462476
// Digest the JSON sequence

0 commit comments

Comments
 (0)