Summary
Verifiers enforce a maximum age for a generated proof, but there is no way to express that limit in the authorization request. The holder side has no way to learn the constraint it is expected to satisfy, so the mismatch can only surface after the fact, as a rejected proof.
The enforced limit
Off-chain, checkQueryRequest rejects a proof whose timestamp public signal is older than acceptedProofGenerationDelay:
// src/proof/verifiers/query.ts:24
const defaultProofGenerationDelayOpts = 24 * 60 * 60 * 1000; // 24 hours
// src/proof/verifiers/query.ts:84-87
const timeDiff = Date.now() - getDateFromUnixTimestamp(Number(outputs.timestamp)).getTime();
if (timeDiff > acceptedProofGenerationDelay) {
throw new Error('generated proof is outdated');
}
It is a VerifyOpts field, so each verifier may set its own value. On-chain the equivalent knob is AuthV3Validator.proofExpirationTimeout, which defaults to 1 hours — 24x stricter than the off-chain default. Both are verifier-local configuration.
The gap
Nothing in the request carries that value. ZeroKnowledgeProofRequest and ZeroKnowledgeProofQuery are closed types with no time-related field:
// src/iden3comm/types/protocol/auth.ts:46-67
export type ZeroKnowledgeProofRequest = {
id: number | string;
circuitId: CircuitId;
optional?: boolean;
query?: ZeroKnowledgeProofQuery;
params?: {
nullifierSessionId?: string | number;
sender?: string;
challenge?: string;
};
};
export type ZeroKnowledgeProofQuery = {
allowedIssuers: string[];
context: string;
credentialSubject?: JsonDocumentObject;
proofType?: ProofType;
skipClaimRevocationCheck?: boolean;
groupId?: number;
type: string;
};
params is a closed set rather than an open JSONObject, so the value cannot be passed through it without a type change either.
expires_time on the message does not cover this. verifyExpiresTime bounds the lifetime of the request message:
// src/iden3comm/handlers/common.ts:364-368
export const verifyExpiresTime = (message: BasicMessage) => {
if (message?.expires_time && message.expires_time < getUnixTimestamp(new Date())) {
throw new Error('Message expired');
}
};
A request that has not expired can still be answered with a proof that is too old to pass checkQueryRequest. The two constraints apply to different objects.
Impact
A holder implementation cannot determine how fresh a proof has to be for a given verifier. The two documented defaults differ by 24x, and either may be overridden per verifier, so there is no safe value to assume. The mismatch is only observable as generated proof is outdated after the response has been submitted, and the message carries neither the proof's timestamp, the accepted window, nor by how much it was exceeded.
Proposal
- Let the request advertise the constraint — a freshness hint on the request, for example
body.scope[].params.maxProofAge (seconds), or an equivalent field on the query.
- Have the verifier derive
acceptedProofGenerationDelay from that field when present, so the advertised value and the enforced value cannot drift apart.
- Include the numbers in the error: proof timestamp, accepted window, and actual age. Even without (1), this alone makes the failure diagnosable instead of opaque.
(3) is useful on its own and independent of the rest. Happy to open a PR if the direction looks right — mainly want to confirm whether a request-level field is the preferred shape before doing so.
Summary
Verifiers enforce a maximum age for a generated proof, but there is no way to express that limit in the authorization request. The holder side has no way to learn the constraint it is expected to satisfy, so the mismatch can only surface after the fact, as a rejected proof.
The enforced limit
Off-chain,
checkQueryRequestrejects a proof whosetimestamppublic signal is older thanacceptedProofGenerationDelay:It is a
VerifyOptsfield, so each verifier may set its own value. On-chain the equivalent knob isAuthV3Validator.proofExpirationTimeout, which defaults to1 hours— 24x stricter than the off-chain default. Both are verifier-local configuration.The gap
Nothing in the request carries that value.
ZeroKnowledgeProofRequestandZeroKnowledgeProofQueryare closed types with no time-related field:paramsis a closed set rather than an openJSONObject, so the value cannot be passed through it without a type change either.expires_timeon the message does not cover this.verifyExpiresTimebounds the lifetime of the request message:A request that has not expired can still be answered with a proof that is too old to pass
checkQueryRequest. The two constraints apply to different objects.Impact
A holder implementation cannot determine how fresh a proof has to be for a given verifier. The two documented defaults differ by 24x, and either may be overridden per verifier, so there is no safe value to assume. The mismatch is only observable as
generated proof is outdatedafter the response has been submitted, and the message carries neither the proof's timestamp, the accepted window, nor by how much it was exceeded.Proposal
body.scope[].params.maxProofAge(seconds), or an equivalent field on the query.acceptedProofGenerationDelayfrom that field when present, so the advertised value and the enforced value cannot drift apart.(3) is useful on its own and independent of the rest. Happy to open a PR if the direction looks right — mainly want to confirm whether a request-level field is the preferred shape before doing so.