rework public inputs

This commit is contained in:
0xturboblitz
2023-11-04 22:43:33 +03:00
parent 5788f63c1e
commit 10f6e2b032
9 changed files with 497 additions and 94 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 zk-email
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,175 @@
pragma circom 2.1.2;
include "./utils.circom";
// circuits in this folder copied from zk-email, credits to them
// A set of utils for shifting and packing signal arrays
// Performs extraction of reveal signals and packed signals
// From https://github.com/iden3/circomlib/blob/master/circuits/multiplexer.circom
function log2(a) {
if (a == 0) {
return 0;
}
var n = 1;
var r = 1;
while (n<a) {
r++;
n *= 2;
}
return r;
}
// Pack size is # of chunks i.e. number of char signals that fit into a signal (default 7 but can be 30)
template PackBytes(max_in_signals, max_out_signals, pack_size) {
assert(max_out_signals == ((max_in_signals - 1) \ pack_size + 1)); // Packing constant is wrong
signal input in[max_in_signals];
signal output out[max_out_signals];
component packer[max_out_signals];
for (var i = 0; i < max_out_signals; i++) {
packer[i] = Bytes2Packed(pack_size);
for (var j = 0; j < pack_size; j++) {
var reveal_idx = i * pack_size + j;
if (reveal_idx < max_in_signals) {
packer[i].in[j] <== in[i * pack_size + j];
} else {
packer[i].in[j] <== 0;
}
}
out[i] <== packer[i].out;
}
}
// Shift the input left by variable size of bytes.
// From https://demo.hedgedoc.org/s/Le0R3xUhB
// Note that if len_bits < max_substr * C, C around 1, then
// it's more efficient to use Sampriti's O(nk) solution instead
template VarShiftLeft(in_array_len, out_array_len) {
var len_bits = log2(in_array_len);
assert(in_array_len <= (1 << len_bits));
signal input in[in_array_len]; // x
signal input shift; // k
signal output out[out_array_len]; // y
component n2b = Num2Bits(len_bits);
n2b.in <== shift;
signal tmp[len_bits][in_array_len];
for (var j = 0; j < len_bits; j++) {
for (var i = 0; i < in_array_len; i++) {
var offset = (i + (1 << j)) % in_array_len;
// Shift left by 2^j indices if bit is 1
if (j == 0) {
tmp[j][i] <== n2b.out[j] * (in[offset] - in[i]) + in[i];
} else {
tmp[j][i] <== n2b.out[j] * (tmp[j-1][offset] - tmp[j-1][i]) + tmp[j-1][i];
}
}
}
// Return last row
// TODO: Assert the rest of the values are 0
for (var i = 0; i < out_array_len; i++) {
out[i] <== tmp[len_bits - 1][i];
}
}
// Shift the input left by variable size of bytes.
// Its input and output are the same as those of VarShiftLeft.
// However, it assumes the input is the masked bytes and checks that shift is the first index of the non-masked bytes.
template VarShiftMaskedStr(in_array_len, out_array_len) {
signal input in[in_array_len]; // x
signal input shift; // k
signal output out[out_array_len] <== VarShiftLeft(in_array_len, out_array_len)(in, shift);
signal is_target_idx[in_array_len];
signal prev_byte[in_array_len];
signal is_this_zero[in_array_len];
signal is_prev_zero[in_array_len];
for(var i = 0; i < in_array_len; i++) {
is_target_idx[i] <== IsEqual()([i, shift]);
is_this_zero[i] <== IsZero()(in[i]);
is_target_idx[i] * is_this_zero[i] === 0;
if(i == 0) {
is_prev_zero[i] <== 1;
} else {
is_prev_zero[i] <== IsZero()(in[i-1]);
}
is_target_idx[i] * (1 - is_prev_zero[i]) === 0;
}
}
// From https://demo.hedgedoc.org/s/Le0R3xUhB -- unused
template ClearSubarrayAfterEndIndex(n, nBits) {
signal input in[n]; // x
signal input end; // k
signal output out[n]; // y
component lt[n];
for (var i = 0; i < n; i++) {
lt[i] = LessThan(nBits);
lt[i].in[0] <== i;
lt[i].in[1] <== end;
// y[i] = (i < k) * x[i]
out[i] <== lt[i].out * in[i];
}
}
// Lengths here are in signals, even though the final output array is 1/7 the size of max_substr_len
// TODO: Maybe a better architectural decision to avoid mistakes is to require both values and assert their equality
template ShiftAndPack(in_array_len, max_substr_len, pack_size) {
var max_substr_len_packed = ((max_substr_len - 1) \ pack_size + 1);
component shifter = VarShiftLeft(in_array_len, max_substr_len);
component packer = PackBytes(max_substr_len, max_substr_len_packed, pack_size);
signal input in[in_array_len];
signal input shift;
signal output out[max_substr_len_packed];
for (var i = 0; i < in_array_len; i++) {
shifter.in[i] <== in[i];
}
shifter.shift <== shift;
// Note that this technically doesn't constrain the rest øf the bits after the max_substr_len to be 0/unmatched/unrevealed
// Because of the constraints on signed inputs, it seems this should be OK security wise
// But still, TODO unconstrained assert to double check they are 0
for (var i = 0; i < max_substr_len; i++) {
packer.in[i] <== shifter.out[i];
}
for (var i = 0; i < max_substr_len_packed; i++) {
out[i] <== packer.out[i];
}
}
// Shift the input left by variable size of bytes and pack the shifted bytes into fields under pack_size.
// Its input and output are the same as those of ShiftAndPack.
// However, it assumes the input is the masked bytes and checks that shift is the first index of the non-masked bytes.
template ShiftAndPackMaskedStr(in_array_len, max_substr_len, pack_size) {
var max_substr_len_packed = ((max_substr_len - 1) \ pack_size + 1);
component shifter = VarShiftMaskedStr(in_array_len, max_substr_len);
component packer = PackBytes(max_substr_len, max_substr_len_packed, pack_size);
signal input in[in_array_len];
signal input shift;
signal output out[max_substr_len_packed];
for (var i = 0; i < in_array_len; i++) {
shifter.in[i] <== in[i];
}
shifter.shift <== shift;
for (var i = 0; i < max_substr_len; i++) {
packer.in[i] <== shifter.out[i];
}
for (var i = 0; i < max_substr_len_packed; i++) {
out[i] <== packer.out[i];
}
}

View File

@@ -0,0 +1,149 @@
pragma circom 2.1.5;
include "../../node_modules/circomlib/circuits/bitify.circom";
include "../../node_modules/circomlib/circuits/comparators.circom";
include "../../node_modules/circomlib/circuits/mimcsponge.circom";
include "./fp.circom";
// returns ceil(log2(a+1))
function log2_ceil(a) {
var n = a+1;
var r = 0;
while (n>0) {
r++;
n \= 2;
}
return r;
}
// returns ceil(log2(a+1))
function count_packed(n, chunks) {
return (n - 1) \ chunks + 1;
}
// Lifted from MACI https://github.com/privacy-scaling-explorations/maci/blob/v1/circuits/circom/trees/incrementalQuinTree.circom#L29
// Bits is ceil(log2 choices)
template QuinSelector(choices, bits) {
signal input in[choices];
signal input index;
signal output out;
// Ensure that index < choices
component lessThan = LessThan(bits);
lessThan.in[0] <== index;
lessThan.in[1] <== choices;
lessThan.out === 1;
component calcTotal = CalculateTotal(choices);
component eqs[choices];
// For each item, check whether its index equals the input index.
for (var i = 0; i < choices; i ++) {
eqs[i] = IsEqual();
eqs[i].in[0] <== i;
eqs[i].in[1] <== index;
// eqs[i].out is 1 if the index matches. As such, at most one input to
// calcTotal is not 0.
calcTotal.nums[i] <== eqs[i].out * in[i];
}
// Returns 0 + 0 + ... + item
out <== calcTotal.sum;
}
template CalculateTotal(n) {
signal input nums[n];
signal output sum;
signal sums[n];
sums[0] <== nums[0];
for (var i=1; i < n; i++) {
sums[i] <== sums[i - 1] + nums[i];
}
sum <== sums[n - 1];
}
// Written by us
// n bytes per signal, n = 31 usually
template Packed2Bytes(n){
signal input in; // < 2 ^ (8 * 31)
signal output out[n]; // each out is < 64
// Rangecheck in and out?
// Constrain bits
component nbytes = Num2Bits(8 * n);
nbytes.in <== in;
component bytes[n];
for (var k = 0; k < n; k++){
// Witness gen out
out[k] <-- (in >> (k * 8)) % 256;
// Constrain bits to match
bytes[k] = Num2Bits(8);
bytes[k].in <== out[k];
for (var j = 0; j < 8; j++) {
nbytes.out[k * 8 + j] === bytes[k].out[j];
}
}
}
// n bytes per signal, n = 31 usually (i.e. 31 8-bit values being packed into 248 bits)
// when calling this, you must constrain each 'in' value yourself to be < 256
// TODO: Rangecheck in and out?
template Bytes2Packed(n){
signal input in[n]; // each in value is < 256 (i.e. 2^8)
signal pow2[n+1]; // [k] is 2^k
signal in_prefix_sum[n+1]; // each [k] is in[0] + 2^8 in[1]... 2^{8k-8} in[k-1]. cont.
// [0] is 0. [1] is in[0]. [n+1] is out.
signal output out; // < 2 ^ (8 * 31)
// Rangecheck in and out?
// Witness gen out
in_prefix_sum[0] <-- 0;
for (var k = 0; k < n; k++){
in_prefix_sum[k+1] <-- in_prefix_sum[k] + in[k] * (2 ** (k * 8));
}
out <-- in_prefix_sum[n];
// Constrain out bits
component nbytes = Num2Bits(8 * n);
nbytes.in <== out; // I think this auto-rangechecks out to be < 8*n bits.
component bytes[n];
for (var k = 0; k < n; k++){
bytes[k] = Num2Bits(8);
bytes[k].in <== in[k];
for (var j = 0; j < 8; j++) {
nbytes.out[k * 8 + j] === bytes[k].out[j];
}
}
}
// salt_is_message_id_from, custom_anon_from_hashed_salt = MakeAnonEmailSalt(max_email_from_len, max_message_id_len)(email_from, custom_message_id_from, shifted_message_id)
template MakeAnonEmailSalt(email_len, blinder_len) {
signal input email[email_len];
signal input custom_message_id[blinder_len]; // previous message id, used to source past account
signal input original_message_id[blinder_len]; // previous message id, used to source past account
signal intermediate_is_message_id_from[blinder_len + 1];
signal isEq[blinder_len];
signal output blinder_matches;
signal output anon_salt;
component hasher = MiMCSponge(email_len + blinder_len, 220, 1);
hasher.k <== 123;
for (var i = 0; i < email_len; i++) {
hasher.ins[i] <== email[i];
}
intermediate_is_message_id_from[0] <== 1;
for (var i = 0; i < blinder_len; i++) {
hasher.ins[i + email_len] <== custom_message_id[i];
isEq[i] <== IsEqual()([custom_message_id[i], original_message_id[i]]);
intermediate_is_message_id_from[i + 1] <== isEq[i] * intermediate_is_message_id_from[i];
}
blinder_matches <== intermediate_is_message_id_from[blinder_len];
anon_salt <== hasher.outs[0];
}

View File

@@ -3,84 +3,123 @@ pragma circom 2.1.5;
include "./rsa/rsa.circom";
include "./sha256Bytes.circom";
include "../node_modules/circomlib/circuits/sha256/sha256.circom";
include "../node_modules/circomlib/circuits/poseidon.circom";
include "./helpers/extract.circom";
template PassportVerifier(n, k) {
signal input mrz[93]; // formatted mrz (5 + 88) chars
signal input reveal_bitmap[88];
signal input dataHashes[297];
signal input eContentBytes[104];
signal input mrz[93]; // formatted mrz (5 + 88) chars
signal input reveal_bitmap[88];
signal input dataHashes[297];
signal input eContentBytes[104];
signal input pubkey[k];
signal input signature[k];
signal input pubkey[k];
signal input signature[k];
signal input address;
// compute sha256 of formatted mrz
signal mrzSha[256] <== Sha256Bytes(93)(mrz);
// compute sha256 of formatted mrz
signal mrzSha[256] <== Sha256Bytes(93)(mrz);
// get output of sha256 into bytes to check against dataHashes
component sha256_bytes[32];
for (var i = 0; i < 32; i++) {
sha256_bytes[i] = Bits2Num(8);
for (var j = 0; j < 8; j++) {
sha256_bytes[i].in[7 - j] <== mrzSha[i * 8 + j];
}
}
// get output of sha256 into bytes to check against dataHashes
component sha256_bytes[32];
for (var i = 0; i < 32; i++) {
sha256_bytes[i] = Bits2Num(8);
for (var j = 0; j < 8; j++) {
sha256_bytes[i].in[7 - j] <== mrzSha[i * 8 + j];
}
}
// check that it is in the right position in dataHashes
for(var i = 0; i < 32; i++) {
dataHashes[31 + i] === sha256_bytes[i].out;
}
// check that it is in the right position in dataHashes
for(var i = 0; i < 32; i++) {
dataHashes[31 + i] === sha256_bytes[i].out;
}
// hash dataHashes
signal dataHashesSha[256] <== Sha256Bytes(297)(dataHashes);
// hash dataHashes
signal dataHashesSha[256] <== Sha256Bytes(297)(dataHashes);
// get output of dataHashes sha256 into bytes to check against eContent
component dataHashes_sha256_bytes[32];
for (var i = 0; i < 32; i++) {
dataHashes_sha256_bytes[i] = Bits2Num(8);
for (var j = 0; j < 8; j++) {
dataHashes_sha256_bytes[i].in[7 - j] <== dataHashesSha[i * 8 + j];
}
}
// get output of dataHashes sha256 into bytes to check against eContent
component dataHashes_sha256_bytes[32];
for (var i = 0; i < 32; i++) {
dataHashes_sha256_bytes[i] = Bits2Num(8);
for (var j = 0; j < 8; j++) {
dataHashes_sha256_bytes[i].in[7 - j] <== dataHashesSha[i * 8 + j];
}
}
// check that it is in the right position in eContent
for(var i = 0; i < 32; i++) {
eContentBytes[72 + i] === dataHashes_sha256_bytes[i].out;
}
// check that it is in the right position in eContent
for(var i = 0; i < 32; i++) {
eContentBytes[72 + i] === dataHashes_sha256_bytes[i].out;
}
// hash eContentBytes
signal eContentSha[256] <== Sha256Bytes(104)(eContentBytes);
// hash eContentBytes
signal eContentSha[256] <== Sha256Bytes(104)(eContentBytes);
// get output of eContentBytes sha256 into k chunks of n bits each
var msg_len = (256 + n) \ n;
// get output of eContentBytes sha256 into k chunks of n bits each
var msg_len = (256 + n) \ n;
component eContentHash[msg_len];
for (var i = 0; i < msg_len; i++) {
eContentHash[i] = Bits2Num(n);
}
for (var i = 0; i < 256; i++) {
eContentHash[i \ n].in[i % n] <== eContentSha[255 - i];
}
for (var i = 256; i < n * msg_len; i++) {
eContentHash[i \ n].in[i % n] <== 0;
}
// verify eContentHash signature
component rsa = RSAVerify65537(64, 32);
for (var i = 0; i < msg_len; i++) {
rsa.base_message[i] <== eContentHash[i].out;
}
for (var i = msg_len; i < k; i++) {
rsa.base_message[i] <== 0;
}
rsa.modulus <== pubkey;
rsa.signature <== signature;
component eContentHash[msg_len];
for (var i = 0; i < msg_len; i++) {
eContentHash[i] = Bits2Num(n);
}
for (var i = 0; i < 256; i++) {
eContentHash[i \ n].in[i % n] <== eContentSha[255 - i];
}
for (var i = 256; i < n * msg_len; i++) {
eContentHash[i \ n].in[i % n] <== 0;
}
// verify eContentHash signature
component rsa = RSAVerify65537(64, 32);
for (var i = 0; i < msg_len; i++) {
rsa.base_message[i] <== eContentHash[i].out;
}
for (var i = msg_len; i < k; i++) {
rsa.base_message[i] <== 0;
}
rsa.modulus <== pubkey;
rsa.signature <== signature;
signal output reveal[88];
signal reveal[88];
// reveal reveal_bitmap bits of MRZ
for (var i = 0; i < 88; i++) {
reveal[i] <== mrz[5+i] * reveal_bitmap[i];
}
// reveal reveal_bitmap bits of MRZ
for (var i = 0; i < 88; i++) {
reveal[i] <== mrz[5+i] * reveal_bitmap[i];
}
signal output reveal_packed[3] <== PackBytes(88, 3, 31)(reveal);
signal output nullifier;
nullifier <== (signature[0] << 64) + signature[1];
// Calculate the Poseidon hash of public public key and outputs it
// This can be used to verify the public key is correct in contract without requiring the actual key
// We are converting pub_key (modulus) in to 9 chunks of 242 bits, assuming original n, k are 121 and 17.
// This is because Posiedon circuit only support array of 16 elements.
// Otherwise we would have to output the ceil(256/31) = 9 field elements of the public key
var k2_chunked_size = k >> 1;
if(k % 2 == 1) {
k2_chunked_size += 1;
}
signal pubkey_hash_input[k2_chunked_size];
for(var i = 0; i < k2_chunked_size; i++) {
if(i==k2_chunked_size-1 && k2_chunked_size % 2 == 1) {
pubkey_hash_input[i] <== pubkey[2*i];
} else {
pubkey_hash_input[i] <== pubkey[2*i] + (1<<n) * pubkey[2*i+1];
}
}
signal output pubkey_hash <== Poseidon(k2_chunked_size)(pubkey_hash_input);
}
component main{public [pubkey, signature]} = PassportVerifier(64, 32);
component main { public [ address ] } = PassportVerifier(64, 32);
// Us:
// 1 + 2 + 3 + 1
// pubkey_hash + nullifier + reveal_packed + address
// we take nullifier = signature[0, 1] which it 64 + 64 bits long, so chance of collision is 2^128
// Them:
// 1 + 3 + 1
// pubkey_hash + reveal_twitter_packed + address
// Soit on on garde la bitmap privée et on rend l'output publique => on doit sortir 8*88 bits
// Soit on rend l'input publique et on rend seulement les output révélés publics => on doit sortir 88 bits + 8*reveal_chars bits

View File

@@ -1,6 +1,6 @@
pragma circom 2.1.5;
include "./fp.circom";
include "../helpers/fp.circom";
// Computes base^65537 mod modulus
// Does not necessarily reduce fully mod modulus (the answer could be

View File

@@ -50,16 +50,17 @@ describe('Circuit tests', function () {
reveal_bitmap: reveal_bitmap.map(byte => String(byte)),
dataHashes: concatenatedDataHashes.map(toUnsignedByte).map(byte => String(byte)),
eContentBytes: passportData.eContent.map(toUnsignedByte).map(byte => String(byte)),
signature: splitToWords(
BigInt(bytesToBigDecimal(passportData.encryptedDigest)),
BigInt(64),
BigInt(32)
),
pubkey: splitToWords(
BigInt(passportData.modulus),
BigInt(64),
BigInt(32)
),
signature: splitToWords(
BigInt(bytesToBigDecimal(passportData.encryptedDigest)),
BigInt(64),
BigInt(32)
),
address: "0x9D392187c08fc28A86e1354aD63C70897165b982",
}
})
@@ -76,7 +77,7 @@ describe('Circuit tests', function () {
console.log('proof done');
const revealChars = publicSignals.slice(0, 88).map((byte: string) => String.fromCharCode(parseInt(byte, 10))).join('');
// console.log('reveal chars', revealChars);
console.log('reveal chars', revealChars);
const vKey = JSON.parse(fs.readFileSync("build/verification_key.json"));
const verified = await groth16.verify(
@@ -134,7 +135,7 @@ describe('Circuit tests', function () {
issuing_state: [2, 5],
name: [5, 44],
passport_number: [44, 52],
nationality: [54, 57],
nationality: [54, 56],
date_of_birth: [57, 63],
gender: [64, 65],
expiry_date: [65, 71],
@@ -174,31 +175,49 @@ describe('Circuit tests', function () {
console.log('proof done');
console.log('proof:', proof);
const revealChars = publicSignals.slice(0, 88).map((byte: string) => String.fromCharCode(parseInt(byte, 10)))
console.log('publicSignals', publicSignals)
console.log('revealChars', revealChars)
const firstThreeElements = publicSignals.slice(0, 3);
const bytesCount = [31, 31, 26]; // bytes for each of the first three elements
for(let i = 0; i < revealChars.length; i++) {
if (bitmap[i] == '1') {
assert(revealChars[i] != '\x00', 'Should reveal');
} else {
assert(revealChars[i] == '\x00', 'Should not reveal');
}
}
const reveal: Record<string, string | undefined> = {};
Object.keys(attributeToPosition).forEach((attribute) => {
if (attributeToReveal[attribute]) {
const [start, end] = attributeToPosition[attribute];
const value = revealChars.slice(start, end + 1).join('');
reveal[attribute] = value;
} else {
reveal[attribute] = undefined;
}
const bytesArray = firstThreeElements.flatMap((element, index) => {
const bytes = bytesCount[index];
const elementBigInt = BigInt(element);
const byteMask = BigInt(255); // 0xFF
const bytesOfElement = [...Array(bytes)].map((_, byteIndex) => {
return (elementBigInt >> (BigInt(byteIndex) * BigInt(8))) & byteMask;
});
return bytesOfElement.reverse();
});
const result = bytesArray.reverse().map((byte) => String.fromCharCode(Number(byte)));
console.log('reveal', reveal)
console.log(result);
// console.log('revealChars', revealChars)
// for(let i = 0; i < revealChars.length; i++) {
// if (bitmap[i] == '1') {
// assert(revealChars[i] != '\x00', 'Should reveal');
// } else {
// assert(revealChars[i] == '\x00', 'Should not reveal');
// }
// }
// const reveal: Record<string, string | undefined> = {};
// Object.keys(attributeToPosition).forEach((attribute) => {
// if (attributeToReveal[attribute]) {
// const [start, end] = attributeToPosition[attribute];
// const value = revealChars.slice(start, end + 1).join('');
// reveal[attribute] = value;
// } else {
// reveal[attribute] = undefined;
// }
// });
// console.log('reveal', reveal)
const vKey = JSON.parse(fs.readFileSync("build/verification_key.json"));
const verified = await groth16.verify(