From 150b7c864ed575e894a560bb324cdc45df729d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jepsen=20=E2=9C=A8?= <57912727+0xJepsen@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:05:01 -0700 Subject: [PATCH 1/4] Revert "Revert "Chacha20" (#50)" This reverts commit 8aa8b0895aa9a19aa81a98431043fab199e9dcec. --- .../target_1024b/aes_gctr_nivc_1024b.circom | 5 - builds/target_1024b/chacha20_nivc_1024.circom | 5 + .../target_1024b/http_body_mask_1024b.circom | 6 - .../http_lock_header_1024b.circom | 5 - ...ttp_parse_and_lock_start_line_1024b.circom | 5 - builds/target_512b/aes_gctr_nivc_512b.circom | 5 - builds/target_512b/chacha20_nivc_512b.circom | 5 + builds/target_512b/http_body_mask_512b.circom | 6 - .../target_512b/http_lock_header_512b.circom | 5 - ...http_parse_and_lock_start_line_512b.circom | 5 - circuits/chacha20/chacha-qr.circom | 88 +++++++++++ circuits/chacha20/chacha-round.circom | 112 ++++++++++++++ circuits/chacha20/chacha20.circom | 108 ++++++++++++++ circuits/chacha20/nivc/chacha20_nivc.circom | 139 ++++++++++++++++++ .../test/aes-gcm/nivc/aes-gctr-nivc.test.ts | 12 +- circuits/test/chacha20/chacha20-nivc.test.ts | 83 +++++++++++ circuits/test/chacha20/chacha20.test.ts | 123 ++++++++++++++++ circuits/test/common/index.ts | 136 +++++++++++++++++ circuits/test/full/full.test.ts | 117 ++++++--------- circuits/test/utils/array.test.ts | 23 ++- circuits/utils/array.circom | 21 +++ circuits/utils/generics-bits.circom | 74 ++++++++++ 22 files changed, 966 insertions(+), 122 deletions(-) delete mode 100644 builds/target_1024b/aes_gctr_nivc_1024b.circom create mode 100644 builds/target_1024b/chacha20_nivc_1024.circom delete mode 100644 builds/target_1024b/http_body_mask_1024b.circom delete mode 100644 builds/target_1024b/http_lock_header_1024b.circom delete mode 100644 builds/target_1024b/http_parse_and_lock_start_line_1024b.circom delete mode 100644 builds/target_512b/aes_gctr_nivc_512b.circom create mode 100644 builds/target_512b/chacha20_nivc_512b.circom delete mode 100644 builds/target_512b/http_body_mask_512b.circom delete mode 100644 builds/target_512b/http_lock_header_512b.circom delete mode 100644 builds/target_512b/http_parse_and_lock_start_line_512b.circom create mode 100644 circuits/chacha20/chacha-qr.circom create mode 100644 circuits/chacha20/chacha-round.circom create mode 100644 circuits/chacha20/chacha20.circom create mode 100644 circuits/chacha20/nivc/chacha20_nivc.circom create mode 100644 circuits/test/chacha20/chacha20-nivc.test.ts create mode 100644 circuits/test/chacha20/chacha20.test.ts create mode 100644 circuits/utils/generics-bits.circom diff --git a/builds/target_1024b/aes_gctr_nivc_1024b.circom b/builds/target_1024b/aes_gctr_nivc_1024b.circom deleted file mode 100644 index 24a2834..0000000 --- a/builds/target_1024b/aes_gctr_nivc_1024b.circom +++ /dev/null @@ -1,5 +0,0 @@ -pragma circom 2.1.9; - -include "../../circuits/aes-gcm/nivc/aes-gctr-nivc.circom"; - -component main { public [step_in] } = AESGCTRFOLD(1); \ No newline at end of file diff --git a/builds/target_1024b/chacha20_nivc_1024.circom b/builds/target_1024b/chacha20_nivc_1024.circom new file mode 100644 index 0000000..9794058 --- /dev/null +++ b/builds/target_1024b/chacha20_nivc_1024.circom @@ -0,0 +1,5 @@ +pragma circom 2.1.9; + +include "../../circuits/chacha20/chacha20.circom"; + +component main = ChaCha20_NIVC(256); \ No newline at end of file diff --git a/builds/target_1024b/http_body_mask_1024b.circom b/builds/target_1024b/http_body_mask_1024b.circom deleted file mode 100644 index 8309267..0000000 --- a/builds/target_1024b/http_body_mask_1024b.circom +++ /dev/null @@ -1,6 +0,0 @@ -pragma circom 2.1.9; - -include "../../circuits/http/nivc/body_mask.circom"; - -component main { public [step_in] } = HTTPMaskBodyNIVC(1024); - diff --git a/builds/target_1024b/http_lock_header_1024b.circom b/builds/target_1024b/http_lock_header_1024b.circom deleted file mode 100644 index de4863d..0000000 --- a/builds/target_1024b/http_lock_header_1024b.circom +++ /dev/null @@ -1,5 +0,0 @@ -pragma circom 2.1.9; - -include "../../circuits/http/nivc/lock_header.circom"; - -component main { public [step_in] } = LockHeader(1024, 50, 100); \ No newline at end of file diff --git a/builds/target_1024b/http_parse_and_lock_start_line_1024b.circom b/builds/target_1024b/http_parse_and_lock_start_line_1024b.circom deleted file mode 100644 index b70daa1..0000000 --- a/builds/target_1024b/http_parse_and_lock_start_line_1024b.circom +++ /dev/null @@ -1,5 +0,0 @@ -pragma circom 2.1.9; - -include "../../circuits/http/nivc/parse_and_lock_start_line.circom"; - -component main { public [step_in] } = ParseAndLockStartLine(1024, 50, 200, 50); \ No newline at end of file diff --git a/builds/target_512b/aes_gctr_nivc_512b.circom b/builds/target_512b/aes_gctr_nivc_512b.circom deleted file mode 100644 index 24a2834..0000000 --- a/builds/target_512b/aes_gctr_nivc_512b.circom +++ /dev/null @@ -1,5 +0,0 @@ -pragma circom 2.1.9; - -include "../../circuits/aes-gcm/nivc/aes-gctr-nivc.circom"; - -component main { public [step_in] } = AESGCTRFOLD(1); \ No newline at end of file diff --git a/builds/target_512b/chacha20_nivc_512b.circom b/builds/target_512b/chacha20_nivc_512b.circom new file mode 100644 index 0000000..ce593a0 --- /dev/null +++ b/builds/target_512b/chacha20_nivc_512b.circom @@ -0,0 +1,5 @@ +pragma circom 2.1.9; + +include "../../circuits/chacha20/nivc/chacha20_nivc.circom"; + +component main = ChaCha20_NIVC(128); \ No newline at end of file diff --git a/builds/target_512b/http_body_mask_512b.circom b/builds/target_512b/http_body_mask_512b.circom deleted file mode 100644 index dea77d1..0000000 --- a/builds/target_512b/http_body_mask_512b.circom +++ /dev/null @@ -1,6 +0,0 @@ -pragma circom 2.1.9; - -include "../../circuits/http/nivc/body_mask.circom"; - -component main { public [step_in] } = HTTPMaskBodyNIVC(512); - diff --git a/builds/target_512b/http_lock_header_512b.circom b/builds/target_512b/http_lock_header_512b.circom deleted file mode 100644 index 5017fe0..0000000 --- a/builds/target_512b/http_lock_header_512b.circom +++ /dev/null @@ -1,5 +0,0 @@ -pragma circom 2.1.9; - -include "../../circuits/http/nivc/lock_header.circom"; - -component main { public [step_in] } = LockHeader(512, 50, 100); \ No newline at end of file diff --git a/builds/target_512b/http_parse_and_lock_start_line_512b.circom b/builds/target_512b/http_parse_and_lock_start_line_512b.circom deleted file mode 100644 index 6463e4c..0000000 --- a/builds/target_512b/http_parse_and_lock_start_line_512b.circom +++ /dev/null @@ -1,5 +0,0 @@ -pragma circom 2.1.9; - -include "../../circuits/http/nivc/parse_and_lock_start_line.circom"; - -component main { public [step_in] } = ParseAndLockStartLine(512, 50, 200, 50); \ No newline at end of file diff --git a/circuits/chacha20/chacha-qr.circom b/circuits/chacha20/chacha-qr.circom new file mode 100644 index 0000000..7ce1487 --- /dev/null +++ b/circuits/chacha20/chacha-qr.circom @@ -0,0 +1,88 @@ +// initially from https://github.com/reclaimprotocol/zk-symmetric-crypto +// modified for our needs +pragma circom 2.1.9; + +include "../utils/generics-bits.circom"; + +/** + * Perform ChaCha Quarter Round + * Assume 4 words of 32 bits each + * Each word must be little endian + */ +template QR() { + signal input in[4][32]; + signal output out[4][32]; + + var tmp[4][32] = in; + + // a += b + component add1 = AddBits(32); + add1.a <== tmp[0]; + add1.b <== tmp[1]; + + tmp[0] = add1.out; + + // d ^= a + component xor1 = XorBits(32); + xor1.a <== tmp[3]; + xor1.b <== tmp[0]; + tmp[3] = xor1.out; + + // d = RotateLeft32BitsUnsafe(d, 16) + component rot1 = RotateLeftBits(32, 16); + rot1.in <== tmp[3]; + tmp[3] = rot1.out; + + // c += d + component add2 = AddBits(32); + add2.a <== tmp[2]; + add2.b <== tmp[3]; + tmp[2] = add2.out; + + // b ^= c + component xor2 = XorBits(32); + xor2.a <== tmp[1]; + xor2.b <== tmp[2]; + tmp[1] = xor2.out; + + // b = RotateLeft32BitsUnsafe(b, 12) + component rot2 = RotateLeftBits(32, 12); + rot2.in <== tmp[1]; + tmp[1] = rot2.out; + + // a += b + component add3 = AddBits(32); + add3.a <== tmp[0]; + add3.b <== tmp[1]; + tmp[0] = add3.out; + + // d ^= a + component xor3 = XorBits(32); + xor3.a <== tmp[3]; + xor3.b <== tmp[0]; + tmp[3] = xor3.out; + + // d = RotateLeft32BitsUnsafe(d, 8) + component rot3 = RotateLeftBits(32, 8); + rot3.in <== tmp[3]; + tmp[3] = rot3.out; + + // c += d + component add4 = AddBits(32); + add4.a <== tmp[2]; + add4.b <== tmp[3]; + tmp[2] = add4.out; + + // b ^= c + component xor4 = XorBits(32); + xor4.a <== tmp[1]; + xor4.b <== tmp[2]; + tmp[1] = xor4.out; + + // b = RotateLeft32BitsUnsafe(b, 7) + component rot4 = RotateLeftBits(32, 7); + rot4.in <== tmp[1]; + tmp[1] = rot4.out; + + out <== tmp; +} \ No newline at end of file diff --git a/circuits/chacha20/chacha-round.circom b/circuits/chacha20/chacha-round.circom new file mode 100644 index 0000000..95e4340 --- /dev/null +++ b/circuits/chacha20/chacha-round.circom @@ -0,0 +1,112 @@ +// initially from https://github.com/reclaimprotocol/zk-symmetric-crypto +// modified for our needs +pragma circom 2.1.9; + +include "./chacha-qr.circom"; +include "../utils/generics-bits.circom"; + +template Round() { + // in => 16 32-bit words + signal input in[16][32]; + // out => 16 32-bit words + signal output out[16][32]; + + var tmp[16][32] = in; + + component rounds[10 * 8]; + component finalAdd[16]; + // i-th round + var i = 0; + // col loop counter + var j = 0; + // counter for the rounds array + var k = 0; + for(i = 0; i < 10; i++) { + // columns of the matrix in a loop + // 0, 4, 8, 12 + // 1, 5, 9, 13 + // 2, 6, 10, 14 + // 3, 7, 11, 15 + for(j = 0; j < 4; j++) { + rounds[k] = QR(); + rounds[k].in[0] <== tmp[j]; + rounds[k].in[1] <== tmp[j + 4]; + rounds[k].in[2] <== tmp[j + 8]; + rounds[k].in[3] <== tmp[j + 12]; + + tmp[j] = rounds[k].out[0]; + tmp[j + 4] = rounds[k].out[1]; + tmp[j + 8] = rounds[k].out[2]; + tmp[j + 12] = rounds[k].out[3]; + + k ++; + } + + // 4 diagnals + // 0, 5, 10, 15 + rounds[k] = QR(); + rounds[k].in[0] <== tmp[0]; + rounds[k].in[1] <== tmp[5]; + rounds[k].in[2] <== tmp[10]; + rounds[k].in[3] <== tmp[15]; + + tmp[0] = rounds[k].out[0]; + tmp[5] = rounds[k].out[1]; + tmp[10] = rounds[k].out[2]; + tmp[15] = rounds[k].out[3]; + + k ++; + + // 1, 6, 11, 12 + rounds[k] = QR(); + rounds[k].in[0] <== tmp[1]; + rounds[k].in[1] <== tmp[6]; + rounds[k].in[2] <== tmp[11]; + rounds[k].in[3] <== tmp[12]; + + tmp[1] = rounds[k].out[0]; + tmp[6] = rounds[k].out[1]; + tmp[11] = rounds[k].out[2]; + tmp[12] = rounds[k].out[3]; + + k ++; + + // 2, 7, 8, 13 + rounds[k] = QR(); + rounds[k].in[0] <== tmp[2]; + rounds[k].in[1] <== tmp[7]; + rounds[k].in[2] <== tmp[8]; + rounds[k].in[3] <== tmp[13]; + + tmp[2] = rounds[k].out[0]; + tmp[7] = rounds[k].out[1]; + tmp[8] = rounds[k].out[2]; + tmp[13] = rounds[k].out[3]; + + k ++; + + // 3, 4, 9, 14 + rounds[k] = QR(); + rounds[k].in[0] <== tmp[3]; + rounds[k].in[1] <== tmp[4]; + rounds[k].in[2] <== tmp[9]; + rounds[k].in[3] <== tmp[14]; + + tmp[3] = rounds[k].out[0]; + tmp[4] = rounds[k].out[1]; + tmp[9] = rounds[k].out[2]; + tmp[14] = rounds[k].out[3]; + + k ++; + } + + // add the result to the input + for(i = 0; i < 16; i++) { + finalAdd[i] = AddBits(32); + finalAdd[i].a <== tmp[i]; + finalAdd[i].b <== in[i]; + tmp[i] = finalAdd[i].out; + } + + out <== tmp; +} diff --git a/circuits/chacha20/chacha20.circom b/circuits/chacha20/chacha20.circom new file mode 100644 index 0000000..4ba00f8 --- /dev/null +++ b/circuits/chacha20/chacha20.circom @@ -0,0 +1,108 @@ +// initially from https://github.com/reclaimprotocol/zk-symmetric-crypto +// modified for our needs +pragma circom 2.1.9; + +include "./chacha-round.circom"; +include "./chacha-qr.circom"; +include "../utils/generics-bits.circom"; + +/** ChaCha20 in counter mode */ +// Chacha20 opperates a 4x4 matrix of 32-bit words where the first 4 words are constants: C +// and the next 8 words are the 256 bit key: K. The next 2 words are the block counter: # +// and the last 2 words are the nonce: N. +// +---+---+---+---+ +// | C | C | C | C | +// +---+---+---+---+ +// | K | K | K | K | +// +---+---+---+---+ +// | K | K | K | K | +// +---+---+---+---+ +// | # | # | N | N | +// +---+---+---+---+ +// paramaterized by n which is the number of 32-bit words to encrypt +template ChaCha20(N) { + // key => 8 32-bit words = 32 bytes + signal input key[8][32]; + // nonce => 3 32-bit words = 12 bytes + signal input nonce[3][32]; + // counter => 32-bit word to apply w nonce + signal input counter[32]; + + // the below can be both ciphertext or plaintext depending on the direction + // in => N 32-bit words => N 4 byte words + signal input in[N][32]; + // out => N 32-bit words => N 4 byte words + signal output out[N][32]; + + var tmp[16][32] = [ + [ + // constant 0x61707865 + 0, 1, 1, 0, 0, 0, 0, 1, 0, + 1, 1, 1, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 0, 0, 0, 0, 1, 1, + 0, 0, 1, 0, 1 + ], + [ + // constant 0x3320646e + 0, 0, 1, 1, 0, 0, 1, 1, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 1, 0, 0, 0, 1, 1, + 0, 1, 1, 1, 0 + ], + [ + // constant 0x79622d32 + 0, 1, 1, 1, 1, 0, 0, 1, 0, + 1, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 1, 1, 0, 1, 0, 0, 1, + 1, 0, 0, 1, 0 + ], + [ + // constant 0x6b206574 + 0, 1, 1, 0, 1, 0, 1, 1, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 1, 0, 1, 0, 1, 1, + 1, 0, 1, 0, 0 + ], + key[0], key[1], key[2], key[3], + key[4], key[5], key[6], key[7], + counter, nonce[0], nonce[1], nonce[2] + ]; + + // 1 in 32-bit words + signal one[32]; + one <== [ + 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, 1 + ]; + + var i = 0; + var j = 0; + + // do the ChaCha20 rounds + component rounds[N/16]; + component xors[N]; + component counter_adder[N/16 - 1]; + + for(i = 0; i < N/16; i++) { + rounds[i] = Round(); + rounds[i].in <== tmp; + // XOR block with input + for(j = 0; j < 16; j++) { + xors[i*16 + j] = XorBits(32); + xors[i*16 + j].a <== in[i*16 + j]; + xors[i*16 + j].b <== rounds[i].out[j]; + out[i*16 + j] <== xors[i*16 + j].out; + } + + if(i < N/16 - 1) { + counter_adder[i] = AddBits(32); + counter_adder[i].a <== tmp[12]; + counter_adder[i].b <== one; + + // increment the counter + tmp[12] = counter_adder[i].out; + } + } +} \ No newline at end of file diff --git a/circuits/chacha20/nivc/chacha20_nivc.circom b/circuits/chacha20/nivc/chacha20_nivc.circom new file mode 100644 index 0000000..16d0a31 --- /dev/null +++ b/circuits/chacha20/nivc/chacha20_nivc.circom @@ -0,0 +1,139 @@ +// initially from https://github.com/reclaimprotocol/zk-symmetric-crypto +// modified for our needs +pragma circom 2.1.9; + +include "../chacha-round.circom"; +include "../chacha-qr.circom"; +include "../../utils/generics-bits.circom"; +include "../../utils/hash.circom"; +include "../../utils/array.circom"; + + +/** ChaCha20 in counter mode */ +// Chacha20 opperates a 4x4 matrix of 32-bit words where the first 4 words are constants: C +// and the next 8 words are the 256 bit key: K. The next 2 words are the block counter: # +// and the last 2 words are the nonce: N. +// +---+---+---+---+ +// | C | C | C | C | +// +---+---+---+---+ +// | K | K | K | K | +// +---+---+---+---+ +// | K | K | K | K | +// +---+---+---+---+ +// | # | N | N | N | +// +---+---+---+---+ +// paramaterized by n which is the number of 32-bit words to encrypt +template ChaCha20_NIVC(N) { + // key => 8 32-bit words = 32 bytes + signal input key[8][32]; + // nonce => 3 32-bit words = 12 bytes + signal input nonce[3][32]; + // counter => 32-bit word to apply w nonce + signal input counter[32]; + + // the below can be both ciphertext or plaintext depending on the direction + // in => N 32-bit words => N 4 byte words + signal input plainText[N][32]; + // out => N 32-bit words => N 4 byte words + signal input cipherText[N][32]; + + signal input step_in[1]; + signal output step_out[1]; + + var tmp[16][32] = [ + [ + // constant 0x61707865 + 0, 1, 1, 0, 0, 0, 0, 1, 0, + 1, 1, 1, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 0, 0, 0, 0, 1, 1, + 0, 0, 1, 0, 1 + ], + [ + // constant 0x3320646e + 0, 0, 1, 1, 0, 0, 1, 1, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 1, 0, 0, 0, 1, 1, + 0, 1, 1, 1, 0 + ], + [ + // constant 0x79622d32 + 0, 1, 1, 1, 1, 0, 0, 1, 0, + 1, 1, 0, 0, 0, 1, 0, 0, 0, + 1, 0, 1, 1, 0, 1, 0, 0, 1, + 1, 0, 0, 1, 0 + ], + [ + // constant 0x6b206574 + 0, 1, 1, 0, 1, 0, 1, 1, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 1, 0, 1, 0, 1, 1, + 1, 0, 1, 0, 0 + ], + key[0], key[1], key[2], key[3], + key[4], key[5], key[6], key[7], + counter, nonce[0], nonce[1], nonce[2] + ]; + + // 1 in 32-bit words + signal one[32]; + one <== [ + 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, 1 + ]; + + var i = 0; + var j = 0; + + // do the ChaCha20 rounds + // rounds opperates on 4 words at a time + component rounds[N/16]; + component xors[N]; + component counter_adder[N/16 - 1]; + + signal computedCipherText[N][32]; + + for(i = 0; i < N/16; i++) { + rounds[i] = Round(); + rounds[i].in <== tmp; + // XOR block with input + for(j = 0; j < 16; j++) { + xors[i*16 + j] = XorBits(32); + xors[i*16 + j].a <== plainText[i*16 + j]; + xors[i*16 + j].b <== rounds[i].out[j]; + computedCipherText[i*16 + j] <== xors[i*16 + j].out; + } + + if(i < N/16 - 1) { + counter_adder[i] = AddBits(32); + counter_adder[i].a <== tmp[12]; + counter_adder[i].b <== one; + + // increment the counter + tmp[12] = counter_adder[i].out; + } + } + + signal ciphertext_equal_check[N][32]; + for(var i = 0 ; i < N; i++) { + for(var j = 0 ; j < 32 ; j++) { + ciphertext_equal_check[i][j] <== IsEqual()([computedCipherText[i][j], cipherText[i][j]]); + ciphertext_equal_check[i][j] === 1; + } + } + + component toBytes[N]; + signal bigEndianPlaintext[N*4]; + for(var i = 0 ; i < N; i++) { + toBytes[i] = fromLittleEndianToWords32(); + for(var j = 0 ; j < 32 ; j++) { + toBytes[i].data[j] <== plainText[i][j]; + } + for(var j = 0; j < 4; j++) { + bigEndianPlaintext[i*4 + j] <== toBytes[i].words[j]; + } + } + signal data_hash <== DataHasher(N*4)(bigEndianPlaintext); + step_out[0] <== data_hash; +} \ No newline at end of file diff --git a/circuits/test/aes-gcm/nivc/aes-gctr-nivc.test.ts b/circuits/test/aes-gcm/nivc/aes-gctr-nivc.test.ts index 1d918b6..28f1faf 100644 --- a/circuits/test/aes-gcm/nivc/aes-gctr-nivc.test.ts +++ b/circuits/test/aes-gcm/nivc/aes-gctr-nivc.test.ts @@ -1,18 +1,8 @@ import { assert } from "chai"; import { WitnessTester } from "circomkit"; -import { circomkit } from "../../common"; +import { circomkit, bytesToBigInt } from "../../common"; import { PoseidonModular } from "../../common/poseidon"; -function bytesToBigInt(bytes: number[] | Uint8Array): bigint { - let result = BigInt(0); - - for (let i = 0; i < 16; i++) { - result += BigInt(bytes[i]) * BigInt(2 ** (8 * i)); - } - - return result; -} - describe("aes-gctr-nivc", () => { let circuit_one_block: WitnessTester<["key", "iv", "plainText", "aad", "ctr", "cipherText", "step_in"], ["step_out"]>; diff --git a/circuits/test/chacha20/chacha20-nivc.test.ts b/circuits/test/chacha20/chacha20-nivc.test.ts new file mode 100644 index 0000000..9b09dc1 --- /dev/null +++ b/circuits/test/chacha20/chacha20-nivc.test.ts @@ -0,0 +1,83 @@ +import { WitnessTester } from "circomkit"; +import { circomkit, toUint32Array, uintArray32ToBits } from "../common"; +import { DataHasher } from "../common/poseidon"; +import { assert } from "chai"; + + +describe("chacha20-nivc", () => { + describe("2 block test", () => { + let circuit: WitnessTester<["key", "nonce", "counter", "plainText", "cipherText", "step_in"], ["step_out"]>; + it("should perform encryption", async () => { + circuit = await circomkit.WitnessTester(`ChaCha20`, { + file: "chacha20/nivc/chacha20_nivc", + template: "ChaCha20_NIVC", + params: [16] // number of 32-bit words in the key, 32 * 16 = 512 bits + }); + // Test case from RCF https://www.rfc-editor.org/rfc/rfc7539.html#section-2.4.2 + // the input encoding here is not the most intuitive. inputs are serialized as little endian. + // i.e. "e4e7f110" is serialized as "10 f1 e7 e4". So the way i am reading in inputs is + // to ensure that every 32 bit word is byte reversed before being turned into bits. + // i think this should be easy when we compute witness in rust. + let keyBytes = [ + 0x00, 0x01, 0x02, 0x03, + 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f + ]; + + let nonceBytes = + [ + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x4a, + 0x00, 0x00, 0x00, 0x00 + ]; + let plaintextBytes = + [ + 0x4c, 0x61, 0x64, 0x69, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x47, 0x65, 0x6e, 0x74, 0x6c, + 0x65, 0x6d, 0x65, 0x6e, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x61, 0x73, + 0x73, 0x20, 0x6f, 0x66, 0x20, 0x27, 0x39, 0x39, 0x3a, 0x20, 0x49, 0x66, 0x20, 0x49, 0x20, 0x63, + 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x6f, 0x66, 0x66, 0x65, 0x72, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x6f, + ]; + let ciphertextBytes = + [ + 0x6e, 0x2e, 0x35, 0x9a, 0x25, 0x68, 0xf9, 0x80, 0x41, 0xba, 0x07, 0x28, 0xdd, 0x0d, 0x69, 0x81, + 0xe9, 0x7e, 0x7a, 0xec, 0x1d, 0x43, 0x60, 0xc2, 0x0a, 0x27, 0xaf, 0xcc, 0xfd, 0x9f, 0xae, 0x0b, + 0xf9, 0x1b, 0x65, 0xc5, 0x52, 0x47, 0x33, 0xab, 0x8f, 0x59, 0x3d, 0xab, 0xcd, 0x62, 0xb3, 0x57, + 0x16, 0x39, 0xd6, 0x24, 0xe6, 0x51, 0x52, 0xab, 0x8f, 0x53, 0x0c, 0x35, 0x9f, 0x08, 0x61, 0xd8 + ]; + const ciphertextBits = toInput(Buffer.from(ciphertextBytes)) + const plaintextBits = toInput(Buffer.from(plaintextBytes)) + const counterBits = uintArray32ToBits([1])[0] + let w = await circuit.compute({ + key: toInput(Buffer.from(keyBytes)), + nonce: toInput(Buffer.from(nonceBytes)), + counter: counterBits, + cipherText: ciphertextBits, + plainText: plaintextBits, + step_in: 0 + }, (["step_out"])); + assert.deepEqual(w.step_out, DataHasher(plaintextBytes)); + }); + }); +}); + + +export function toInput(bytes: Buffer) { + return uintArray32ToBits(toUint32Array(bytes)) +} + +export function fromInput(bits: number[]) { + const uint32Array = new Uint32Array(bits.length / 32); + for (let i = 0; i < uint32Array.length; i++) { + uint32Array[i] = parseInt(bits.slice(i * 32, (i + 1) * 32).join(''), 2); + } + const buffer = Buffer.alloc(uint32Array.length * 4); + for (let i = 0; i < uint32Array.length; i++) { + buffer.writeUInt32LE(uint32Array[i], i * 4); + } + return buffer; +} \ No newline at end of file diff --git a/circuits/test/chacha20/chacha20.test.ts b/circuits/test/chacha20/chacha20.test.ts new file mode 100644 index 0000000..bfe5a38 --- /dev/null +++ b/circuits/test/chacha20/chacha20.test.ts @@ -0,0 +1,123 @@ +import { WitnessTester } from "circomkit"; +import { circomkit, hexToBits, toUint32Array, uintArray32ToBits } from "../common"; + +describe("chacha20", () => { + describe("qtr-round", () => { + let circuit: WitnessTester<["in"], ["out"]>; + it("should perform qtr-round", async () => { + circuit = await circomkit.WitnessTester(`QR`, { + file: "chacha20/chacha-qr", + template: "QR", + }); + // Test case from RCF https://www.rfc-editor.org/rfc/rfc7539.html#section-2.1 + let input = [ + hexToBits("0x11111111"), + hexToBits("0x01020304"), + hexToBits("0x9b8d6f43"), + hexToBits("0x01234567") + ]; + let expected = [ + hexToBits("0xea2a92f4"), + hexToBits("0xcb1cf8ce"), + hexToBits("0x4581472e"), + hexToBits("0x5881c4bb") + ]; + await circuit.expectPass({ in: input }, { out: expected }); + }); + }); + + describe("full-round", () => { + let circuit: WitnessTester<["in"], ["out"]>; + it("should perform qtr-round", async () => { + circuit = await circomkit.WitnessTester(`QR`, { + file: "chacha20/chacha-round", + template: "Round", + }); + // Test case from RCF https://www.rfc-editor.org/rfc/rfc7539.html#section-2.1 + let input = [ + hexToBits("61707865"), hexToBits("3320646e"), hexToBits("79622d32"), hexToBits("6b206574"), + hexToBits("03020100"), hexToBits("07060504"), hexToBits("0b0a0908"), hexToBits("0f0e0d0c"), + hexToBits("13121110"), hexToBits("17161514"), hexToBits("1b1a1918"), hexToBits("1f1e1d1c"), + hexToBits("00000001"), hexToBits("09000000"), hexToBits("4a000000"), hexToBits("00000000") + ]; + let expected = [ + hexToBits("e4e7f110"), hexToBits("15593bd1"), hexToBits("1fdd0f50"), hexToBits("c47120a3"), + hexToBits("c7f4d1c7"), hexToBits("0368c033"), hexToBits("9aaa2204"), hexToBits("4e6cd4c3"), + hexToBits("466482d2"), hexToBits("09aa9f07"), hexToBits("05d7c214"), hexToBits("a2028bd9"), + hexToBits("d19c12b5"), hexToBits("b94e16de"), hexToBits("e883d0cb"), hexToBits("4e3c50a2") + ]; + await circuit.expectPass({ in: input }, { out: expected }); + }); + }); + + // this is failing right now + describe("2 block test", () => { + let circuit: WitnessTester<["key", "nonce", "counter", "in"], ["out"]>; + it("should perform encryption", async () => { + circuit = await circomkit.WitnessTester(`ChaCha20`, { + file: "chacha20/chacha20", + template: "ChaCha20", + params: [16] // number of 32-bit words in the key, 512 / 32 = 16 + }); + // Test case from RCF https://www.rfc-editor.org/rfc/rfc7539.html#section-2.4.2 + // the input encoding here is not the most intuitive. inputs are serialized as little endian. + // i.e. "e4e7f110" is serialized as "10 f1 e7 e4". So the way i am reading in inputs is + // to ensure that every 32 bit word is byte reversed before being turned into bits. + // i think this should be easy when we compute witness in rust. + let test = { + keyBytes: Buffer.from( + [ + 0x00, 0x01, 0x02, 0x03, + 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f + ] + ), + nonceBytes: Buffer.from( + [ + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x4a, + 0x00, 0x00, 0x00, 0x00 + ] + ), + counter: 1, + plaintextBytes: Buffer.from( + [ + 0x4c, 0x61, 0x64, 0x69, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x47, 0x65, 0x6e, 0x74, 0x6c, + 0x65, 0x6d, 0x65, 0x6e, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x61, 0x73, + 0x73, 0x20, 0x6f, 0x66, 0x20, 0x27, 0x39, 0x39, 0x3a, 0x20, 0x49, 0x66, 0x20, 0x49, 0x20, 0x63, + 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x6f, 0x66, 0x66, 0x65, 0x72, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x6f, + ] + ), + ciphertextBytes: Buffer.from( + [ + 0x6e, 0x2e, 0x35, 0x9a, 0x25, 0x68, 0xf9, 0x80, 0x41, 0xba, 0x07, 0x28, 0xdd, 0x0d, 0x69, 0x81, + 0xe9, 0x7e, 0x7a, 0xec, 0x1d, 0x43, 0x60, 0xc2, 0x0a, 0x27, 0xaf, 0xcc, 0xfd, 0x9f, 0xae, 0x0b, + 0xf9, 0x1b, 0x65, 0xc5, 0x52, 0x47, 0x33, 0xab, 0x8f, 0x59, 0x3d, 0xab, 0xcd, 0x62, 0xb3, 0x57, + 0x16, 0x39, 0xd6, 0x24, 0xe6, 0x51, 0x52, 0xab, 0x8f, 0x53, 0x0c, 0x35, 0x9f, 0x08, 0x61, 0xd8 + ] + )} + const ciphertextBits = uintArray32ToBits(toUint32Array(test.ciphertextBytes)) + const plaintextBits = uintArray32ToBits(toUint32Array(test.plaintextBytes)) + const counterBits = uintArray32ToBits([test.counter])[0] + await circuit.expectPass({ + key: uintArray32ToBits(toUint32Array(test.keyBytes)), + nonce: uintArray32ToBits(toUint32Array(test.nonceBytes)), + counter: counterBits, + in: plaintextBits, + }, { out: ciphertextBits }); + + /// decryption since symmetric + const w2 = await circuit.expectPass({ + key: uintArray32ToBits(toUint32Array(test.keyBytes)), + nonce: uintArray32ToBits(toUint32Array(test.nonceBytes)), + counter: counterBits, + in: ciphertextBits, + }, { out: plaintextBits }); + }); + }); +}); \ No newline at end of file diff --git a/circuits/test/common/index.ts b/circuits/test/common/index.ts index b659a16..d65d7c6 100644 --- a/circuits/test/common/index.ts +++ b/circuits/test/common/index.ts @@ -97,4 +97,140 @@ return hexBytes.map(byte => { let n = BigInt(byte); return n; }); +} + +export function hexToBits(hex: string): number[] { + if (hex.startsWith('0x')) { + hex = hex.slice(2); + } + const bits: number[] = []; + for (let i = 0; i < hex.length; i++) { + const nibble = parseInt(hex[i], 16); + for (let j = 3; j >= 0; j--) { + bits.push((nibble >> j) & 1); + } + } + return bits; +} + +export function bitsToHex(bits: number[]): string { + let hex = ''; + for (let i = 0; i < bits.length; i += 4) { + let nibble = 0; + for (let j = 0; j < 4; j++) { + nibble = (nibble << 1) | (bits[i + j] || 0); + } + hex += nibble.toString(16); + } + return hex; +} + +export function bitsToBytes(bits: number[]): number[] { + const bytes: number[] = []; + for (let i = 0; i < bits.length; i += 8) { + let byte = 0; + for (let j = 0; j < 8; j++) { + byte = (byte << 1) | (bits[i + j] || 0); + } + bytes.push(byte); + } + return bytes; +} + +export function BytesToInput(bytes: number[]): number[] { + const output: number[][] = []; + let counter = 1; + let bits: number[] = []; + for (const byte of bytes) { + for (let i = 7; i >= 0; i--) { + bits.push((byte >> i) & 1); + } + if (counter % 4 == 0) { + output.push(bits); + bits = []; + } + + } + return bits; +} +export function hexArrayToBits(bytes: number[]): number[] { + const bits: number[] = []; + for (const byte of bytes) { + for (let i = 7; i >= 0; i--) { + bits.push((byte >> i) & 1); + } + } + return bits; +} + +export function binaryStringToHex(binaryString: string): string { + let hex = ''; + for (let i = 0; i < binaryString.length; i += 4) { + const chunk = binaryString.slice(i, i + 4); + const hexDigit = parseInt(chunk, 2).toString(16); + hex += hexDigit; + } + return hex; +} + +/** + * Converts a Uint8Array to an array of bits. + * BE order. + */ +export function uint8ArrayToBitsBE(buff: Uint8Array | number[]) { + const res: number[] = [] + for (let i = 0; i < buff.length; i++) { + for (let j = 0; j < 8; j++) { + if ((buff[i] >> 7-j) & 1) { + res.push(1); + } else { + res.push(0); + } + } + } + return res; +} + +export function toUint32Array(buf: Uint8Array) { + const arr = new Uint32Array(buf.length / 4) + const arrView = new DataView(buf.buffer, buf.byteOffset, buf.byteLength) + for(let i = 0;i < arr.length;i++) { + arr[i] = arrView.getUint32(i * 4, true) + } + return arr +} + +/** + * Converts a Uint32Array to an array of bits. + * LE order. + */ +export function uintArray32ToBits(uintArray: Uint32Array | number[]) { + const bits: number[][] = [] + for (let i = 0; i < uintArray.length; i++) { + const uint = uintArray[i] + bits.push(numToBitsNumerical(uint)) + } + + return bits +} + +export function numToBitsNumerical(num: number, bitCount = 32) { + const bits: number[] = [] + for(let i = 2 ** (bitCount - 1);i >= 1;i /= 2) { + const bit = num >= i ? 1 : 0 + bits.push(bit) + num -= bit * i + } + + return bits +} + +export function bytesToBigInt(bytes: number[] | Uint8Array): bigint { + let result = BigInt(0); + + for (let i = 0; i < 16; i++) { + result += BigInt(bytes[i]) * BigInt(2 ** (8 * i)); + } + + return result; } \ No newline at end of file diff --git a/circuits/test/full/full.test.ts b/circuits/test/full/full.test.ts index af4f8ea..4c7978d 100644 --- a/circuits/test/full/full.test.ts +++ b/circuits/test/full/full.test.ts @@ -1,6 +1,8 @@ import { assert } from "chai"; -import { circomkit, WitnessTester, toByte } from "../common"; +import { circomkit, WitnessTester, toByte, uintArray32ToBits, toUint32Array } from "../common"; import { DataHasher } from "../common/poseidon"; +import { toInput } from "../chacha20/chacha20-nivc.test"; +import { buffer } from "stream/consumers"; // HTTP/1.1 200 OK // content-type: application/json; charset=utf-8 @@ -39,26 +41,18 @@ const http_response_plaintext = [ 10, 32, 32, 32, 125, 13, 10, 125]; const http_response_ciphertext = [ - 75, 220, 142, 158, 79, 135, 141, 163, 211, 26, 242, 137, 81, 253, 181, 117, - 253, 246, 197, 197, 61, 46, 55, 87, 218, 137, 240, 143, 241, 177, 225, 129, - 80, 114, 125, 72, 45, 18, 224, 179, 79, 231, 153, 198, 163, 252, 197, 219, - 233, 46, 202, 120, 99, 253, 76, 9, 70, 11, 200, 218, 228, 251, 133, 248, - 233, 177, 19, 241, 205, 128, 65, 76, 10, 31, 71, 198, 177, 78, 108, 246, - 175, 152, 42, 97, 255, 182, 157, 245, 123, 95, 130, 101, 129, 138, 236, 146, - 47, 22, 22, 13, 125, 1, 109, 158, 189, 131, 44, 43, 203, 118, 79, 181, - 86, 33, 235, 186, 75, 20, 7, 147, 102, 75, 90, 222, 255, 140, 94, 52, - 191, 145, 192, 71, 239, 245, 247, 175, 117, 136, 173, 235, 250, 189, 74, 155, - 103, 25, 164, 187, 22, 26, 39, 37, 113, 248, 170, 146, 73, 75, 45, 208, - 125, 49, 101, 11, 120, 215, 93, 160, 14, 147, 129, 181, 150, 59, 167, 197, - 230, 122, 77, 245, 247, 215, 136, 98, 1, 180, 213, 30, 214, 88, 83, 42, - 33, 112, 61, 4, 197, 75, 134, 149, 22, 228, 24, 95, 131, 35, 44, 181, - 135, 31, 173, 36, 23, 192, 177, 127, 156, 199, 167, 212, 66, 235, 194, 102, - 61, 144, 121, 59, 187, 179, 212, 34, 117, 47, 96, 3, 169, 73, 204, 88, - 36, 48, 158, 220, 237, 198, 180, 105, 7, 188, 109, 24, 201, 217, 186, 191, - 232, 63, 93, 153, 118, 214, 157, 167, 15, 216, 191, 152, 41, 106, 24, 127, - 8, 144, 78, 218, 133, 125, 89, 97, 10, 246, 8, 244, 112, 169, 190, 206, - 14, 217, 109, 147, 130, 61, 214, 237, 143, 77, 14, 14, 70, 56, 94, 97, - 207, 214, 106, 249, 37, 7, 186, 95, 174, 146, 203, 148, 173, 172, 13, 113 + 2,125,219,141,140,93,49,129,95,178,135,109,48,36,194,46,239,155,160,70,208,147,37,212,17,195,149, + 190,38,215,23,241,84,204,167,184,179,172,187,145,38,75,123,96,81,6,149,36,135,227,226,254,177,90, + 241,159,0,230,183,163,210,88,133,176,9,122,225,83,171,157,185,85,122,4,110,52,2,90,36,189,145,63, + 122,75,94,21,163,24,77,85,110,90,228,157,103,41,59,128,233,149,57,175,121,163,185,144,162,100,17, + 34,9,252,162,223,59,221,106,127,104,11,121,129,154,49,66,220,65,130,171,165,43,8,21,248,12,214,33, + 6,109,3,144,52,124,225,206,223,213,86,186,93,170,146,141,145,140,57,152,226,218,57,30,4,131,161,0, + 248,172,49,206,181,47,231,87,72,96,139,145,117,45,77,134,249,71,87,178,239,30,244,156,70,118,180, + 176,90,92,80,221,177,86,120,222,223,244,109,150,226,142,97,171,210,38,117,143,163,204,25,223,238, + 209,58,59,100,1,86,241,103,152,228,37,187,79,36,136,133,171,41,184,145,146,45,192,173,219,146,133, + 12,246,190,5,54,99,155,8,198,156,174,99,12,210,95,5,128,166,118,50,66,26,20,3,129,232,1,192,104, + 23,152,212,94,97,138,162,90,185,108,221,211,247,184,253,15,16,24,32,240,240,3,148,89,30,54,161, + 131,230,161,217,29,229,251,33,220,230,102,131,245,27,141,220,67,16,26 ]; const http_start_line = [ @@ -212,7 +206,7 @@ const json_key3_mask = [ const json_key3_mask_hash = DataHasher(json_key3_mask); describe("NIVC_FULL", async () => { - let aesCircuit: WitnessTester<["key", "iv", "aad", "ctr", "plainText", "cipherText", "step_in"], ["step_out"]>; + let chacha20Circuit: WitnessTester<["key", "nonce", "counter", "plainText", "cipherText", "step_in"], ["step_out"]>; let httpCircuit: WitnessTester<["step_in", "data", "start_line_hash", "header_hashes", "body_hash"], ["step_out"]>; let json_mask_object_circuit: WitnessTester<["step_in", "data", "key", "keyLen"], ["step_out"]>; let json_mask_arr_circuit: WitnessTester<["step_in", "data", "index"], ["step_out"]>; @@ -227,12 +221,12 @@ describe("NIVC_FULL", async () => { const MAX_VALUE_LENGTH = 32; before(async () => { - aesCircuit = await circomkit.WitnessTester("AESGCTRFOLD", { - file: "aes-gcm/nivc/aes-gctr-nivc", - template: "AESGCTRFOLD", - params: [1] + chacha20Circuit = await circomkit.WitnessTester("CHACHA20", { + file: "chacha20/nivc/chacha20_nivc", + template: "ChaCha20_NIVC", + params: [80] // 80 * 32 = 2560 bits / 8 = 320 bytes }); - console.log("#constraints (AES-GCTR):", await aesCircuit.getConstraintCount()); + console.log("#constraints (CHACHA20):", await chacha20Circuit.getConstraintCount()); httpCircuit = await circomkit.WitnessTester(`HttpNIVC`, { file: "http/nivc/http_nivc", @@ -264,25 +258,18 @@ describe("NIVC_FULL", async () => { }); it("NIVC_CHAIN", async () => { - // Run AES chain - let ctr = [0x00, 0x00, 0x00, 0x01]; + // Run ChaCha20 const init_nivc_input = 0; - - let pt = http_response_plaintext.slice(0, 16); - let ct = http_response_ciphertext.slice(0, 16); - let aes_gcm = await aesCircuit.compute({ key: Array(16).fill(0), iv: Array(12).fill(0), ctr: ctr, plainText: pt, aad: Array(16).fill(0), cipherText: ct, step_in: init_nivc_input }, ["step_out"]); - let i = 0; - console.log("AES `step_out[", i, "]`: ", aes_gcm.step_out); - for (i = 1; i < (DATA_BYTES / 16); i++) { - ctr[3] += 1; // This will work since we don't run a test that overlows a byte - let pt = http_response_plaintext.slice(i * 16, i * 16 + 16); - let ct = http_response_ciphertext.slice(i * 16, i * 16 + 16); - aes_gcm = await aesCircuit.compute({ key: Array(16).fill(0), iv: Array(12).fill(0), ctr: ctr, plainText: pt, aad: Array(16).fill(0), cipherText: ct, step_in: aes_gcm.step_out }, ["step_out"]); - console.log("AES `step_out[", i, "]`: ", aes_gcm.step_out); - } - assert.deepEqual(http_response_hash, aes_gcm.step_out); - - let http = await httpCircuit.compute({ step_in: aes_gcm.step_out, data: http_response_plaintext, start_line_hash: http_start_line_hash, header_hashes: [http_header_0_hash, http_header_1_hash], body_hash: http_body_mask_hash }, ["step_out"]); + const counterBits = uintArray32ToBits([1])[0] + const ptIn = toInput(Buffer.from(http_response_plaintext)); + const ctIn = toInput(Buffer.from(http_response_ciphertext)); + const keyIn = toInput(Buffer.from(Array(32).fill(0))); + const nonceIn = toInput(Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00])); + let chacha20 = await chacha20Circuit.compute({ key: keyIn, nonce: nonceIn, counter: counterBits, plainText: ptIn, cipherText: ctIn, step_in: init_nivc_input }, ["step_out"]); + console.log("ChaCha20 `step_out`:", chacha20.step_out); + assert.deepEqual(DataHasher(http_response_plaintext), chacha20.step_out); + + let http = await httpCircuit.compute({ step_in: chacha20.step_out, data: http_response_plaintext, start_line_hash: http_start_line_hash, header_hashes: [http_header_0_hash, http_header_1_hash], body_hash: http_body_mask_hash }, ["step_out"]); console.log("HttpNIVC `step_out`:", http.step_out); let key0 = [100, 97, 116, 97, 0, 0, 0, 0]; // "data" @@ -326,7 +313,7 @@ describe("NIVC_FULL", async () => { describe("NIVC_FULL_2", async () => { - let aesCircuit: WitnessTester<["key", "iv", "aad", "ctr", "plainText", "cipherText", "step_in"], ["step_out"]>; + let chacha20Circuit: WitnessTester<["key", "nonce", "counter", "plainText", "cipherText", "step_in"], ["step_out"]>; let httpCircuit: WitnessTester<["step_in", "data", "start_line_hash", "header_hashes", "body_hash"], ["step_out"]>; let json_mask_object_circuit: WitnessTester<["step_in", "data", "key", "keyLen"], ["step_out"]>; let json_mask_arr_circuit: WitnessTester<["step_in", "data", "index"], ["step_out"]>; @@ -341,12 +328,12 @@ describe("NIVC_FULL_2", async () => { const MAX_VALUE_LENGTH = 32; before(async () => { - aesCircuit = await circomkit.WitnessTester("AESGCTRFOLD", { - file: "aes-gcm/nivc/aes-gctr-nivc", - template: "AESGCTRFOLD", - params: [2] + chacha20Circuit = await circomkit.WitnessTester("CHACHA20", { + file: "chacha20/nivc/chacha20_nivc", + template: "ChaCha20_NIVC", + params: [80] // 80 * 32 = 2560 bits / 8 = 320 bytes }); - console.log("#constraints (AES-GCTR):", await aesCircuit.getConstraintCount()); + console.log("#constraints (CHACHA20):", await chacha20Circuit.getConstraintCount()); httpCircuit = await circomkit.WitnessTester(`HttpNIVC`, { file: "http/nivc/http_nivc", @@ -378,25 +365,19 @@ describe("NIVC_FULL_2", async () => { }); it("NIVC_CHAIN_2", async () => { - // Run AES chain - let ctr = [0x00, 0x00, 0x00, 0x01]; + // Run ChaCha20 const init_nivc_input = 0; + const counterBits = uintArray32ToBits([1])[0] + const ptIn = toInput(Buffer.from(http_response_plaintext)); + const ctIn = toInput(Buffer.from(http_response_ciphertext)); + const keyIn = toInput(Buffer.from(Array(32).fill(0))); + const nonceIn = toInput(Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00])); + + let chacha20 = await chacha20Circuit.compute({ key: keyIn, nonce: nonceIn, counter: counterBits, plainText: ptIn, cipherText: ctIn, step_in: init_nivc_input }, ["step_out"]); + console.log("ChaCha20 `step_out`:", chacha20.step_out); + assert.deepEqual(DataHasher(http_response_plaintext), chacha20.step_out); - let pt = [http_response_plaintext.slice(0, 16), http_response_plaintext.slice(16, 32)]; - let ct = [http_response_ciphertext.slice(0, 16), http_response_ciphertext.slice(16, 32)]; - let aes_gcm = await aesCircuit.compute({ key: Array(16).fill(0), iv: Array(12).fill(0), ctr: ctr, plainText: pt, aad: Array(16).fill(0), cipherText: ct, step_in: init_nivc_input }, ["step_out"]); - let i = 0; - console.log("AES `step_out[", i, "]`: ", aes_gcm.step_out); - for (i = 1; i < (DATA_BYTES / (16 * 2)); i++) { - ctr[3] += 2; // This will work since we don't run a test that overlows a byte - let pt = [http_response_plaintext.slice(i * 32, i * 32 + 16), http_response_plaintext.slice(i * 32 + 16, i * 32 + 32)]; - let ct = [http_response_ciphertext.slice(i * 32, i * 32 + 16), http_response_ciphertext.slice(i * 32 + 16, i * 32 + 32)]; - aes_gcm = await aesCircuit.compute({ key: Array(16).fill(0), iv: Array(12).fill(0), ctr: ctr, plainText: pt, aad: Array(16).fill(0), cipherText: ct, step_in: aes_gcm.step_out }, ["step_out"]); - console.log("AES `step_out[", i, "]`: ", aes_gcm.step_out); - } - assert.deepEqual(http_response_hash, aes_gcm.step_out); - - let http = await httpCircuit.compute({ step_in: aes_gcm.step_out, data: http_response_plaintext, start_line_hash: http_start_line_hash, header_hashes: [http_header_0_hash, http_header_1_hash], body_hash: http_body_mask_hash }, ["step_out"]); + let http = await httpCircuit.compute({ step_in: chacha20.step_out, data: http_response_plaintext, start_line_hash: http_start_line_hash, header_hashes: [http_header_0_hash, http_header_1_hash], body_hash: http_body_mask_hash }, ["step_out"]); console.log("HttpNIVC `step_out`:", http.step_out); let key0 = [100, 97, 116, 97, 0, 0, 0, 0]; // "data" diff --git a/circuits/test/utils/array.test.ts b/circuits/test/utils/array.test.ts index b8538e6..7069932 100644 --- a/circuits/test/utils/array.test.ts +++ b/circuits/test/utils/array.test.ts @@ -461,4 +461,25 @@ describe("ToBlocks", () => { } ); }); - }); \ No newline at end of file + }); + + +describe("fromLittleEndianToWords32", () => { + let circuit: WitnessTester<["data"], ["words"]>; + it("fromLittleEndianToWords32", async () => { + circuit = await circomkit.WitnessTester(`fromLittleEndianToWords32`, { + file: "utils/array", + template: "fromLittleEndianToWords32", + }); + console.log("#constraints:", await circuit.getConstraintCount()); + + let input = [ + 0, 1, 0, 1, 0, 0, 0, 0, 0, + 1, 0, 1, 0, 1, 0, 0, 0, 1, + 0, 1, 0, 1, 0, 0, 0, 1, 0, + 0, 1, 0, 0, 0 + ]; + await circuit.expectPass({data: input}, {words: [72, 84, 84, 80]}) + }); +}); + diff --git a/circuits/utils/array.circom b/circuits/utils/array.circom index 5b393f5..75f7380 100644 --- a/circuits/utils/array.circom +++ b/circuits/utils/array.circom @@ -511,4 +511,25 @@ template IncrementWord() { carry[i - 1] <== IsGreaterThan[i].out; } } +} + +// // from little endian to 32 bit words +// // example: +// 0, 1, 0, 1, 0, 0, 0, 0, => 80 +// 0, 1, 0, 1, 0, 1, 0, 0, => 84 +// 0, 1, 0, 1, 0, 1, 0, 0, => 84 +// 0, 1, 0, 0, 1, 0, 0, 0, => 72 // getting ten here +// shoud be encoded as +// 72, 84, 84, 80 +template fromLittleEndianToWords32() { + signal input data[32]; + signal output words[4]; + component Bits2Num[4]; + for(var i = 3; i >= 0; i--) { + Bits2Num[i] = Bits2Num(8); + for(var j = 7; j >= 0; j--) { + Bits2Num[i].in[7-j] <== data[i*8 + j]; + } + words[3-i] <== Bits2Num[i].out; + } } \ No newline at end of file diff --git a/circuits/utils/generics-bits.circom b/circuits/utils/generics-bits.circom new file mode 100644 index 0000000..b1311e9 --- /dev/null +++ b/circuits/utils/generics-bits.circom @@ -0,0 +1,74 @@ +// initially from https://github.com/reclaimprotocol/zk-symmetric-crypto +// modified for our needs +pragma circom 2.1.9; + +/** + * Add N bit numbers together + * copied in from: https://github.com/iden3/circomlib/blob/master/circuits/binsum.circom + * but rewritten slightly to reduce the final number of wires & labels + * and possibly look at reducing the number of constraints + */ +template AddBits(BITS) { + signal input a[BITS]; + signal input b[BITS]; + signal output out[BITS]; + signal carrybit; + + var lin = 0; + var lout = 0; + + var k; + var j = 0; + + var e2; + + // create e2 which + // is the numerical sum of 2^k + e2 = 1; + for (k = BITS - 1; k >= 0; k--) { + lin += (a[k] + b[k]) * e2; + e2 *= 2; + } + + e2 = 1; + for (k = BITS - 1; k >= 0; k--) { + out[k] <-- (lin >> j) & 1; + // Ensure out is binary + out[k] * (out[k] - 1) === 0; + lout += out[k] * e2; + e2 *= 2; + j += 1; + } + + carrybit <-- (lin >> j) & 1; + // Ensure out is binary + carrybit * (carrybit - 1) === 0; + lout += carrybit * e2; + + // Ensure the sum matches + lin === lout; +} + +/** + * Rotate left a BITS bit integer L bits + */ +template RotateLeftBits(BITS, L) { + signal input in[BITS]; + signal output out[BITS]; + for (var i = 0; i < BITS; i++) { + out[i] <== in[(i + L) % BITS]; + } +} + +/** + * XOR BITS-bit words +*/ +template XorBits(BITS) { + signal input a[BITS]; + signal input b[BITS]; + signal output out[BITS]; + + for (var k=0; k Date: Tue, 19 Nov 2024 21:00:01 -0700 Subject: [PATCH 2/4] Update circuits/test/chacha20/chacha20-nivc.test.ts Co-authored-by: Colin Roberts --- circuits/test/chacha20/chacha20-nivc.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circuits/test/chacha20/chacha20-nivc.test.ts b/circuits/test/chacha20/chacha20-nivc.test.ts index 9b09dc1..2d878d1 100644 --- a/circuits/test/chacha20/chacha20-nivc.test.ts +++ b/circuits/test/chacha20/chacha20-nivc.test.ts @@ -5,7 +5,7 @@ import { assert } from "chai"; describe("chacha20-nivc", () => { - describe("2 block test", () => { + describe("16 block test", () => { let circuit: WitnessTester<["key", "nonce", "counter", "plainText", "cipherText", "step_in"], ["step_out"]>; it("should perform encryption", async () => { circuit = await circomkit.WitnessTester(`ChaCha20`, { From 28c4d1c25ee25f049057fded6d3c482eabb32d12 Mon Sep 17 00:00:00 2001 From: Waylon Jepsen Date: Wed, 20 Nov 2024 11:03:53 +0700 Subject: [PATCH 3/4] add back aes artifacts --- builds/target_1024b/aes_gctr_nivc_1024b.circom | 5 +++++ builds/target_512b/aes_gctr_nivc_512b.circom | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 builds/target_1024b/aes_gctr_nivc_1024b.circom create mode 100644 builds/target_512b/aes_gctr_nivc_512b.circom diff --git a/builds/target_1024b/aes_gctr_nivc_1024b.circom b/builds/target_1024b/aes_gctr_nivc_1024b.circom new file mode 100644 index 0000000..24a2834 --- /dev/null +++ b/builds/target_1024b/aes_gctr_nivc_1024b.circom @@ -0,0 +1,5 @@ +pragma circom 2.1.9; + +include "../../circuits/aes-gcm/nivc/aes-gctr-nivc.circom"; + +component main { public [step_in] } = AESGCTRFOLD(1); \ No newline at end of file diff --git a/builds/target_512b/aes_gctr_nivc_512b.circom b/builds/target_512b/aes_gctr_nivc_512b.circom new file mode 100644 index 0000000..24a2834 --- /dev/null +++ b/builds/target_512b/aes_gctr_nivc_512b.circom @@ -0,0 +1,5 @@ +pragma circom 2.1.9; + +include "../../circuits/aes-gcm/nivc/aes-gctr-nivc.circom"; + +component main { public [step_in] } = AESGCTRFOLD(1); \ No newline at end of file From ea6ab02f604548e2613ab3adce81fd1fb2fcabf3 Mon Sep 17 00:00:00 2001 From: Waylon Jepsen Date: Wed, 20 Nov 2024 11:24:43 +0700 Subject: [PATCH 4/4] put aes back in full test --- circuits/test/full/full.test.ts | 92 ++++++++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/circuits/test/full/full.test.ts b/circuits/test/full/full.test.ts index 4c7978d..6139fc3 100644 --- a/circuits/test/full/full.test.ts +++ b/circuits/test/full/full.test.ts @@ -40,7 +40,7 @@ const http_response_plaintext = [ 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 125, 13, 10, 32, 32, 32, 32, 32, 32, 32, 93, 13, 10, 32, 32, 32, 125, 13, 10, 125]; -const http_response_ciphertext = [ +const chacha20_http_response_ciphertext = [ 2,125,219,141,140,93,49,129,95,178,135,109,48,36,194,46,239,155,160,70,208,147,37,212,17,195,149, 190,38,215,23,241,84,204,167,184,179,172,187,145,38,75,123,96,81,6,149,36,135,227,226,254,177,90, 241,159,0,230,183,163,210,88,133,176,9,122,225,83,171,157,185,85,122,4,110,52,2,90,36,189,145,63, @@ -55,6 +55,29 @@ const http_response_ciphertext = [ 131,230,161,217,29,229,251,33,220,230,102,131,245,27,141,220,67,16,26 ]; +const aes_http_response_ciphertext = [ + 75, 220, 142, 158, 79, 135, 141, 163, 211, 26, 242, 137, 81, 253, 181, 117, + 253, 246, 197, 197, 61, 46, 55, 87, 218, 137, 240, 143, 241, 177, 225, 129, + 80, 114, 125, 72, 45, 18, 224, 179, 79, 231, 153, 198, 163, 252, 197, 219, + 233, 46, 202, 120, 99, 253, 76, 9, 70, 11, 200, 218, 228, 251, 133, 248, + 233, 177, 19, 241, 205, 128, 65, 76, 10, 31, 71, 198, 177, 78, 108, 246, + 175, 152, 42, 97, 255, 182, 157, 245, 123, 95, 130, 101, 129, 138, 236, 146, + 47, 22, 22, 13, 125, 1, 109, 158, 189, 131, 44, 43, 203, 118, 79, 181, + 86, 33, 235, 186, 75, 20, 7, 147, 102, 75, 90, 222, 255, 140, 94, 52, + 191, 145, 192, 71, 239, 245, 247, 175, 117, 136, 173, 235, 250, 189, 74, 155, + 103, 25, 164, 187, 22, 26, 39, 37, 113, 248, 170, 146, 73, 75, 45, 208, + 125, 49, 101, 11, 120, 215, 93, 160, 14, 147, 129, 181, 150, 59, 167, 197, + 230, 122, 77, 245, 247, 215, 136, 98, 1, 180, 213, 30, 214, 88, 83, 42, + 33, 112, 61, 4, 197, 75, 134, 149, 22, 228, 24, 95, 131, 35, 44, 181, + 135, 31, 173, 36, 23, 192, 177, 127, 156, 199, 167, 212, 66, 235, 194, 102, + 61, 144, 121, 59, 187, 179, 212, 34, 117, 47, 96, 3, 169, 73, 204, 88, + 36, 48, 158, 220, 237, 198, 180, 105, 7, 188, 109, 24, 201, 217, 186, 191, + 232, 63, 93, 153, 118, 214, 157, 167, 15, 216, 191, 152, 41, 106, 24, 127, + 8, 144, 78, 218, 133, 125, 89, 97, 10, 246, 8, 244, 112, 169, 190, 206, + 14, 217, 109, 147, 130, 61, 214, 237, 143, 77, 14, 14, 70, 56, 94, 97, + 207, 214, 106, 249, 37, 7, 186, 95, 174, 146, 203, 148, 173, 172, 13, 113 +] + const http_start_line = [ 72, 84, 84, 80, 47, 49, 46, 49, 32, 50, 48, 48, 32, 79, 75, 13, 10, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -206,6 +229,7 @@ const json_key3_mask = [ const json_key3_mask_hash = DataHasher(json_key3_mask); describe("NIVC_FULL", async () => { + let aesCircuit: WitnessTester<["key", "iv", "aad", "ctr", "plainText", "cipherText", "step_in"], ["step_out"]>; let chacha20Circuit: WitnessTester<["key", "nonce", "counter", "plainText", "cipherText", "step_in"], ["step_out"]>; let httpCircuit: WitnessTester<["step_in", "data", "start_line_hash", "header_hashes", "body_hash"], ["step_out"]>; let json_mask_object_circuit: WitnessTester<["step_in", "data", "key", "keyLen"], ["step_out"]>; @@ -221,6 +245,13 @@ describe("NIVC_FULL", async () => { const MAX_VALUE_LENGTH = 32; before(async () => { + aesCircuit = await circomkit.WitnessTester("AESGCTRFOLD", { + file: "aes-gcm/nivc/aes-gctr-nivc", + template: "AESGCTRFOLD", + params: [1] + }); + console.log("#constraints (AES):", await aesCircuit.getConstraintCount()); + chacha20Circuit = await circomkit.WitnessTester("CHACHA20", { file: "chacha20/nivc/chacha20_nivc", template: "ChaCha20_NIVC", @@ -258,16 +289,36 @@ describe("NIVC_FULL", async () => { }); it("NIVC_CHAIN", async () => { - // Run ChaCha20 const init_nivc_input = 0; + + // This tests both AES and ChaCha20 right now but we will only one in practice. + // Run AES chain + let ctr = [0x00, 0x00, 0x00, 0x01]; + let pt = http_response_plaintext.slice(0, 16); + let ct = aes_http_response_ciphertext.slice(0, 16); + let aes_gcm = await aesCircuit.compute({ key: Array(16).fill(0), iv: Array(12).fill(0), ctr: ctr, plainText: pt, aad: Array(16).fill(0), cipherText: ct, step_in: init_nivc_input }, ["step_out"]); + let i = 0; + console.log("AES `step_out[", i, "]`: ", aes_gcm.step_out); + for (i = 1; i < (DATA_BYTES / 16); i++) { + ctr[3] += 1; // This will work since we don't run a test that overlows a byte + let pt = http_response_plaintext.slice(i * 16, i * 16 + 16); + let ct = aes_http_response_ciphertext.slice(i * 16, i * 16 + 16); + aes_gcm = await aesCircuit.compute({ key: Array(16).fill(0), iv: Array(12).fill(0), ctr: ctr, plainText: pt, aad: Array(16).fill(0), cipherText: ct, step_in: aes_gcm.step_out }, ["step_out"]); + console.log("AES `step_out[", i, "]`: ", aes_gcm.step_out); + } + assert.deepEqual(http_response_hash, aes_gcm.step_out); + + // Run ChaCha20 const counterBits = uintArray32ToBits([1])[0] const ptIn = toInput(Buffer.from(http_response_plaintext)); - const ctIn = toInput(Buffer.from(http_response_ciphertext)); + const ctIn = toInput(Buffer.from(chacha20_http_response_ciphertext)); const keyIn = toInput(Buffer.from(Array(32).fill(0))); const nonceIn = toInput(Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00])); let chacha20 = await chacha20Circuit.compute({ key: keyIn, nonce: nonceIn, counter: counterBits, plainText: ptIn, cipherText: ctIn, step_in: init_nivc_input }, ["step_out"]); console.log("ChaCha20 `step_out`:", chacha20.step_out); - assert.deepEqual(DataHasher(http_response_plaintext), chacha20.step_out); + assert.deepEqual(http_response_hash, chacha20.step_out); + + assert.deepEqual(chacha20.step_out, aes_gcm.step_out); let http = await httpCircuit.compute({ step_in: chacha20.step_out, data: http_response_plaintext, start_line_hash: http_start_line_hash, header_hashes: [http_header_0_hash, http_header_1_hash], body_hash: http_body_mask_hash }, ["step_out"]); console.log("HttpNIVC `step_out`:", http.step_out); @@ -313,6 +364,7 @@ describe("NIVC_FULL", async () => { describe("NIVC_FULL_2", async () => { + let aesCircuit: WitnessTester<["key", "iv", "aad", "ctr", "plainText", "cipherText", "step_in"], ["step_out"]>; let chacha20Circuit: WitnessTester<["key", "nonce", "counter", "plainText", "cipherText", "step_in"], ["step_out"]>; let httpCircuit: WitnessTester<["step_in", "data", "start_line_hash", "header_hashes", "body_hash"], ["step_out"]>; let json_mask_object_circuit: WitnessTester<["step_in", "data", "key", "keyLen"], ["step_out"]>; @@ -328,6 +380,13 @@ describe("NIVC_FULL_2", async () => { const MAX_VALUE_LENGTH = 32; before(async () => { + aesCircuit = await circomkit.WitnessTester("AESGCTRFOLD", { + file: "aes-gcm/nivc/aes-gctr-nivc", + template: "AESGCTRFOLD", + params: [2] + }); + console.log("#constraints (AES):", await aesCircuit.getConstraintCount()); + chacha20Circuit = await circomkit.WitnessTester("CHACHA20", { file: "chacha20/nivc/chacha20_nivc", template: "ChaCha20_NIVC", @@ -365,17 +424,36 @@ describe("NIVC_FULL_2", async () => { }); it("NIVC_CHAIN_2", async () => { - // Run ChaCha20 const init_nivc_input = 0; + + // Run AES chain + let ctr = [0x00, 0x00, 0x00, 0x01]; + let pt = [http_response_plaintext.slice(0, 16), http_response_plaintext.slice(16, 32)]; + let ct = [aes_http_response_ciphertext.slice(0, 16), aes_http_response_ciphertext.slice(16, 32)]; + let aes_gcm = await aesCircuit.compute({ key: Array(16).fill(0), iv: Array(12).fill(0), ctr: ctr, plainText: pt, aad: Array(16).fill(0), cipherText: ct, step_in: init_nivc_input }, ["step_out"]); + let i = 0; + console.log("AES `step_out[", i, "]`: ", aes_gcm.step_out); + for (i = 1; i < (DATA_BYTES / (16 * 2)); i++) { + ctr[3] += 2; // This will work since we don't run a test that overlows a byte + let pt = [http_response_plaintext.slice(i * 32, i * 32 + 16), http_response_plaintext.slice(i * 32 + 16, i * 32 + 32)]; + let ct = [aes_http_response_ciphertext.slice(i * 32, i * 32 + 16), aes_http_response_ciphertext.slice(i * 32 + 16, i * 32 + 32)]; + aes_gcm = await aesCircuit.compute({ key: Array(16).fill(0), iv: Array(12).fill(0), ctr: ctr, plainText: pt, aad: Array(16).fill(0), cipherText: ct, step_in: aes_gcm.step_out }, ["step_out"]); + console.log("AES `step_out[", i, "]`: ", aes_gcm.step_out); + } + assert.deepEqual(http_response_hash, aes_gcm.step_out); + + // Run ChaCha20 const counterBits = uintArray32ToBits([1])[0] const ptIn = toInput(Buffer.from(http_response_plaintext)); - const ctIn = toInput(Buffer.from(http_response_ciphertext)); + const ctIn = toInput(Buffer.from(chacha20_http_response_ciphertext)); const keyIn = toInput(Buffer.from(Array(32).fill(0))); const nonceIn = toInput(Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00])); let chacha20 = await chacha20Circuit.compute({ key: keyIn, nonce: nonceIn, counter: counterBits, plainText: ptIn, cipherText: ctIn, step_in: init_nivc_input }, ["step_out"]); console.log("ChaCha20 `step_out`:", chacha20.step_out); - assert.deepEqual(DataHasher(http_response_plaintext), chacha20.step_out); + assert.deepEqual(http_response_hash, chacha20.step_out); + + assert.deepEqual(chacha20.step_out, aes_gcm.step_out); let http = await httpCircuit.compute({ step_in: chacha20.step_out, data: http_response_plaintext, start_line_hash: http_start_line_hash, header_hashes: [http_header_0_hash, http_header_1_hash], body_hash: http_body_mask_hash }, ["step_out"]); console.log("HttpNIVC `step_out`:", http.step_out); diff --git a/package.json b/package.json index 80d75cf..2094b0c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "web-prover-circuits", "description": "ZK Circuits for WebProofs", - "version": "0.5.5", + "version": "0.5.6", "license": "Apache-2.0", "repository": { "type": "git",