Forbidden Country Check Bypass via Packed Byte Overflow
Description. The CountryNotInList template checks that a given country code is not contained in an input list of forbidden countries. For Aadhaar, this is used with a hard-coded country code of IND (India) as input. The forbidden countries are packed into fewer field elements using PackBytes, and returned as public output.
template CountryNotInList(MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH) {
signal input country[3];
signal input forbidden_countries_list[MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH * 3];
signal equality_result[MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH][4];
signal is_equal[MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH][3];
for (var i = 0; i < MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH; i++) {
equality_result[i][0] <== 1;
for (var j = 1; j < 3 + 1; j++) {
is_equal[i][j - 1] <== IsEqual()([country[j - 1], forbidden_countries_list[i * 3 + j - 1]]);
equality_result[i][j] <== is_equal[i][j - 1] * equality_result[i][j - 1];
}
0 === equality_result[i][3];
}
var chunkLength = computeIntChunkLength(MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH * 3);
signal output forbidden_countries_list_packed[chunkLength] <== PackBytes(MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH * 3)(forbidden_countries_list);
}
The issue here is that elements of forbidden_countries_list are not range‑checked to be bytes, which means PackBytes allows for aliasing. This weakness allows an attacker to easily bypass the intended forbidden country check, which has only the packed output bytes available.
For example, the exploit could be done as follows: Supply a list containing
[73, 78, 68 + 256](i.e., the three IND bytes, but adding 256 to the last byte);- the bytes of some other country, modified by
-1in the lowest byte.
The modification +256 implies that the not-in-list check will succeed, because in unpacked form, none of the country codes equal IND. However, after packing, the country list encodes [IND, <other country>]. This allows the attacker to use an Aadhaar proof even if India is among the forbidden countries.
The same issue is present in forbidden country checks for passports and EU IDs, in the ProveCountryIsNotInList and ProveCountryIsNotInList_ID templates.
Impact. Forbidden country checks can be bypassed for all supported ID document types.
Recommendation. Add explicit byte range constraints, for example using AssertBytes, on every country code element before passing them to PackBytes in CountryNotInList, ProveCountryIsNotInList, and ProveCountryIsNotInList_ID.
Client Response. The issue was fixed in commits 60501d1 and 4914074.