diff --git a/.github/workflows/unit-test.yaml b/.github/workflows/unit-test.yaml index a60c995..4d35e33 100644 --- a/.github/workflows/unit-test.yaml +++ b/.github/workflows/unit-test.yaml @@ -33,13 +33,13 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20.16.0 + node-version: 22.15.0 - name: Enable Corepack run: | npm i -g corepack@latest corepack enable - corepack prepare yarn@4.6.0 --activate + corepack prepare pnpm@9.12.0 --activate - name: Install circom 2.1.9 run: | diff --git a/circuits/credential/credential.circom b/circuits/credential/credential.circom index 6fdbc93..c75c693 100644 --- a/circuits/credential/credential.circom +++ b/circuits/credential/credential.circom @@ -9,7 +9,6 @@ include "../utils/iden3/constants.circom"; include "../utils/iden3/numbers.circom"; include "../utils/iden3/strings.circom"; include "../utils/passport/parser/extractors.circom"; -include "../utils/passport/date/dateDiffGreaterThanYear.circom"; include "self/circuits/circuits/utils/crypto/bitify/bytes.circom"; include "self/circuits/circuits/utils/crypto/hasher/hash.circom"; @@ -119,7 +118,6 @@ template DG1FieldParser(hashAlgo, nLevels, smtChanges) { documentDOEExtractor.dg1 <== dg1; documentDOEExtractor.currentDate <== currentDate; signal documentDOE <== documentDOEExtractor.out; - signal documentDOETimestamp <== documentDOEExtractor.timestamp; var keysToUpdate[smtChanges] = [ GetDateOfBirth(), // credentialSubject.dateOfBirth @@ -143,8 +141,8 @@ template DG1FieldParser(hashAlgo, nLevels, smtChanges) { component issuanceDateFitsTo64Bits = CheckMaxBits(64); issuanceDateFitsTo64Bits.inputInteger <== issuanceDate; - // issuanceDate and documentDOETimestamp are in UnixTimestamp format - signal credentialExpiration <== DateDiffGreaterThanYear()(issuanceDate, documentDOETimestamp); + // documentDOEExtractor.timestamp is in UnixTimestamp format + signal credentialExpiration <== documentDOEExtractor.timestamp; /* // For debuging mt update diff --git a/circuits/utils/passport/date/dateDiffGreaterThanYear.circom b/circuits/utils/passport/date/dateDiffGreaterThanYear.circom deleted file mode 100644 index 6d14729..0000000 --- a/circuits/utils/passport/date/dateDiffGreaterThanYear.circom +++ /dev/null @@ -1,36 +0,0 @@ -pragma circom 2.1.9; - -include "circomlib/circuits/comparators.circom"; -include "circomlib/circuits/mux1.circom"; - -template DateDiffGreaterThanYear() { - signal input currentTimestamp; - signal input expirationTimestamp; - signal output expiration; - - // Constant representing one year in seconds - var ONE_YEAR_SECONDS = 31536000; - - // Calculate difference - signal diff; - diff <== expirationTimestamp - currentTimestamp; - - // Ensure difference is non-negative (expirationTimestamp >= currentTimestamp) - component isNonNegative = GreaterEqThan(64); - isNonNegative.in[0] <== diff; - isNonNegative.in[1] <== 0; - isNonNegative.out === 1; - - // Comparator to check if diff is >= ONE_YEAR_SECONDS - component isGreaterEq = GreaterEqThan(64); - isGreaterEq.in[0] <== diff; - isGreaterEq.in[1] <== ONE_YEAR_SECONDS; - - // Multiplexer for conditional logic - component expirationMux = Mux1(); - expirationMux.s <== isGreaterEq.out; - expirationMux.c[0] <== expirationTimestamp; - expirationMux.c[1] <== currentTimestamp + ONE_YEAR_SECONDS; - - expiration <== expirationMux.out; -} \ No newline at end of file diff --git a/package.json b/package.json index 5765e9a..b363075 100644 --- a/package.json +++ b/package.json @@ -48,12 +48,13 @@ "@zk-email/zk-regex-circom": "^2.3.2", "@zk-kit/binary-merkle-root.circom": "^1.0.0", "@zk-kit/circuits": "^1.0.0-beta", + "anon-aadhaar": "git+https://github.com/anon-aadhaar/anon-aadhaar.git", "asn1": "^0.2.6", "asn1.js": "^5.4.1", "asn1js": "^3.0.5", - "circom_tester": "github:remicolin/circom_tester#main", "circom-bigint": "https://github.com/0xbok/circom-bigint", "circom-dl": "https://github.com/distributed-lab/circom-dl", + "circom_tester": "0.0.22", "circomlib": "^2.0.5", "circomlibjs": "^0.1.7", "dotenv": "^16.4.7", @@ -65,8 +66,7 @@ "js-sha512": "^0.9.0", "node-forge": "^1.3.1", "pkijs": "^3.2.5", - "snarkjs": "^0.7.5", - "anon-aadhaar": "git+https://github.com/anon-aadhaar/anon-aadhaar.git", - "self": "git+https://github.com/selfxyz/self.git" + "self": "git+https://github.com/selfxyz/self.git", + "snarkjs": "^0.7.5" } -} \ No newline at end of file +} diff --git a/tests/credential/credential.test.ts b/tests/credential/credential.test.ts index 705c638..9b350ad 100644 --- a/tests/credential/credential.test.ts +++ b/tests/credential/credential.test.ts @@ -50,21 +50,20 @@ testSuite.forEach(({ shaAlg, shaLength }) => { ); const inputs = await generateCircuitInputsCredential(passportData, { currentDate: new Date('2024-01-01'), - issuanceDate: 1742578132000000000n, - expirationDate: 1774114132000000000n + issuanceDate: 1742578132000000000n }); const w = await circuit.calculateWitness(inputs, true); await circuit.checkConstraints(w); // Hash Index assert( - w[1] === 21746881643302549454056364807195696746352973338897115776686822385268131050953n, + w[1] === 19292235858276444384280114997827871641118146185714873530023364159040018023263n, `Hash Index: ${w[1]}` ); // Hash Value assert( - w[2] === 20008859012517445819901041236908823100073815023181291226591238728478957482360n, + w[2] === 20661880459224054680311568334655353588113926319608771155576598304028828385849n, `Hash Value: ${w[2]}` ); diff --git a/tests/utils/circuits/dateDiffGreaterThanYear.circom b/tests/utils/circuits/dateDiffGreaterThanYear.circom deleted file mode 100644 index c09c8e2..0000000 --- a/tests/utils/circuits/dateDiffGreaterThanYear.circom +++ /dev/null @@ -1,5 +0,0 @@ -pragma circom 2.1.9; - -include "../../../circuits/utils/passport/date/dateDiffGreaterThanYear.circom"; - -component main = DateDiffGreaterThanYear(); diff --git a/utils/circuits/generateInputs.ts b/utils/circuits/generateInputs.ts index 850f9d7..10d2b5b 100644 --- a/utils/circuits/generateInputs.ts +++ b/utils/circuits/generateInputs.ts @@ -152,7 +152,6 @@ export function generateCircuitInputsSignature( interface Options { currentDate: Date; issuanceDate: bigint; - expirationDate: bigint; } export async function generateCircuitInputsCredential(passportData: PassportData, opts?: Options) { @@ -217,9 +216,7 @@ export async function generateCircuitInputsCredential(passportData: PassportData const issuerDateTimestamp = opts?.issuanceDate ? opts.issuanceDate / 1000000000n : BigInt(Math.round(+new Date() / 1000)); - const expirationDate = opts?.expirationDate - ? opts.expirationDate / 1000000000n - : issuerDateTimestamp + BigInt(365 * 24 * 60 * 60); + const expirationDate = BigInt(new Date(extractExpirationFromMrz(passportData.mrz)).getTime() / 1000) const updateTemplate = [ '4817156672888655522763064392525239094511187154831557262772815264540847425378', @@ -336,3 +333,70 @@ export function formatInput(input: any) { return [BigInt(input).toString()]; } } + +/** + * Extract expiration date from MRZ (passport / TD3). + * + * Supports TD3 (2 lines × 44 chars) MRZs. If you pass the two-line MRZ + * either as a single string with `\n` between lines, or as a single 88-char + * string (concatenated lines), the function will try to parse it. + * + * Returns ISO date "YYYY-MM-DD" or null if parsing fails. + */ +export function extractExpirationFromMrz(mrz: string): string | null { + if (!mrz || typeof mrz !== "string") return null; + + const cleaned = mrz.replace(/\r/g, "").trim(); + + let lines = cleaned.split("\n").map((l) => l.trim()); + + // If user passed a single-line 88-char MRZ (two lines concatenated), split it + if (lines.length === 1 && lines[0].length === 88) { + lines = [lines[0].slice(0, 44), lines[0].slice(44, 88)]; + } + + if (lines.length >= 2) { + const line1 = lines[0]; + const line2 = lines[1]; + + if (line1.length >= 44 && line2.length >= 44) { + const expiryYYMMDD = line2.slice(21, 27); // 6 chars, YYMMDD + const iso = parseMrzDateYYMMDD(expiryYYMMDD); + return iso; + } + } + return null; +} + +/** + * Parse YYMMDD (from MRZ) into ISO YYYY-MM-DD. + * Heuristic to choose century: + * - prefer 2000+YY if it results in a year <= (currentYear + 20) + * - otherwise fallback to 1900+YY + * + * Returns ISO date string or null on failure. + */ +function parseMrzDateYYMMDD(yyMMdd: string): string | null { + if (!/^\d{6}$/.test(yyMMdd)) return null; + const yy = parseInt(yyMMdd.slice(0, 2), 10); + const mm = parseInt(yyMMdd.slice(2, 4), 10); + const dd = parseInt(yyMMdd.slice(4, 6), 10); + + if (mm < 1 || mm > 12) return null; + if (dd < 1 || dd > 31) return null; + + const now = new Date(); + const currentYear = now.getUTCFullYear(); + const cand2000 = 2000 + yy; + const cand1900 = 1900 + yy; + const year = cand2000 <= currentYear + 20 ? cand2000 : cand1900; + + const d = new Date(Date.UTC(year, mm - 1, dd, 0, 0, 0)); + if (d.getUTCFullYear() !== year || d.getUTCMonth() !== mm - 1 || d.getUTCDate() !== dd) { + return null; + } + const YYYY = d.getUTCFullYear().toString().padStart(4, "0"); + const MM = String(d.getUTCMonth() + 1).padStart(2, "0"); + const DD = String(d.getUTCDate()).padStart(2, "0"); + return `${YYYY}-${MM}-${DD}`; +} diff --git a/yarn.lock b/yarn.lock index 8b7b86b..1ed0647 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2606,4 +2606,4 @@ yn@^2.0.0: yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== \ No newline at end of file