Skip to content

Proof freshness window is enforced by the verifier but cannot be expressed in the request #413

Description

@OBrezhniev

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

  1. 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.
  2. Have the verifier derive acceptedProofGenerationDelay from that field when present, so the advertised value and the enforced value cannot drift apart.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions