@zkproofport-app/sdk 0.5.0-beta.2 → 0.5.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -784,7 +784,9 @@ Verify the proof cryptographically by calling the deployed Solidity verifier con
784
784
 
785
785
  `verifyResponseOnChain` checks the proof against the verifier contract the SDK ships with for that circuit. Which contract judges a proof is never taken from the proof's own response: the SDK carries the deployed verifiers, generated from the circuits' deployment records, and a response whose `verifierAddress` or `chainId` differs from them is refused rather than checked elsewhere. If you configure your own verifier in the SDK's `verifiers` option, that one is used instead.
786
786
 
787
- For OIDC domain proofs the SDK also checks that the proof was made with a signing key Google or Microsoft actually publishes. That key is part of the proof's public inputs, so a proof made with a self-generated key would otherwise pass the contract. The SDK fetches the provider's published keys, caches them as the provider's own cache headers allow, and returns `{ valid: false, error }` when the key is not among them or the keys cannot be fetched. Every verify method does this; there is nothing to call separately.
787
+ For OIDC domain proofs the SDK first checks what the proof says about the sign-in token: it must have been issued to a ZKProofport sign-in client (not to some other website), the proof must have been made within the last 15 minutes (and not more than 5 minutes ahead of your clock), and a proof made by the ZKProofport app must carry the nonce of **your** request — so a proof made for another request cannot be replayed as the answer to yours. For a request you created with `createRelayRequest` on the same SDK instance this happens automatically. If you verify in a different process (or after a restart), pass the request back: `sdk.verifyResponseOnChain(response, provider, { request: { scope, requestId } })`, with the scope you requested and the `requestId` the relay returned; without it an OIDC response is refused. `verifyOnChain` and `verifyResponseOffChain` take the same `request` option.
788
+
789
+ The SDK also checks that the proof was made with a signing key Google or Microsoft actually publishes. That key is part of the proof's public inputs, so a proof made with a self-generated key would otherwise pass the contract. The SDK fetches the provider's published keys, caches them as the provider's own cache headers allow, and returns `{ valid: false, error }` when the key is not among them or the keys cannot be fetched. Every verify method does this; there is nothing to call separately.
788
790
 
789
791
  For Coinbase KYC, Coinbase country and GIWA proofs the SDK checks that the attestation's signer belongs to the published signer set: the proof carries the root of the signer list it was made against, and a proof made against any other list is refused with `{ valid: false, error }`. This too runs in every verify method.
790
792
 
@@ -932,7 +934,7 @@ import {
932
934
  const scope = extractScopeFromPublicInputs(publicInputs, 'coinbase_attestation');
933
935
  const nullifier = extractNullifierFromPublicInputs(publicInputs, 'coinbase_attestation');
934
936
 
935
- // OIDC domain attestation uses a different public input layout (148 fields)
937
+ // OIDC domain attestation uses a different public input layout (214 fields)
936
938
  const oidcScope = extractScopeFromPublicInputs(publicInputs, 'oidc_domain_attestation');
937
939
  const oidcNullifier = extractNullifierFromPublicInputs(publicInputs, 'oidc_domain_attestation');
938
940
 
@@ -1156,7 +1158,7 @@ const signalHash = toBytes32(publicInputs.slice(L.SIGNAL_HASH_START, L.SIGNAL_HA
1156
1158
 
1157
1159
  The country list and the `is_included` flag are public inputs, which means the proof is only as strong as the list it was proved against. Read indices 64–85 back and confirm they are the list and the direction you asked for; a valid proof against a different list is still a valid proof.
1158
1160
 
1159
- ### `oidc_domain_attestation` — 148 entries
1161
+ ### `oidc_domain_attestation` — 214 entries
1160
1162
 
1161
1163
  | Index | Constants | Circuit field | What it is |
1162
1164
  |-------|-----------|---------------|------------|
@@ -1166,6 +1168,12 @@ The country list and the `is_included` flag are public inputs, which means the p
1166
1168
  | 83–114 | `SCOPE_START` / `_END` | `scope` | `bytes32` scope hash |
1167
1169
  | 115–146 | `NULLIFIER_START` / `_END` | `nullifier` | `bytes32` over the user's email and the scope |
1168
1170
  | 147 | `PROVIDER` | `provider` | `0` = Google, `1` = Microsoft. The circuit accepts no other value |
1171
+ | 148–179 | `AUD_HASH_START` / `_END` | `aud_hash` | keccak256 of the ID token's `aud` (the sign-in client it was issued to) |
1172
+ | 180 | `NONCE_MODE` | `nonce_mode` | `1` = the token's nonce is bound to your request, `0` = no nonce (agent device sign-in) |
1173
+ | 181–212 | `NONCE_BINDING_START` / `_END` | `nonce_binding` | In mode `1`, keccak256("zkproofport-oidc-v1" ‖ keccak256(scope) ‖ requestId); all zero in mode `0` |
1174
+ | 213 | `NOW` | `now` | Unix seconds the proof was made at; the circuit proves the token was valid then |
1175
+
1176
+ Proofs from the earlier 148-entry version of this circuit bind no audience, expiry or nonce and are refused by every verify method.
1169
1177
 
1170
1178
  `DOMAIN_START` and `DOMAIN_END` are kept as deprecated aliases of `DOMAIN_STORAGE_START` and `DOMAIN_LEN`. Use the current names — the old pair reads as a range and is not one.
1171
1179
 
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { ProofRequest, ProofResponse, CircuitType, CircuitInputs, ProofportConfig, QRCodeOptions, ChallengeResponse, WalletSigner, RelayProofRequest, RelayProofResult } from './types';
5
5
  import { type OffChainVerifyOptions } from './offChainVerifier';
6
+ import { type OidcRequestContext } from './verifier';
6
7
  import type { SDKEnvironment } from './types';
7
8
  /**
8
9
  * Main SDK class for interacting with the ZKProofport mobile app.
@@ -41,6 +42,15 @@ import type { SDKEnvironment } from './types';
41
42
  export declare class ProofportSDK {
42
43
  private config;
43
44
  private pendingRequests;
45
+ /**
46
+ * requestId -> scope of the relay requests this instance created, so that
47
+ * verifyResponse* can check an OIDC proof's request-bound nonce without the
48
+ * dApp passing the request back in. Oldest entries go first past
49
+ * {@link ProofportSDK.MAX_REMEMBERED_RELAY_REQUESTS}.
50
+ */
51
+ private relayRequests;
52
+ /** Bound on {@link relayRequests}; a dApp that verifies later can pass the request explicitly. */
53
+ static readonly MAX_REMEMBERED_RELAY_REQUESTS = 1000;
44
54
  private signer;
45
55
  private relayUrl;
46
56
  private socket;
@@ -497,10 +507,23 @@ export declare class ProofportSDK {
497
507
  * }
498
508
  * ```
499
509
  */
500
- verifyOnChain(circuit: CircuitType, proof: string, publicInputs: string[], providerOrSigner?: any): Promise<{
510
+ verifyOnChain(circuit: CircuitType, proof: string, publicInputs: string[], providerOrSigner?: any, options?: {
511
+ request?: OidcRequestContext;
512
+ }): Promise<{
501
513
  valid: boolean;
502
514
  error?: string;
503
515
  }>;
516
+ /**
517
+ * The request an `oidc_domain_attestation` response answers: the one the
518
+ * caller passed, else the relay request this instance created under the
519
+ * response's requestId. `{ error }` when there is none, or when the two
520
+ * disagree -- an OIDC proof made with a request-bound nonce is only
521
+ * meaningful against the request it was bound to, so a response this SDK
522
+ * cannot place is refused rather than checked without it.
523
+ */
524
+ private oidcRequestFor;
525
+ /** @internal Remember a relay request's scope for verifyResponse*. */
526
+ private rememberRelayRequest;
504
527
  /**
505
528
  * The verifier for a circuit: the one the dApp configured, otherwise the
506
529
  * pinned deployment on the chain this SDK's environment verifies that
@@ -516,6 +539,9 @@ export declare class ProofportSDK {
516
539
  *
517
540
  * @param response - ProofResponse object from parseResponse()
518
541
  * @param providerOrSigner - ethers v6 Provider or Signer (defaults to base mainnet public RPC)
542
+ * @param options.request - For `oidc_domain_attestation`: the `{ scope, requestId }`
543
+ * the response answers. Not needed for a relay request this SDK instance
544
+ * created; an OIDC response this instance cannot place is refused without it.
519
545
  *
520
546
  * @returns Promise resolving to verification result with valid boolean and optional error message
521
547
  *
@@ -542,7 +568,9 @@ export declare class ProofportSDK {
542
568
  * });
543
569
  * ```
544
570
  */
545
- verifyResponseOnChain(response: ProofResponse, providerOrSigner?: any): Promise<{
571
+ verifyResponseOnChain(response: ProofResponse, providerOrSigner?: any, options?: {
572
+ request?: OidcRequestContext;
573
+ }): Promise<{
546
574
  valid: boolean;
547
575
  error?: string;
548
576
  }>;
package/dist/index.d.ts CHANGED
@@ -32,6 +32,8 @@ export { verifyProofOffChain } from './offChainVerifier';
32
32
  export type { OffChainVerifyResult, OffChainVerifyOptions, } from './offChainVerifier';
33
33
  export { PINNED_VERIFIERS, getPinnedVerifier } from './verifier';
34
34
  export type { PinnedVerifier } from './verifier';
35
+ export { checkOidcPolicy, deriveOidcNonceBinding, oidcAudienceHash, OIDC_ALLOWED_AUDIENCES, OIDC_NONCE_DOMAIN, OIDC_NOW_MAX_AGE_SECONDS, OIDC_NOW_MAX_FUTURE_SECONDS, OIDC_V1_PUBLIC_INPUT_COUNT, OIDC_V2_PUBLIC_INPUT_COUNT, } from './verifier';
36
+ export type { OidcRequestContext, OidcAudience, OidcNonceMode } from './verifier';
35
37
  export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, MDL_KR_PUBLIC_INPUT_LAYOUT, } from './constants';
36
38
  export type { CircuitId, CircuitSupportStatus } from './circuits';
37
39
  export { CIRCUIT_ACTION_BINDING, circuitActionBinding, } from './circuits';
package/dist/index.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { checkProofIssuer, parseProofForOnChain, verifyProofOnChain, getPinnedVerifier, extractScopeFromPublicInputs, extractNullifierFromPublicInputs, extractDomainFromPublicInputs } from '@zkproofport-app/core';
2
- export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, MDL_KR_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, PINNED_VERIFIERS, extractDomainFromPublicInputs, extractNullifierFromPublicInputs, extractScopeFromPublicInputs, getPinnedVerifier } from '@zkproofport-app/core';
2
+ export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, MDL_KR_PUBLIC_INPUT_LAYOUT, OIDC_ALLOWED_AUDIENCES, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, OIDC_NONCE_DOMAIN, OIDC_NOW_MAX_AGE_SECONDS, OIDC_NOW_MAX_FUTURE_SECONDS, OIDC_V1_PUBLIC_INPUT_COUNT, OIDC_V2_PUBLIC_INPUT_COUNT, PINNED_VERIFIERS, checkOidcPolicy, deriveOidcNonceBinding, extractDomainFromPublicInputs, extractNullifierFromPublicInputs, extractScopeFromPublicInputs, getPinnedVerifier, oidcAudienceHash } from '@zkproofport-app/core';
3
3
  import { isCircuitId, ALL_CIRCUIT_IDS, circuitActionBinding, CIRCUIT_VK_PATHS, CIRCUIT_NEEDS_WALLET_SIGNATURE } from '@zkproofport-app/core/circuits';
4
4
  export { ALL_CIRCUIT_IDS, CIRCUIT_ACTION_BINDING, CIRCUIT_IDS, CIRCUIT_SUPPORT_STATUS, CIRCUIT_VK_PATHS, EXPERIMENTAL_CIRCUIT_IDS, PLANNED_CIRCUIT_IDS, SUPPORTED_CIRCUIT_IDS, circuitActionBinding, getCircuitSupportStatus, isCircuitId, isSupportedCircuitId } from '@zkproofport-app/core/circuits';
5
5
  import { validateTypedAction } from '@zkproofport-app/core/typedAction';
@@ -124,8 +124,10 @@ const CIRCUIT_METADATA = {
124
124
  oidc_domain_attestation: {
125
125
  name: 'OIDC Domain',
126
126
  description: 'Prove email domain affiliation via OIDC JWT',
127
- publicInputsCount: 148,
128
- publicInputNames: ['pubkey_modulus_limbs', 'domain', 'scope', 'nullifier', 'provider'],
127
+ // v2: 148 v1 fields, then aud_hash (32), nonce_mode (1), nonce_binding
128
+ // (32), now (1). oidc-domain-attestation/src/main.nr.
129
+ publicInputsCount: 214,
130
+ publicInputNames: ['pubkey_modulus_limbs', 'domain', 'scope', 'nullifier', 'provider', 'aud_hash', 'nonce_mode', 'nonce_binding', 'now'],
129
131
  },
130
132
  // Planned circuit — the identifier is reserved and the circuit exists, but it
131
133
  // is not officially supported yet. See CIRCUIT_SUPPORT_STATUS in ./circuits.
@@ -4006,9 +4008,11 @@ async function fetchVerificationKey(circuit, timeoutMs) {
4006
4008
  * Needs no setup from the caller: the verifier ships with this SDK and is
4007
4009
  * loaded on first use.
4008
4010
  *
4009
- * For `oidc_domain_attestation` it first checks the proof's RSA key against the
4010
- * provider's published JWKS (core's `checkOidcProviderKey`); a key the provider
4011
- * does not publish, or a JWKS that cannot be fetched, is `{ valid: false }`.
4011
+ * For `oidc_domain_attestation` it first checks the token's audience, nonce and
4012
+ * time (core's `checkOidcPolicy`, against `options.request` when given) and the
4013
+ * proof's RSA key against the provider's published JWKS (core's
4014
+ * `checkOidcProviderKey`); any refusal, or a JWKS that cannot be fetched, is
4015
+ * `{ valid: false }`.
4012
4016
  *
4013
4017
  * @param circuit - Canonical circuit identifier
4014
4018
  * @param proof - Proof bytes as hex, WITHOUT public inputs (as the relay sends it)
@@ -4036,7 +4040,7 @@ async function verifyProofOffChain(circuit, proof, publicInputs, options = {}) {
4036
4040
  // their trust anchor as a public input the prover chooses. For OIDC that is
4037
4041
  // the RSA key -- see core's oidcProviderKeys. Checked here so a dApp that
4038
4042
  // verifies off-chain gets the same answer as one that verifies on-chain.
4039
- const issuer = await checkProofIssuer(circuit, publicInputs);
4043
+ const issuer = await checkProofIssuer(circuit, publicInputs, { request: options.request });
4040
4044
  if (!issuer.ok) {
4041
4045
  return { valid: false, error: issuer.error };
4042
4046
  }
@@ -4178,6 +4182,13 @@ class ProofportSDK {
4178
4182
  */
4179
4183
  constructor(config = {}) {
4180
4184
  this.pendingRequests = new Map();
4185
+ /**
4186
+ * requestId -> scope of the relay requests this instance created, so that
4187
+ * verifyResponse* can check an OIDC proof's request-bound nonce without the
4188
+ * dApp passing the request back in. Oldest entries go first past
4189
+ * {@link ProofportSDK.MAX_REMEMBERED_RELAY_REQUESTS}.
4190
+ */
4191
+ this.relayRequests = new Map();
4181
4192
  this.signer = null;
4182
4193
  this.socket = null;
4183
4194
  /**
@@ -4739,9 +4750,49 @@ class ProofportSDK {
4739
4750
  * }
4740
4751
  * ```
4741
4752
  */
4742
- async verifyOnChain(circuit, proof, publicInputs, providerOrSigner) {
4753
+ async verifyOnChain(circuit, proof, publicInputs, providerOrSigner, options = {}) {
4743
4754
  const parsedProof = parseProofForOnChain(proof, publicInputs, publicInputs.length);
4744
- return verifyProofOnChain(circuit, parsedProof, providerOrSigner, this.verifierTarget(circuit));
4755
+ return verifyProofOnChain(circuit, parsedProof, providerOrSigner, this.verifierTarget(circuit), undefined, {
4756
+ request: options.request,
4757
+ });
4758
+ }
4759
+ /**
4760
+ * The request an `oidc_domain_attestation` response answers: the one the
4761
+ * caller passed, else the relay request this instance created under the
4762
+ * response's requestId. `{ error }` when there is none, or when the two
4763
+ * disagree -- an OIDC proof made with a request-bound nonce is only
4764
+ * meaningful against the request it was bound to, so a response this SDK
4765
+ * cannot place is refused rather than checked without it.
4766
+ */
4767
+ oidcRequestFor(response, explicit) {
4768
+ if (explicit !== undefined) {
4769
+ if (explicit === null || typeof explicit !== 'object' || explicit.requestId !== response.requestId) {
4770
+ return {
4771
+ error: `The request passed in (${JSON.stringify(explicit?.requestId)}) is not the one this response answers (${JSON.stringify(response.requestId)}).`,
4772
+ };
4773
+ }
4774
+ return { request: explicit };
4775
+ }
4776
+ const remembered = this.relayRequests.get(response.requestId);
4777
+ if (!remembered) {
4778
+ return {
4779
+ error: `No request ${JSON.stringify(response.requestId)} was created by this SDK instance, so the OIDC proof's ` +
4780
+ 'nonce cannot be checked against it. Pass { request: { scope, requestId } } -- the scope and relay ' +
4781
+ 'requestId of the request you created.',
4782
+ };
4783
+ }
4784
+ return { request: remembered };
4785
+ }
4786
+ /** @internal Remember a relay request's scope for verifyResponse*. */
4787
+ rememberRelayRequest(requestId, scope) {
4788
+ if (typeof requestId !== 'string' || typeof scope !== 'string')
4789
+ return;
4790
+ this.relayRequests.delete(requestId);
4791
+ this.relayRequests.set(requestId, { scope, requestId });
4792
+ while (this.relayRequests.size > ProofportSDK.MAX_REMEMBERED_RELAY_REQUESTS) {
4793
+ const oldest = this.relayRequests.keys().next().value;
4794
+ this.relayRequests.delete(oldest);
4795
+ }
4745
4796
  }
4746
4797
  /**
4747
4798
  * The verifier for a circuit: the one the dApp configured, otherwise the
@@ -4768,6 +4819,9 @@ class ProofportSDK {
4768
4819
  *
4769
4820
  * @param response - ProofResponse object from parseResponse()
4770
4821
  * @param providerOrSigner - ethers v6 Provider or Signer (defaults to base mainnet public RPC)
4822
+ * @param options.request - For `oidc_domain_attestation`: the `{ scope, requestId }`
4823
+ * the response answers. Not needed for a relay request this SDK instance
4824
+ * created; an OIDC response this instance cannot place is refused without it.
4771
4825
  *
4772
4826
  * @returns Promise resolving to verification result with valid boolean and optional error message
4773
4827
  *
@@ -4794,10 +4848,17 @@ class ProofportSDK {
4794
4848
  * });
4795
4849
  * ```
4796
4850
  */
4797
- async verifyResponseOnChain(response, providerOrSigner) {
4851
+ async verifyResponseOnChain(response, providerOrSigner, options = {}) {
4798
4852
  if (response.status !== 'completed' || !response.proof || !response.publicInputs) {
4799
4853
  return { valid: false, error: 'Invalid or incomplete response' };
4800
4854
  }
4855
+ let request;
4856
+ if (response.circuit === 'oidc_domain_attestation') {
4857
+ const found = this.oidcRequestFor(response, options.request);
4858
+ if ('error' in found)
4859
+ return { valid: false, error: found.error };
4860
+ request = found.request;
4861
+ }
4801
4862
  const parsedProof = parseProofForOnChain(response.proof, response.publicInputs, response.publicInputs.length);
4802
4863
  // The response's verifierAddress and chainId are a claim to check, never
4803
4864
  // the contract to call: whoever wrote the response would otherwise pick
@@ -4806,7 +4867,7 @@ class ProofportSDK {
4806
4867
  return verifyProofOnChain(response.circuit, parsedProof, providerOrSigner, this.verifierTarget(response.circuit), {
4807
4868
  verifierAddress: response.verifierAddress,
4808
4869
  chainId: response.chainId,
4809
- });
4870
+ }, { request });
4810
4871
  }
4811
4872
  /**
4812
4873
  * Verifies a proof response off-chain, against the circuit's verification key.
@@ -4836,7 +4897,14 @@ class ProofportSDK {
4836
4897
  if (response.status !== 'completed' || !response.proof || !response.publicInputs) {
4837
4898
  return { valid: false, error: 'Invalid or incomplete response' };
4838
4899
  }
4839
- return verifyProofOffChain(response.circuit, response.proof, response.publicInputs, options);
4900
+ let request;
4901
+ if (response.circuit === 'oidc_domain_attestation') {
4902
+ const found = this.oidcRequestFor(response, options?.request);
4903
+ if ('error' in found)
4904
+ return { valid: false, error: found.error };
4905
+ request = found.request;
4906
+ }
4907
+ return verifyProofOffChain(response.circuit, response.proof, response.publicInputs, { ...options, request });
4840
4908
  }
4841
4909
  // ============ Utility Methods ============
4842
4910
  /**
@@ -5275,7 +5343,9 @@ class ProofportSDK {
5275
5343
  const error = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
5276
5344
  throw new Error(error.error || `Relay request failed: HTTP ${response.status}`);
5277
5345
  }
5278
- return await response.json();
5346
+ const created = await response.json();
5347
+ this.rememberRelayRequest(created.requestId, scope);
5348
+ return created;
5279
5349
  }
5280
5350
  /**
5281
5351
  * Polls the relay for proof result status.
@@ -5558,6 +5628,8 @@ class ProofportSDK {
5558
5628
  return extractDomainFromPublicInputs(publicInputs, circuit);
5559
5629
  }
5560
5630
  }
5631
+ /** Bound on {@link relayRequests}; a dApp that verifies later can pass the request explicitly. */
5632
+ ProofportSDK.MAX_REMEMBERED_RELAY_REQUESTS = 1000;
5561
5633
  /**
5562
5634
  * How each circuit's request is built and checked. Keyed by circuit id and
5563
5635
  * exhaustive over it, so a new circuit is a compile error here instead of