From e93c1a8360c1efa27a83df45e31d06c11e55f7ce Mon Sep 17 00:00:00 2001 From: carsonp6 Date: Wed, 12 Aug 2026 14:03:58 -0700 Subject: [PATCH 1/2] Deprecate AuthSession.encryptedSessionSigningKey (SP-3690) Client-held-key sessions (now the norm for every credential type) never populate this field; it remains only for legacy OAUTH/PASSKEY sessions not yet migrated. Marks it deprecated:true, reframes descriptions across the auth credential/session endpoints and schemas around the client-held-key model, and drops the stale encryptedSessionSigningKey from the EMAIL_OTP refresh example (EMAIL_OTP sessions never return it). Mirrors webdev lightsparkdev/webdev#32460 so the next regen-from-source doesn't overwrite the deprecation. --- mintlify/openapi.yaml | 26 +++++------ openapi.yaml | 26 +++++------ .../auth/AuthCredentialChallengeRequest.yaml | 10 ++--- .../components/schemas/auth/AuthSession.yaml | 43 ++++++++++--------- .../auth/AuthSessionRefreshRequest.yaml | 19 ++++---- .../OauthCredentialVerifyRequestFields.yaml | 6 +-- openapi/paths/auth/auth_credentials.yaml | 4 +- .../auth/auth_credentials_{id}_challenge.yaml | 5 ++- .../auth/auth_credentials_{id}_verify.yaml | 15 ++++--- .../auth/auth_sessions_{id}_refresh.yaml | 8 ++-- 10 files changed, 85 insertions(+), 77 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index d2332c99e..6e64a5275 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -5534,7 +5534,7 @@ paths: Embedded Wallet internal accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the account. Use this endpoint to add another credential (`SMS_OTP`, `OAUTH`, or `PASSKEY`), or to add `EMAIL_OTP` / `SMS_OTP` back after it has been removed. Only one `EMAIL_OTP` and one `SMS_OTP` credential are supported per internal account; multiple distinct `PASSKEY` credentials may be registered. - Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`. + Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (the session signing key the client holds for it) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`. operationId: createAuthCredential tags: - Embedded Wallet Auth @@ -5852,7 +5852,7 @@ paths: For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request. - On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. For `OAUTH` and `PASSKEY` the session signing key is delivered as `encryptedSessionSigningKey` (HPKE-sealed to the supplied `clientPublicKey`); for OTP credentials the client already holds the session signing key (the TEK private key it generated) and that field is omitted from the response. The `expiresAt` timestamp marks when the session expires. + On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. Under the client-held-key model the client already holds the session signing key — the private key it generated before authentication — so no key material is returned and the deprecated `encryptedSessionSigningKey` is omitted. That field is populated only for legacy `OAUTH` and `PASSKEY` sessions not yet migrated, where the key was HPKE-sealed to the supplied `clientPublicKey`. The `expiresAt` timestamp marks when the session expires. operationId: verifyAuthCredential tags: - Embedded Wallet Auth @@ -5983,7 +5983,7 @@ paths: `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `clientPublicKey`. - For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — this seals the resulting session signing key to the client. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. + For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — under the client-held-key model the client retains the matching private key as the resulting session signing key. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. operationId: challengeAuthCredential tags: - Embedded Wallet Auth @@ -6203,7 +6203,7 @@ paths: 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. - 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession` with an `encryptedSessionSigningKey` sealed to that client public key. + 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession`. Under the client-held-key model the client retains the new session signing key, so no key material is returned; the deprecated `encryptedSessionSigningKey` is sealed to the client public key only for legacy sessions not yet migrated. The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead. operationId: refreshAuthSession @@ -6258,7 +6258,6 @@ paths: id: Session:019542f5-b3e7-1d02-0000-000000000011 accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 type: EMAIL_OTP - encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf nickname: example@lightspark.com createdAt: '2026-04-08T15:30:01Z' updatedAt: '2026-04-08T15:35:00Z' @@ -22817,7 +22816,7 @@ components: example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q clientPublicKey: type: string - description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (0x04 prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key must remain on the client. Grid encrypts the session signing key returned in the response to this public key. The key is ephemeral and one-time-use per verification request. + description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (0x04 prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key is retained on the client and serves as the session signing key under the client-held-key model. The key is ephemeral and one-time-use per verification request. example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 OauthCredentialVerifyRequest: title: OAuth Credential Verify Request @@ -22887,7 +22886,7 @@ components: PASSKEY: '#/components/schemas/PasskeyCredentialVerifyRequest' AuthSession: title: Authentication Session - description: An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or `POST /auth/sessions/{id}/refresh` (on mid-session refresh). Only session-issuing responses include `encryptedSessionSigningKey` — it is delivered exactly once at the moment the session is issued and is never returned by the list endpoint. + description: An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or `POST /auth/sessions/{id}/refresh` (on mid-session refresh). Under the client-held-key model the client generates and retains the session signing key, so session-issuing responses carry no key material. The deprecated `encryptedSessionSigningKey` is populated only for legacy sessions not yet migrated to that model and is never returned by the list endpoint. allOf: - $ref: '#/components/schemas/AuthMethod' - type: object @@ -22901,15 +22900,16 @@ components: example: Session:019542f5-b3e7-1d02-0000-000000000003 encryptedSessionSigningKey: type: string + deprecated: true description: |- - HPKE-encrypted session signing key, sealed to the `clientPublicKey` supplied on the verification or refresh request. Encoded as a base58check string: the decoded payload is a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext. The client decrypts this key with its private key and uses it to sign subsequent Embedded Wallet requests until `expiresAt`. + Deprecated. Superseded by the client-held-key model, which is now the norm for every credential type: the client generates a session keypair before authentication and retains the private key, so the server has no key to deliver and this field is omitted from session-issuing responses. - Returned only by session-issuing responses for `OAUTH` and `PASSKEY` credentials. `EMAIL_OTP` and `SMS_OTP` sessions omit this field — the client generates a TEK keypair before verification and retains the private key throughout, so the server has nothing to deliver. Always omitted from list responses (`GET /auth/sessions`) since Grid does not retain the plaintext key after the client has decrypted it. + Historically populated only for `OAUTH` and `PASSKEY` sessions, where the session signing key was HPKE-sealed to the supplied `clientPublicKey` and returned here as a base58check string (a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext) for the client to decrypt with its private key. Migrated credential types omit it entirely; it remains present only for legacy sessions not yet moved to the client-held-key model, and will be removed in a future version. Always omitted from list responses (`GET /auth/sessions`). example: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf expiresAt: type: string format: date-time - description: Timestamp after which the session is no longer valid and the `encryptedSessionSigningKey` must not be used to sign further requests. + description: Timestamp after which the session is no longer valid and the session signing key must not be used to sign further requests. example: '2026-04-09T15:30:01Z' AuthCredentialChallengeRequest: title: Auth Credential Challenge Request @@ -22921,7 +22921,7 @@ components: pattern: ^04[0-9a-fA-F]{128}$ minLength: 130 maxLength: 130 - description: Required for `PASSKEY` credentials. Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key must remain on the client. Grid bakes this key into the session-creation payload that the returned `challenge` is computed from, so the resulting session signing key is sealed to the client. Ignored for `EMAIL_OTP` and `SMS_OTP`. + description: Required for `PASSKEY` credentials. Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key is retained on the client and serves as the session signing key under the client-held-key model. Grid bakes this public key into the session-creation payload that the returned `challenge` is computed from. Ignored for `EMAIL_OTP` and `SMS_OTP`. example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 PasskeyAuthChallenge: title: Passkey Auth Challenge @@ -22976,7 +22976,7 @@ components: $ref: '#/components/schemas/AuthSession' AuthSessionRefreshRequest: title: Auth Session Refresh Request - description: Request body for refreshing an active authentication session. The `clientPublicKey` is required on both steps of the signed-retry flow. On the initial call, Grid binds this key into the session-creation payload returned as `payloadToSign`; on the signed retry, the client echoes the same key back and Grid uses it to encrypt the newly issued session signing key. + description: Request body for refreshing an active authentication session. The `clientPublicKey` is required on both steps of the signed-retry flow; its matching private key is retained on the client and becomes the new session signing key under the client-held-key model. On the initial call, Grid binds this key into the session-creation payload returned as `payloadToSign`; the client echoes the same key back on the signed retry. type: object required: - clientPublicKey @@ -22984,7 +22984,7 @@ components: clientPublicKey: type: string pattern: ^04[0-9a-fA-F]{128}$ - description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key must remain on the client. Grid binds this key into the session-creation payload on the initial call and seals the returned `encryptedSessionSigningKey` to it on the signed retry. + description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key is retained on the client and serves as the new session signing key under the client-held-key model. Grid binds this public key into the session-creation payload on the initial call; for legacy sessions not yet migrated it additionally seals the deprecated `encryptedSessionSigningKey` to it on the signed retry. example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 DelegatedKeyStatus: type: string diff --git a/openapi.yaml b/openapi.yaml index d2332c99e..6e64a5275 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5534,7 +5534,7 @@ paths: Embedded Wallet internal accounts are initialized with an `EMAIL_OTP` credential tied to the customer email on the account. Use this endpoint to add another credential (`SMS_OTP`, `OAUTH`, or `PASSKEY`), or to add `EMAIL_OTP` / `SMS_OTP` back after it has been removed. Only one `EMAIL_OTP` and one `SMS_OTP` credential are supported per internal account; multiple distinct `PASSKEY` credentials may be registered. - Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (decrypted client-side from its `encryptedSessionSigningKey`) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`. + Adding a credential requires a signature from an existing verified credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified credential (the session signing key the client holds for it) to build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns `201` with the created `AuthMethod`. For OTP credentials, the one-time password is triggered on the signed retry, and the credential must then be activated via `POST /auth/credentials/{id}/verify`. operationId: createAuthCredential tags: - Embedded Wallet Auth @@ -5852,7 +5852,7 @@ paths: For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request. - On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. For `OAUTH` and `PASSKEY` the session signing key is delivered as `encryptedSessionSigningKey` (HPKE-sealed to the supplied `clientPublicKey`); for OTP credentials the client already holds the session signing key (the TEK private key it generated) and that field is omitted from the response. The `expiresAt` timestamp marks when the session expires. + On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. Under the client-held-key model the client already holds the session signing key — the private key it generated before authentication — so no key material is returned and the deprecated `encryptedSessionSigningKey` is omitted. That field is populated only for legacy `OAUTH` and `PASSKEY` sessions not yet migrated, where the key was HPKE-sealed to the supplied `clientPublicKey`. The `expiresAt` timestamp marks when the session expires. operationId: verifyAuthCredential tags: - Embedded Wallet Auth @@ -5983,7 +5983,7 @@ paths: `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `clientPublicKey`. - For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — this seals the resulting session signing key to the client. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. + For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — under the client-held-key model the client retains the matching private key as the resulting session signing key. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. operationId: challengeAuthCredential tags: - Embedded Wallet Auth @@ -6203,7 +6203,7 @@ paths: 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. - 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession` with an `encryptedSessionSigningKey` sealed to that client public key. + 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession`. Under the client-held-key model the client retains the new session signing key, so no key material is returned; the deprecated `encryptedSessionSigningKey` is sealed to the client public key only for legacy sessions not yet migrated. The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead. operationId: refreshAuthSession @@ -6258,7 +6258,6 @@ paths: id: Session:019542f5-b3e7-1d02-0000-000000000011 accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 type: EMAIL_OTP - encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf nickname: example@lightspark.com createdAt: '2026-04-08T15:30:01Z' updatedAt: '2026-04-08T15:35:00Z' @@ -22817,7 +22816,7 @@ components: example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q clientPublicKey: type: string - description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (0x04 prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key must remain on the client. Grid encrypts the session signing key returned in the response to this public key. The key is ephemeral and one-time-use per verification request. + description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (0x04 prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key is retained on the client and serves as the session signing key under the client-held-key model. The key is ephemeral and one-time-use per verification request. example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 OauthCredentialVerifyRequest: title: OAuth Credential Verify Request @@ -22887,7 +22886,7 @@ components: PASSKEY: '#/components/schemas/PasskeyCredentialVerifyRequest' AuthSession: title: Authentication Session - description: An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or `POST /auth/sessions/{id}/refresh` (on mid-session refresh). Only session-issuing responses include `encryptedSessionSigningKey` — it is delivered exactly once at the moment the session is issued and is never returned by the list endpoint. + description: An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or `POST /auth/sessions/{id}/refresh` (on mid-session refresh). Under the client-held-key model the client generates and retains the session signing key, so session-issuing responses carry no key material. The deprecated `encryptedSessionSigningKey` is populated only for legacy sessions not yet migrated to that model and is never returned by the list endpoint. allOf: - $ref: '#/components/schemas/AuthMethod' - type: object @@ -22901,15 +22900,16 @@ components: example: Session:019542f5-b3e7-1d02-0000-000000000003 encryptedSessionSigningKey: type: string + deprecated: true description: |- - HPKE-encrypted session signing key, sealed to the `clientPublicKey` supplied on the verification or refresh request. Encoded as a base58check string: the decoded payload is a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext. The client decrypts this key with its private key and uses it to sign subsequent Embedded Wallet requests until `expiresAt`. + Deprecated. Superseded by the client-held-key model, which is now the norm for every credential type: the client generates a session keypair before authentication and retains the private key, so the server has no key to deliver and this field is omitted from session-issuing responses. - Returned only by session-issuing responses for `OAUTH` and `PASSKEY` credentials. `EMAIL_OTP` and `SMS_OTP` sessions omit this field — the client generates a TEK keypair before verification and retains the private key throughout, so the server has nothing to deliver. Always omitted from list responses (`GET /auth/sessions`) since Grid does not retain the plaintext key after the client has decrypted it. + Historically populated only for `OAUTH` and `PASSKEY` sessions, where the session signing key was HPKE-sealed to the supplied `clientPublicKey` and returned here as a base58check string (a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext) for the client to decrypt with its private key. Migrated credential types omit it entirely; it remains present only for legacy sessions not yet moved to the client-held-key model, and will be removed in a future version. Always omitted from list responses (`GET /auth/sessions`). example: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf expiresAt: type: string format: date-time - description: Timestamp after which the session is no longer valid and the `encryptedSessionSigningKey` must not be used to sign further requests. + description: Timestamp after which the session is no longer valid and the session signing key must not be used to sign further requests. example: '2026-04-09T15:30:01Z' AuthCredentialChallengeRequest: title: Auth Credential Challenge Request @@ -22921,7 +22921,7 @@ components: pattern: ^04[0-9a-fA-F]{128}$ minLength: 130 maxLength: 130 - description: Required for `PASSKEY` credentials. Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key must remain on the client. Grid bakes this key into the session-creation payload that the returned `challenge` is computed from, so the resulting session signing key is sealed to the client. Ignored for `EMAIL_OTP` and `SMS_OTP`. + description: Required for `PASSKEY` credentials. Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key is retained on the client and serves as the session signing key under the client-held-key model. Grid bakes this public key into the session-creation payload that the returned `challenge` is computed from. Ignored for `EMAIL_OTP` and `SMS_OTP`. example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 PasskeyAuthChallenge: title: Passkey Auth Challenge @@ -22976,7 +22976,7 @@ components: $ref: '#/components/schemas/AuthSession' AuthSessionRefreshRequest: title: Auth Session Refresh Request - description: Request body for refreshing an active authentication session. The `clientPublicKey` is required on both steps of the signed-retry flow. On the initial call, Grid binds this key into the session-creation payload returned as `payloadToSign`; on the signed retry, the client echoes the same key back and Grid uses it to encrypt the newly issued session signing key. + description: Request body for refreshing an active authentication session. The `clientPublicKey` is required on both steps of the signed-retry flow; its matching private key is retained on the client and becomes the new session signing key under the client-held-key model. On the initial call, Grid binds this key into the session-creation payload returned as `payloadToSign`; the client echoes the same key back on the signed retry. type: object required: - clientPublicKey @@ -22984,7 +22984,7 @@ components: clientPublicKey: type: string pattern: ^04[0-9a-fA-F]{128}$ - description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key must remain on the client. Grid binds this key into the session-creation payload on the initial call and seals the returned `encryptedSessionSigningKey` to it on the signed retry. + description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key is retained on the client and serves as the new session signing key under the client-held-key model. Grid binds this public key into the session-creation payload on the initial call; for legacy sessions not yet migrated it additionally seals the deprecated `encryptedSessionSigningKey` to it on the signed retry. example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 DelegatedKeyStatus: type: string diff --git a/openapi/components/schemas/auth/AuthCredentialChallengeRequest.yaml b/openapi/components/schemas/auth/AuthCredentialChallengeRequest.yaml index 254c798bd..daab61be9 100644 --- a/openapi/components/schemas/auth/AuthCredentialChallengeRequest.yaml +++ b/openapi/components/schemas/auth/AuthCredentialChallengeRequest.yaml @@ -18,9 +18,9 @@ properties: Required for `PASSKEY` credentials. Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters - total). The matching private key must remain on the client. Grid - bakes this key into the session-creation payload that the - returned `challenge` is computed from, so the resulting session - signing key is sealed to the client. Ignored for `EMAIL_OTP` and - `SMS_OTP`. + total). The matching private key is retained on the client and + serves as the session signing key under the client-held-key + model. Grid bakes this public key into the session-creation + payload that the returned `challenge` is computed from. Ignored + for `EMAIL_OTP` and `SMS_OTP`. example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 diff --git a/openapi/components/schemas/auth/AuthSession.yaml b/openapi/components/schemas/auth/AuthSession.yaml index 58c43698a..dceb6d183 100644 --- a/openapi/components/schemas/auth/AuthSession.yaml +++ b/openapi/components/schemas/auth/AuthSession.yaml @@ -3,10 +3,12 @@ description: >- An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or - `POST /auth/sessions/{id}/refresh` (on mid-session refresh). - Only session-issuing responses include `encryptedSessionSigningKey` — - it is delivered exactly once at the moment the session is issued and is - never returned by the list endpoint. + `POST /auth/sessions/{id}/refresh` (on mid-session refresh). Under the + client-held-key model the client generates and retains the session + signing key, so session-issuing responses carry no key material. The + deprecated `encryptedSessionSigningKey` is populated only for legacy + sessions not yet migrated to that model and is never returned by the + list endpoint. allOf: - $ref: ./AuthMethod.yaml - type: object @@ -25,30 +27,29 @@ allOf: example: Session:019542f5-b3e7-1d02-0000-000000000003 encryptedSessionSigningKey: type: string + deprecated: true description: >- - HPKE-encrypted session signing key, sealed to the - `clientPublicKey` supplied on the verification or refresh request. - Encoded as - a base58check string: the decoded payload is a 33-byte - compressed P-256 encapsulated public key followed by - AES-256-GCM ciphertext. The client decrypts this key with - its private key and uses it to sign subsequent Embedded - Wallet requests until `expiresAt`. + Deprecated. Superseded by the client-held-key model, which is + now the norm for every credential type: the client generates a + session keypair before authentication and retains the private + key, so the server has no key to deliver and this field is + omitted from session-issuing responses. - Returned only by session-issuing responses for `OAUTH` and - `PASSKEY` credentials. `EMAIL_OTP` and `SMS_OTP` sessions omit this - field — the client generates a TEK keypair before verification and - retains the private key throughout, so the server has nothing - to deliver. Always omitted from list responses - (`GET /auth/sessions`) since Grid does not retain the - plaintext key after the client has decrypted it. + Historically populated only for `OAUTH` and `PASSKEY` sessions, + where the session signing key was HPKE-sealed to the supplied + `clientPublicKey` and returned here as a base58check string (a + 33-byte compressed P-256 encapsulated public key followed by + AES-256-GCM ciphertext) for the client to decrypt with its + private key. Migrated credential types omit it entirely; it + remains present only for legacy sessions not yet moved to the + client-held-key model, and will be removed in a future version. + Always omitted from list responses (`GET /auth/sessions`). example: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf expiresAt: type: string format: date-time description: >- Timestamp after which the session is no longer valid and the - `encryptedSessionSigningKey` must not be used to sign further - requests. + session signing key must not be used to sign further requests. example: '2026-04-09T15:30:01Z' diff --git a/openapi/components/schemas/auth/AuthSessionRefreshRequest.yaml b/openapi/components/schemas/auth/AuthSessionRefreshRequest.yaml index bd170bb59..9672c8997 100644 --- a/openapi/components/schemas/auth/AuthSessionRefreshRequest.yaml +++ b/openapi/components/schemas/auth/AuthSessionRefreshRequest.yaml @@ -1,10 +1,11 @@ title: Auth Session Refresh Request description: >- Request body for refreshing an active authentication session. The - `clientPublicKey` is required on both steps of the signed-retry flow. On the - initial call, Grid binds this key into the session-creation payload - returned as `payloadToSign`; on the signed retry, the client echoes the same - key back and Grid uses it to encrypt the newly issued session signing key. + `clientPublicKey` is required on both steps of the signed-retry flow; its + matching private key is retained on the client and becomes the new session + signing key under the client-held-key model. On the initial call, Grid + binds this key into the session-creation payload returned as + `payloadToSign`; the client echoes the same key back on the signed retry. type: object required: - clientPublicKey @@ -15,8 +16,10 @@ properties: description: >- Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; - 130 hex characters total). The matching private key must remain on the - client. Grid binds this key into the session-creation payload on the - initial call and seals the returned `encryptedSessionSigningKey` to it on - the signed retry. + 130 hex characters total). The matching private key is retained on the + client and serves as the new session signing key under the + client-held-key model. Grid binds this public key into the + session-creation payload on the initial call; for legacy sessions not + yet migrated it additionally seals the deprecated + `encryptedSessionSigningKey` to it on the signed retry. example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 diff --git a/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml b/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml index b1145e8c7..c47acd650 100644 --- a/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml +++ b/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml @@ -28,7 +28,7 @@ properties: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (0x04 prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key - must remain on the client. Grid encrypts the session signing key - returned in the response to this public key. The key is ephemeral - and one-time-use per verification request. + is retained on the client and serves as the session signing key + under the client-held-key model. The key is ephemeral and + one-time-use per verification request. example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 diff --git a/openapi/paths/auth/auth_credentials.yaml b/openapi/paths/auth/auth_credentials.yaml index 7a16ed121..f0232cbe2 100644 --- a/openapi/paths/auth/auth_credentials.yaml +++ b/openapi/paths/auth/auth_credentials.yaml @@ -16,8 +16,8 @@ post: credential on the same account. Call this endpoint with the new credential's details to receive `202` with `payloadToSign` and `requestId`. Use the session API keypair of an existing verified - credential (decrypted client-side from its - `encryptedSessionSigningKey`) to build an API-key stamp over + credential (the session signing key the client holds for it) to + build an API-key stamp over `payloadToSign`, then retry the same request with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The signed retry returns diff --git a/openapi/paths/auth/auth_credentials_{id}_challenge.yaml b/openapi/paths/auth/auth_credentials_{id}_challenge.yaml index 184d9cba9..64bc1f049 100644 --- a/openapi/paths/auth/auth_credentials_{id}_challenge.yaml +++ b/openapi/paths/auth/auth_credentials_{id}_challenge.yaml @@ -22,8 +22,9 @@ post: For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation - payload the returned challenge is computed from — this seals the - resulting session signing key to the client. + payload the returned challenge is computed from — under the + client-held-key model the client retains the matching private key + as the resulting session signing key. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded diff --git a/openapi/paths/auth/auth_credentials_{id}_verify.yaml b/openapi/paths/auth/auth_credentials_{id}_verify.yaml index af9d90931..71ba73ce8 100644 --- a/openapi/paths/auth/auth_credentials_{id}_verify.yaml +++ b/openapi/paths/auth/auth_credentials_{id}_verify.yaml @@ -43,13 +43,14 @@ post: On success for `OAUTH` and `PASSKEY`, and on the signed retry for - OTP credentials, the response contains an `AuthSession`. For `OAUTH` - and `PASSKEY` the session signing key is delivered as - `encryptedSessionSigningKey` (HPKE-sealed to the supplied - `clientPublicKey`); for OTP credentials the client already holds the - session signing key (the TEK private key it generated) and that - field is omitted from the response. The `expiresAt` timestamp - marks when the session expires. + OTP credentials, the response contains an `AuthSession`. Under the + client-held-key model the client already holds the session signing + key — the private key it generated before authentication — so no + key material is returned and the deprecated + `encryptedSessionSigningKey` is omitted. That field is populated + only for legacy `OAUTH` and `PASSKEY` sessions not yet migrated, + where the key was HPKE-sealed to the supplied `clientPublicKey`. + The `expiresAt` timestamp marks when the session expires. operationId: verifyAuthCredential tags: - Embedded Wallet Auth diff --git a/openapi/paths/auth/auth_sessions_{id}_refresh.yaml b/openapi/paths/auth/auth_sessions_{id}_refresh.yaml index 7248f7337..0f57a70f3 100644 --- a/openapi/paths/auth/auth_sessions_{id}_refresh.yaml +++ b/openapi/paths/auth/auth_sessions_{id}_refresh.yaml @@ -15,8 +15,11 @@ post: 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in - the request body. On success, Grid returns a new `AuthSession` with an - `encryptedSessionSigningKey` sealed to that client public key. + the request body. On success, Grid returns a new `AuthSession`. Under the + client-held-key model the client retains the new session signing key, so + no key material is returned; the deprecated `encryptedSessionSigningKey` + is sealed to the client public key only for legacy sessions not yet + migrated. The original session must still be active on both steps so it can authorize @@ -81,7 +84,6 @@ post: id: Session:019542f5-b3e7-1d02-0000-000000000011 accountId: InternalAccount:019542f5-b3e7-1d02-0000-000000000002 type: EMAIL_OTP - encryptedSessionSigningKey: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf nickname: example@lightspark.com createdAt: '2026-04-08T15:30:01Z' updatedAt: '2026-04-08T15:35:00Z' From d83a651fe782cd7b652c7314c7e8053cb9b6724d Mon Sep 17 00:00:00 2001 From: carsonp6 Date: Thu, 13 Aug 2026 10:08:58 -0700 Subject: [PATCH 2/2] feat(auth): accept compressed clientPublicKey to select the client-held-key flow (SP-3690) The clientPublicKey encoding now selects the flow: compressed SEC1 (02/03 prefix, 66 hex) opts into the client-held session key model documented for EMAIL_OTP/SMS_OTP, extended here to OAUTH and PASSKEY; uncompressed SEC1 (04 prefix, 130 hex) keeps the deprecated legacy HPKE-sealed flow. Relaxes the challenge/refresh clientPublicKey patterns to accept both, switches representative examples to the compressed form, and documents the new "Client-held session key" flow in the client-keys.mdx guide. Also teaches the offramp scripts' gen-keypair helper a --compressed flag for generating client-held session keys. Consolidates lightsparkdev/grid-api#811 into this PR. Co-Authored-By: Claude Opus 4.8 --- mintlify/openapi.yaml | 42 +++++++++---------- .../snippets/global-accounts/client-keys.mdx | 22 ++++++++-- openapi.yaml | 42 +++++++++---------- .../auth/AuthCredentialChallengeRequest.yaml | 25 ++++++----- .../components/schemas/auth/AuthSession.yaml | 41 +++++++++--------- .../auth/AuthSessionRefreshRequest.yaml | 34 ++++++++------- .../OauthCredentialVerifyRequestFields.yaml | 17 ++++---- .../auth/auth_credentials_{id}_challenge.yaml | 9 ++-- .../auth/auth_credentials_{id}_verify.yaml | 20 +++++---- .../auth/auth_sessions_{id}_refresh.yaml | 24 ++++++----- scripts/embedded-wallet-sign.js | 39 ++++++++++------- 11 files changed, 178 insertions(+), 137 deletions(-) diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 6e64a5275..a578752bd 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -5852,7 +5852,7 @@ paths: For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request. - On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. Under the client-held-key model the client already holds the session signing key — the private key it generated before authentication — so no key material is returned and the deprecated `encryptedSessionSigningKey` is omitted. That field is populated only for legacy `OAUTH` and `PASSKEY` sessions not yet migrated, where the key was HPKE-sealed to the supplied `clientPublicKey`. The `expiresAt` timestamp marks when the session expires. + On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. Sending a compressed `clientPublicKey` selects the recommended client-held-key model: the client already holds the session signing key — the private key it generated before authentication — so no key material is returned and the deprecated `encryptedSessionSigningKey` is omitted. Sending an uncompressed `clientPublicKey` selects the deprecated legacy flow, where the session signing key is HPKE-sealed to that key and returned as `encryptedSessionSigningKey` for the client to decrypt. The `expiresAt` timestamp marks when the session expires. operationId: verifyAuthCredential tags: - Embedded Wallet Auth @@ -5907,7 +5907,7 @@ paths: value: type: OAUTH oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 passkey: summary: Verify a passkey credential value: @@ -5983,7 +5983,7 @@ paths: `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `clientPublicKey`. - For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — under the client-held-key model the client retains the matching private key as the resulting session signing key. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. + For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — send a compressed key for the recommended client-held-key model, where the client retains the matching private key as the resulting session signing key, or an uncompressed key for the deprecated legacy flow. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. operationId: challengeAuthCredential tags: - Embedded Wallet Auth @@ -6007,7 +6007,7 @@ paths: passkey: summary: Re-challenge a passkey credential value: - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 emailOtp: summary: Re-challenge an email-OTP credential (empty body) value: {} @@ -6201,9 +6201,9 @@ paths: description: | Refresh an active Embedded Wallet auth session and create a new session signing key. Session refresh is a two-step signed-retry flow: - 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. + 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "02..." }` and no signature headers. Send a freshly generated client public key and retain its private key. Grid binds the supplied `clientPublicKey` into the session-refresh payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. - 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession`. Under the client-held-key model the client retains the new session signing key, so no key material is returned; the deprecated `encryptedSessionSigningKey` is sealed to the client public key only for legacy sessions not yet migrated. + 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession`. Sending a compressed `clientPublicKey` selects the recommended client-held-key model, where the client retains the new session signing key and no key material is returned; sending an uncompressed key selects the deprecated legacy flow, where the new key is sealed to it and returned as `encryptedSessionSigningKey`. The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead. operationId: refreshAuthSession @@ -6243,7 +6243,7 @@ paths: refresh: summary: Refresh an active session value: - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 responses: '201': description: New authentication session created successfully. @@ -6272,7 +6272,7 @@ paths: challenge: summary: Session refresh challenge value: - payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}' + payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}' requestId: Request:019542f5-b3e7-1d02-0000-000000000010 expiresAt: '2026-04-08T15:35:00Z' '400': @@ -22816,8 +22816,8 @@ components: example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q clientPublicKey: type: string - description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (0x04 prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key is retained on the client and serves as the session signing key under the client-held-key model. The key is ephemeral and one-time-use per verification request. - example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + description: Client-generated, ephemeral P-256 public key; the matching private key is retained on the client and is one-time-use per verification request. Send a compressed SEC1 key (`02`/`03` prefix followed by the 32-byte X coordinate; 66 hex characters) for the recommended client-held-key model, where that private key becomes the session signing key. Send an uncompressed SEC1 key (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) for the deprecated legacy flow, where Grid seals the session signing key to it instead. + example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 OauthCredentialVerifyRequest: title: OAuth Credential Verify Request allOf: @@ -22886,7 +22886,7 @@ components: PASSKEY: '#/components/schemas/PasskeyCredentialVerifyRequest' AuthSession: title: Authentication Session - description: An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or `POST /auth/sessions/{id}/refresh` (on mid-session refresh). Under the client-held-key model the client generates and retains the session signing key, so session-issuing responses carry no key material. The deprecated `encryptedSessionSigningKey` is populated only for legacy sessions not yet migrated to that model and is never returned by the list endpoint. + description: 'An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or `POST /auth/sessions/{id}/refresh` (on mid-session refresh). The `clientPublicKey` encoding on the issuing request selects the flow: a compressed key gets the client-held-key model, where the client generates and retains the session signing key and session-issuing responses carry no key material; an uncompressed key gets the deprecated legacy flow, where Grid seals the session signing key to that public key and returns it as `encryptedSessionSigningKey`. Never returned by the list endpoint.' allOf: - $ref: '#/components/schemas/AuthMethod' - type: object @@ -22902,9 +22902,9 @@ components: type: string deprecated: true description: |- - Deprecated. Superseded by the client-held-key model, which is now the norm for every credential type: the client generates a session keypair before authentication and retains the private key, so the server has no key to deliver and this field is omitted from session-issuing responses. + Deprecated; present only for the legacy flow, selected by sending an uncompressed `clientPublicKey` on the verification or refresh request. Grid seals the session signing key to that public key and returns it here as a base58check string (a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext) for the client to decrypt with its private key. - Historically populated only for `OAUTH` and `PASSKEY` sessions, where the session signing key was HPKE-sealed to the supplied `clientPublicKey` and returned here as a base58check string (a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext) for the client to decrypt with its private key. Migrated credential types omit it entirely; it remains present only for legacy sessions not yet moved to the client-held-key model, and will be removed in a future version. Always omitted from list responses (`GET /auth/sessions`). + The recommended client-held-key flow sends a compressed `clientPublicKey` instead: the client generates and retains the session signing key itself, so this field is omitted — the same way `EMAIL_OTP` and `SMS_OTP` sessions have always worked. See the "Client keys & signing" guide. Always omitted from list responses (`GET /auth/sessions`). example: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf expiresAt: type: string @@ -22918,11 +22918,11 @@ components: properties: clientPublicKey: type: string - pattern: ^04[0-9a-fA-F]{128}$ - minLength: 130 + pattern: ^(0[23][0-9a-fA-F]{64}|04[0-9a-fA-F]{128})$ + minLength: 66 maxLength: 130 - description: Required for `PASSKEY` credentials. Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key is retained on the client and serves as the session signing key under the client-held-key model. Grid bakes this public key into the session-creation payload that the returned `challenge` is computed from. Ignored for `EMAIL_OTP` and `SMS_OTP`. - example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + description: Required for `PASSKEY` credentials; the matching private key is retained on the client. Send a compressed SEC1 key (`02`/`03` prefix followed by the 32-byte X coordinate; 66 hex characters) for the recommended client-held-key model, where that private key becomes the session signing key. Send an uncompressed SEC1 key (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) for the deprecated legacy flow, where Grid seals the session signing key to it instead. Grid bakes this public key into the session-creation payload that the returned `challenge` is computed from. Ignored for `EMAIL_OTP` and `SMS_OTP`. + example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 PasskeyAuthChallenge: title: Passkey Auth Challenge description: Extended `AuthMethod` shape returned for `PASSKEY` credentials from `POST /auth/credentials/{id}/challenge`. Includes the WebAuthn `credentialId` needed to target the passkey, plus the Grid-issued `challenge`, corresponding `requestId`, and challenge `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation request body, not a base64url string. The client UTF-8 encodes this string as the WebAuthn challenge and signs it with the passkey to produce the assertion submitted to `POST /auth/credentials/{id}/verify`. @@ -22976,16 +22976,16 @@ components: $ref: '#/components/schemas/AuthSession' AuthSessionRefreshRequest: title: Auth Session Refresh Request - description: Request body for refreshing an active authentication session. The `clientPublicKey` is required on both steps of the signed-retry flow; its matching private key is retained on the client and becomes the new session signing key under the client-held-key model. On the initial call, Grid binds this key into the session-creation payload returned as `payloadToSign`; the client echoes the same key back on the signed retry. + description: 'Request body for refreshing an active authentication session. The `clientPublicKey` is required on both steps of the signed-retry flow and must match on both. Its SEC1 encoding selects how the refreshed session signing key is delivered: a compressed key gets the recommended client-held-key model, where the client retains the new signing key itself; an uncompressed key gets the deprecated legacy flow, where Grid returns the new key as `encryptedSessionSigningKey` sealed to it. On the initial call, Grid binds the supplied key into the session-creation payload returned as `payloadToSign`.' type: object required: - clientPublicKey properties: clientPublicKey: type: string - pattern: ^04[0-9a-fA-F]{128}$ - description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key is retained on the client and serves as the new session signing key under the client-held-key model. Grid binds this public key into the session-creation payload on the initial call; for legacy sessions not yet migrated it additionally seals the deprecated `encryptedSessionSigningKey` to it on the signed retry. - example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + pattern: ^(0[23][0-9a-fA-F]{64}|04[0-9a-fA-F]{128})$ + description: Client-generated P-256 public key; the matching private key is retained on the client. Send a compressed SEC1 key (`02`/`03` prefix followed by the 32-byte X coordinate; 66 hex characters) for the recommended client-held-key model, where that private key becomes the new session signing key. Send an uncompressed SEC1 key (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) for the deprecated legacy flow, where Grid seals the new session signing key to it and returns it as `encryptedSessionSigningKey` on the signed retry. + example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 DelegatedKeyStatus: type: string enum: diff --git a/mintlify/snippets/global-accounts/client-keys.mdx b/mintlify/snippets/global-accounts/client-keys.mdx index af68aacc2..2a60465e9 100644 --- a/mintlify/snippets/global-accounts/client-keys.mdx +++ b/mintlify/snippets/global-accounts/client-keys.mdx @@ -2,14 +2,24 @@ Every signed Global Account action uses two key pairs: | Key pair | Where it lives | What it does | |---|---|---| -| **Client key pair** (P-256) | On the customer's device, generated fresh per session-issuing or export request | Used as the HPKE recipient key so Grid can encrypt session keys or wallet export credentials to the client. Ephemeral — one pair per authentication, session refresh, or wallet export. | -| **Session signing key** (P-256) | Issued by Grid, encrypted to the client public key, decrypted and held on the device | Signs every account action for the lifetime of the session (default 15 minutes). | +| **Client key pair** (P-256) | On the customer's device, generated fresh per session-issuing or export request | In the recommended client-held-key flow, its private key becomes the session signing key directly. In the legacy flow, its public key is the recipient key Grid encrypts the session key (or wallet export credentials) to. Ephemeral — one pair per authentication, session refresh, or wallet export. | +| **Session signing key** (P-256) | Held on the customer's device | Signs every account action for the lifetime of the session (default 15 minutes). In the client-held-key flow it is the client key pair's private key; in the legacy flow Grid issues it encrypted to the client public key for the client to decrypt. | -This page covers generating the client key pair, sending the public key to your backend, decrypting the session signing key, and signing payloads. Everything here runs **on the client**; your integrator backend only relays opaque byte strings. +This page covers generating the client key pair, sending the public key to your backend, holding (or, in the legacy flow, decrypting) the session signing key, and signing payloads. Everything here runs **on the client**; your integrator backend only relays opaque byte strings. + +## Client-held session key + +In the **recommended client-held-key flow**, the key pair you generate in step 1 *is* your session key: you keep the private key on the device and use it directly to sign account actions. There is no session key to receive or decrypt, and Grid never transmits one — so the verify, refresh, and challenge responses omit `encryptedSessionSigningKey`. This is how `EMAIL_OTP` and `SMS_OTP` have always worked; `OAUTH` and `PASSKEY` now follow the same model. + +To use it, send `clientPublicKey` in **compressed** SEC1 form — a 66-character hex string starting with `02` or `03` (the prefix followed by the 32-byte X coordinate). Grid treats a compressed key as a request to adopt it as the session signing key and returns an `AuthSession` with no `encryptedSessionSigningKey`. You then skip steps 2–3 and sign each `payloadToSign` with the private key you kept, exactly as in step 4. Because this key signs directly, generate it as a P-256 **signing** key (ECDSA) — not the ECDH recipient key the step 1 samples below create for the legacy decrypt flow. + + + Sending `clientPublicKey` in **uncompressed** SEC1 form (130 hex characters, `04` prefix) selects the deprecated legacy flow, in which Grid seals the session signing key to your public key and returns it as `encryptedSessionSigningKey` for you to decrypt — steps 2 and 3 below. New integrations should send the compressed key and hold their own session key. + ## 1. Generate a client key pair -Generate a fresh P-256 key pair for every authentication, session refresh, and wallet export. The public key is sent to Grid as `clientPublicKey` — for `PASSKEY` credentials this happens on `POST /auth/credentials/{id}/challenge`; for `EMAIL_OTP` and `OAUTH` it happens on `POST /auth/credentials/{id}/verify`; for session refresh it goes on both `/auth/sessions/{id}/refresh` calls; for wallet export it goes on both `/export` calls. Keep the private key in device-local secure storage (browser `IndexedDB` gated by Web Crypto's non-extractable flag, iOS Keychain, Android Keystore). Send the public key hex-encoded — a 130-character string starting with `04` — through your integrator backend. The Web Crypto, iOS, and Android APIs shown below all produce this format natively. +Generate a fresh P-256 key pair for every authentication, session refresh, and wallet export. The public key is sent to Grid as `clientPublicKey` — for `PASSKEY` credentials this happens on `POST /auth/credentials/{id}/challenge`; for `EMAIL_OTP` and `OAUTH` it happens on `POST /auth/credentials/{id}/verify`; for session refresh it goes on both `/auth/sessions/{id}/refresh` calls; for wallet export it goes on both `/export` calls. Keep the private key in device-local secure storage (browser `IndexedDB` gated by Web Crypto's non-extractable flag, iOS Keychain, Android Keystore). Send the public key hex-encoded through your integrator backend. For the recommended client-held-key flow, send the **compressed** form — a 66-character string starting with `02` or `03` (see [Client-held session key](#client-held-session-key)). For the legacy flow, send the **uncompressed** form — a 130-character string starting with `04`, which the Web Crypto, iOS, and Android APIs below produce natively. For local development, you can generate a P-256 key pair from the command line: @@ -166,6 +176,10 @@ The private key of the pair you generated in step 1 stays on the device — it b ## 2. Verify the credential and receive the encrypted session signing key + + Steps 2 and 3 apply to the **legacy** flow only (uncompressed `clientPublicKey`). In the recommended [client-held-key flow](#client-held-session-key) the response has no `encryptedSessionSigningKey` to receive or decrypt — skip to step 4. + + Your client sends `publicKeyHex` to your integrator backend along with whatever the credential type requires (OTP value, OIDC token, or WebAuthn assertion — see Authentication). Your backend calls `POST /auth/credentials/{id}/verify` and returns the `encryptedSessionSigningKey` from Grid's response to the client. Grid encrypts the session signing key with **HPKE** (RFC 9180) using the suite: diff --git a/openapi.yaml b/openapi.yaml index 6e64a5275..a578752bd 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5852,7 +5852,7 @@ paths: For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request. - On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. Under the client-held-key model the client already holds the session signing key — the private key it generated before authentication — so no key material is returned and the deprecated `encryptedSessionSigningKey` is omitted. That field is populated only for legacy `OAUTH` and `PASSKEY` sessions not yet migrated, where the key was HPKE-sealed to the supplied `clientPublicKey`. The `expiresAt` timestamp marks when the session expires. + On success for `OAUTH` and `PASSKEY`, and on the signed retry for OTP credentials, the response contains an `AuthSession`. Sending a compressed `clientPublicKey` selects the recommended client-held-key model: the client already holds the session signing key — the private key it generated before authentication — so no key material is returned and the deprecated `encryptedSessionSigningKey` is omitted. Sending an uncompressed `clientPublicKey` selects the deprecated legacy flow, where the session signing key is HPKE-sealed to that key and returned as `encryptedSessionSigningKey` for the client to decrypt. The `expiresAt` timestamp marks when the session expires. operationId: verifyAuthCredential tags: - Embedded Wallet Auth @@ -5907,7 +5907,7 @@ paths: value: type: OAUTH oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 passkey: summary: Verify a passkey credential value: @@ -5983,7 +5983,7 @@ paths: `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `clientPublicKey`. - For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — under the client-held-key model the client retains the matching private key as the resulting session signing key. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. + For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation payload the returned challenge is computed from — send a compressed key for the recommended client-held-key model, where the client retains the matching private key as the resulting session signing key, or an uncompressed key for the deprecated legacy flow. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation body, not a base64url string. The client base64url-decodes `credentialId` for `allowCredentials[].id` and UTF-8 encodes `challenge` (for example, `new TextEncoder().encode(challenge)`) as the WebAuthn challenge in `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. operationId: challengeAuthCredential tags: - Embedded Wallet Auth @@ -6007,7 +6007,7 @@ paths: passkey: summary: Re-challenge a passkey credential value: - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 emailOtp: summary: Re-challenge an email-OTP credential (empty body) value: {} @@ -6201,9 +6201,9 @@ paths: description: | Refresh an active Embedded Wallet auth session and create a new session signing key. Session refresh is a two-step signed-retry flow: - 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a Grid session-refresh payload, binds the supplied `clientPublicKey` into that payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. + 1. Call `POST /auth/sessions/{id}/refresh` with the request body `{ "clientPublicKey": "02..." }` and no signature headers. Send a freshly generated client public key and retain its private key. Grid binds the supplied `clientPublicKey` into the session-refresh payload, persists it as a pending request, and returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. - 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession`. Under the client-held-key model the client retains the new session signing key, so no key material is returned; the deprecated `encryptedSessionSigningKey` is sealed to the client public key only for legacy sessions not yet migrated. + 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in the request body. On success, Grid returns a new `AuthSession`. Sending a compressed `clientPublicKey` selects the recommended client-held-key model, where the client retains the new session signing key and no key material is returned; sending an uncompressed key selects the deprecated legacy flow, where the new key is sealed to it and returned as `encryptedSessionSigningKey`. The original session must still be active on both steps so it can authorize the refresh. If the session has already expired, use the credential reauthentication flow instead. operationId: refreshAuthSession @@ -6243,7 +6243,7 @@ paths: refresh: summary: Refresh an active session value: - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 responses: '201': description: New authentication session created successfully. @@ -6272,7 +6272,7 @@ paths: challenge: summary: Session refresh challenge value: - payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}' + payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}' requestId: Request:019542f5-b3e7-1d02-0000-000000000010 expiresAt: '2026-04-08T15:35:00Z' '400': @@ -22816,8 +22816,8 @@ components: example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q clientPublicKey: type: string - description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (0x04 prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key is retained on the client and serves as the session signing key under the client-held-key model. The key is ephemeral and one-time-use per verification request. - example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + description: Client-generated, ephemeral P-256 public key; the matching private key is retained on the client and is one-time-use per verification request. Send a compressed SEC1 key (`02`/`03` prefix followed by the 32-byte X coordinate; 66 hex characters) for the recommended client-held-key model, where that private key becomes the session signing key. Send an uncompressed SEC1 key (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) for the deprecated legacy flow, where Grid seals the session signing key to it instead. + example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 OauthCredentialVerifyRequest: title: OAuth Credential Verify Request allOf: @@ -22886,7 +22886,7 @@ components: PASSKEY: '#/components/schemas/PasskeyCredentialVerifyRequest' AuthSession: title: Authentication Session - description: An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or `POST /auth/sessions/{id}/refresh` (on mid-session refresh). Under the client-held-key model the client generates and retains the session signing key, so session-issuing responses carry no key material. The deprecated `encryptedSessionSigningKey` is populated only for legacy sessions not yet migrated to that model and is never returned by the list endpoint. + description: 'An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or `POST /auth/sessions/{id}/refresh` (on mid-session refresh). The `clientPublicKey` encoding on the issuing request selects the flow: a compressed key gets the client-held-key model, where the client generates and retains the session signing key and session-issuing responses carry no key material; an uncompressed key gets the deprecated legacy flow, where Grid seals the session signing key to that public key and returns it as `encryptedSessionSigningKey`. Never returned by the list endpoint.' allOf: - $ref: '#/components/schemas/AuthMethod' - type: object @@ -22902,9 +22902,9 @@ components: type: string deprecated: true description: |- - Deprecated. Superseded by the client-held-key model, which is now the norm for every credential type: the client generates a session keypair before authentication and retains the private key, so the server has no key to deliver and this field is omitted from session-issuing responses. + Deprecated; present only for the legacy flow, selected by sending an uncompressed `clientPublicKey` on the verification or refresh request. Grid seals the session signing key to that public key and returns it here as a base58check string (a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext) for the client to decrypt with its private key. - Historically populated only for `OAUTH` and `PASSKEY` sessions, where the session signing key was HPKE-sealed to the supplied `clientPublicKey` and returned here as a base58check string (a 33-byte compressed P-256 encapsulated public key followed by AES-256-GCM ciphertext) for the client to decrypt with its private key. Migrated credential types omit it entirely; it remains present only for legacy sessions not yet moved to the client-held-key model, and will be removed in a future version. Always omitted from list responses (`GET /auth/sessions`). + The recommended client-held-key flow sends a compressed `clientPublicKey` instead: the client generates and retains the session signing key itself, so this field is omitted — the same way `EMAIL_OTP` and `SMS_OTP` sessions have always worked. See the "Client keys & signing" guide. Always omitted from list responses (`GET /auth/sessions`). example: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf expiresAt: type: string @@ -22918,11 +22918,11 @@ components: properties: clientPublicKey: type: string - pattern: ^04[0-9a-fA-F]{128}$ - minLength: 130 + pattern: ^(0[23][0-9a-fA-F]{64}|04[0-9a-fA-F]{128})$ + minLength: 66 maxLength: 130 - description: Required for `PASSKEY` credentials. Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key is retained on the client and serves as the session signing key under the client-held-key model. Grid bakes this public key into the session-creation payload that the returned `challenge` is computed from. Ignored for `EMAIL_OTP` and `SMS_OTP`. - example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + description: Required for `PASSKEY` credentials; the matching private key is retained on the client. Send a compressed SEC1 key (`02`/`03` prefix followed by the 32-byte X coordinate; 66 hex characters) for the recommended client-held-key model, where that private key becomes the session signing key. Send an uncompressed SEC1 key (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) for the deprecated legacy flow, where Grid seals the session signing key to it instead. Grid bakes this public key into the session-creation payload that the returned `challenge` is computed from. Ignored for `EMAIL_OTP` and `SMS_OTP`. + example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 PasskeyAuthChallenge: title: Passkey Auth Challenge description: Extended `AuthMethod` shape returned for `PASSKEY` credentials from `POST /auth/credentials/{id}/challenge`. Includes the WebAuthn `credentialId` needed to target the passkey, plus the Grid-issued `challenge`, corresponding `requestId`, and challenge `expiresAt`. The `challenge` value is the lowercase hex-encoded SHA-256 digest of the canonical session-creation request body, not a base64url string. The client UTF-8 encodes this string as the WebAuthn challenge and signs it with the passkey to produce the assertion submitted to `POST /auth/credentials/{id}/verify`. @@ -22976,16 +22976,16 @@ components: $ref: '#/components/schemas/AuthSession' AuthSessionRefreshRequest: title: Auth Session Refresh Request - description: Request body for refreshing an active authentication session. The `clientPublicKey` is required on both steps of the signed-retry flow; its matching private key is retained on the client and becomes the new session signing key under the client-held-key model. On the initial call, Grid binds this key into the session-creation payload returned as `payloadToSign`; the client echoes the same key back on the signed retry. + description: 'Request body for refreshing an active authentication session. The `clientPublicKey` is required on both steps of the signed-retry flow and must match on both. Its SEC1 encoding selects how the refreshed session signing key is delivered: a compressed key gets the recommended client-held-key model, where the client retains the new signing key itself; an uncompressed key gets the deprecated legacy flow, where Grid returns the new key as `encryptedSessionSigningKey` sealed to it. On the initial call, Grid binds the supplied key into the session-creation payload returned as `payloadToSign`.' type: object required: - clientPublicKey properties: clientPublicKey: type: string - pattern: ^04[0-9a-fA-F]{128}$ - description: Client-generated P-256 public key, hex-encoded in uncompressed SEC1 format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters total). The matching private key is retained on the client and serves as the new session signing key under the client-held-key model. Grid binds this public key into the session-creation payload on the initial call; for legacy sessions not yet migrated it additionally seals the deprecated `encryptedSessionSigningKey` to it on the signed retry. - example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + pattern: ^(0[23][0-9a-fA-F]{64}|04[0-9a-fA-F]{128})$ + description: Client-generated P-256 public key; the matching private key is retained on the client. Send a compressed SEC1 key (`02`/`03` prefix followed by the 32-byte X coordinate; 66 hex characters) for the recommended client-held-key model, where that private key becomes the new session signing key. Send an uncompressed SEC1 key (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; 130 hex characters) for the deprecated legacy flow, where Grid seals the new session signing key to it and returns it as `encryptedSessionSigningKey` on the signed retry. + example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 DelegatedKeyStatus: type: string enum: diff --git a/openapi/components/schemas/auth/AuthCredentialChallengeRequest.yaml b/openapi/components/schemas/auth/AuthCredentialChallengeRequest.yaml index daab61be9..ab68ba212 100644 --- a/openapi/components/schemas/auth/AuthCredentialChallengeRequest.yaml +++ b/openapi/components/schemas/auth/AuthCredentialChallengeRequest.yaml @@ -11,16 +11,19 @@ type: object properties: clientPublicKey: type: string - pattern: "^04[0-9a-fA-F]{128}$" - minLength: 130 + pattern: "^(0[23][0-9a-fA-F]{64}|04[0-9a-fA-F]{128})$" + minLength: 66 maxLength: 130 description: >- - Required for `PASSKEY` credentials. Client-generated P-256 public - key, hex-encoded in uncompressed SEC1 format (`04` prefix followed - by the 32-byte X and 32-byte Y coordinates; 130 hex characters - total). The matching private key is retained on the client and - serves as the session signing key under the client-held-key - model. Grid bakes this public key into the session-creation - payload that the returned `challenge` is computed from. Ignored - for `EMAIL_OTP` and `SMS_OTP`. - example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + Required for `PASSKEY` credentials; the matching private key is + retained on the client. Send a compressed SEC1 key (`02`/`03` + prefix followed by the 32-byte X coordinate; 66 hex characters) + for the recommended client-held-key model, where that private key + becomes the session signing key. Send an uncompressed SEC1 key + (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; + 130 hex characters) for the deprecated legacy flow, where Grid + seals the session signing key to it instead. Grid bakes this + public key into the session-creation payload that the returned + `challenge` is computed from. Ignored for `EMAIL_OTP` and + `SMS_OTP`. + example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 diff --git a/openapi/components/schemas/auth/AuthSession.yaml b/openapi/components/schemas/auth/AuthSession.yaml index dceb6d183..50a94fad3 100644 --- a/openapi/components/schemas/auth/AuthSession.yaml +++ b/openapi/components/schemas/auth/AuthSession.yaml @@ -3,12 +3,14 @@ description: >- An authentication session on an Embedded Wallet internal account. Returned from `GET /auth/sessions` (list) and `POST /auth/credentials/{id}/verify` (on credential verification) or - `POST /auth/sessions/{id}/refresh` (on mid-session refresh). Under the - client-held-key model the client generates and retains the session - signing key, so session-issuing responses carry no key material. The - deprecated `encryptedSessionSigningKey` is populated only for legacy - sessions not yet migrated to that model and is never returned by the - list endpoint. + `POST /auth/sessions/{id}/refresh` (on mid-session refresh). The + `clientPublicKey` encoding on the issuing request selects the flow: a + compressed key gets the client-held-key model, where the client + generates and retains the session signing key and session-issuing + responses carry no key material; an uncompressed key gets the + deprecated legacy flow, where Grid seals the session signing key to + that public key and returns it as `encryptedSessionSigningKey`. Never + returned by the list endpoint. allOf: - $ref: ./AuthMethod.yaml - type: object @@ -29,22 +31,21 @@ allOf: type: string deprecated: true description: >- - Deprecated. Superseded by the client-held-key model, which is - now the norm for every credential type: the client generates a - session keypair before authentication and retains the private - key, so the server has no key to deliver and this field is - omitted from session-issuing responses. + Deprecated; present only for the legacy flow, selected by + sending an uncompressed `clientPublicKey` on the verification + or refresh request. Grid seals the session signing key to + that public key and returns it here as a base58check string + (a 33-byte compressed P-256 encapsulated public key followed + by AES-256-GCM ciphertext) for the client to decrypt with its + private key. - Historically populated only for `OAUTH` and `PASSKEY` sessions, - where the session signing key was HPKE-sealed to the supplied - `clientPublicKey` and returned here as a base58check string (a - 33-byte compressed P-256 encapsulated public key followed by - AES-256-GCM ciphertext) for the client to decrypt with its - private key. Migrated credential types omit it entirely; it - remains present only for legacy sessions not yet moved to the - client-held-key model, and will be removed in a future version. - Always omitted from list responses (`GET /auth/sessions`). + The recommended client-held-key flow sends a compressed + `clientPublicKey` instead: the client generates and retains + the session signing key itself, so this field is omitted — + the same way `EMAIL_OTP` and `SMS_OTP` sessions have always + worked. See the "Client keys & signing" guide. Always omitted + from list responses (`GET /auth/sessions`). example: w99a5xV6A75TfoAUkZn869fVyDYvgVsKrawMALZXmrauZd8hEv66EkPU1Z42CUaHESQjcA5bqd8dynTGBMLWB9ewtXWPEVbZvocB4Tw2K1vQVp7uwjf expiresAt: type: string diff --git a/openapi/components/schemas/auth/AuthSessionRefreshRequest.yaml b/openapi/components/schemas/auth/AuthSessionRefreshRequest.yaml index 9672c8997..d9c4fda9e 100644 --- a/openapi/components/schemas/auth/AuthSessionRefreshRequest.yaml +++ b/openapi/components/schemas/auth/AuthSessionRefreshRequest.yaml @@ -1,25 +1,29 @@ title: Auth Session Refresh Request description: >- Request body for refreshing an active authentication session. The - `clientPublicKey` is required on both steps of the signed-retry flow; its - matching private key is retained on the client and becomes the new session - signing key under the client-held-key model. On the initial call, Grid - binds this key into the session-creation payload returned as - `payloadToSign`; the client echoes the same key back on the signed retry. + `clientPublicKey` is required on both steps of the signed-retry flow and + must match on both. Its SEC1 encoding selects how the refreshed session + signing key is delivered: a compressed key gets the recommended + client-held-key model, where the client retains the new signing key + itself; an uncompressed key gets the deprecated legacy flow, where Grid + returns the new key as `encryptedSessionSigningKey` sealed to it. On the + initial call, Grid binds the supplied key into the session-creation + payload returned as `payloadToSign`. type: object required: - clientPublicKey properties: clientPublicKey: type: string - pattern: "^04[0-9a-fA-F]{128}$" + pattern: "^(0[23][0-9a-fA-F]{64}|04[0-9a-fA-F]{128})$" description: >- - Client-generated P-256 public key, hex-encoded in uncompressed SEC1 - format (`04` prefix followed by the 32-byte X and 32-byte Y coordinates; - 130 hex characters total). The matching private key is retained on the - client and serves as the new session signing key under the - client-held-key model. Grid binds this public key into the - session-creation payload on the initial call; for legacy sessions not - yet migrated it additionally seals the deprecated - `encryptedSessionSigningKey` to it on the signed retry. - example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + Client-generated P-256 public key; the matching private key is + retained on the client. Send a compressed SEC1 key (`02`/`03` prefix + followed by the 32-byte X coordinate; 66 hex characters) for the + recommended client-held-key model, where that private key becomes the + new session signing key. Send an uncompressed SEC1 key (`04` prefix + followed by the 32-byte X and 32-byte Y coordinates; 130 hex + characters) for the deprecated legacy flow, where Grid seals the new + session signing key to it and returns it as + `encryptedSessionSigningKey` on the signed retry. + example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 diff --git a/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml b/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml index c47acd650..321796ce5 100644 --- a/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml +++ b/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml @@ -25,10 +25,13 @@ properties: clientPublicKey: type: string description: >- - Client-generated P-256 public key, hex-encoded in uncompressed SEC1 - format (0x04 prefix followed by the 32-byte X and 32-byte Y - coordinates; 130 hex characters total). The matching private key - is retained on the client and serves as the session signing key - under the client-held-key model. The key is ephemeral and - one-time-use per verification request. - example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + Client-generated, ephemeral P-256 public key; the matching private + key is retained on the client and is one-time-use per verification + request. Send a compressed SEC1 key (`02`/`03` prefix followed by + the 32-byte X coordinate; 66 hex characters) for the recommended + client-held-key model, where that private key becomes the session + signing key. Send an uncompressed SEC1 key (`04` prefix followed by + the 32-byte X and 32-byte Y coordinates; 130 hex characters) for the + deprecated legacy flow, where Grid seals the session signing key to + it instead. + example: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 diff --git a/openapi/paths/auth/auth_credentials_{id}_challenge.yaml b/openapi/paths/auth/auth_credentials_{id}_challenge.yaml index 64bc1f049..5df07e520 100644 --- a/openapi/paths/auth/auth_credentials_{id}_challenge.yaml +++ b/openapi/paths/auth/auth_credentials_{id}_challenge.yaml @@ -22,9 +22,10 @@ post: For `PASSKEY` credentials, this issues a fresh Grid reauthentication challenge. The request body must carry the client's ephemeral `clientPublicKey` so Grid can bake it into the session-creation - payload the returned challenge is computed from — under the - client-held-key model the client retains the matching private key - as the resulting session signing key. + payload the returned challenge is computed from — send a compressed + key for the recommended client-held-key model, where the client + retains the matching private key as the resulting session signing + key, or an uncompressed key for the deprecated legacy flow. The response is a `PasskeyAuthChallenge` — the passkey auth method fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`, and `expiresAt`. The `challenge` value is the lowercase hex-encoded @@ -64,7 +65,7 @@ post: passkey: summary: Re-challenge a passkey credential value: - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 emailOtp: summary: Re-challenge an email-OTP credential (empty body) value: {} diff --git a/openapi/paths/auth/auth_credentials_{id}_verify.yaml b/openapi/paths/auth/auth_credentials_{id}_verify.yaml index 71ba73ce8..ae40fcd88 100644 --- a/openapi/paths/auth/auth_credentials_{id}_verify.yaml +++ b/openapi/paths/auth/auth_credentials_{id}_verify.yaml @@ -43,14 +43,16 @@ post: On success for `OAUTH` and `PASSKEY`, and on the signed retry for - OTP credentials, the response contains an `AuthSession`. Under the - client-held-key model the client already holds the session signing - key — the private key it generated before authentication — so no - key material is returned and the deprecated - `encryptedSessionSigningKey` is omitted. That field is populated - only for legacy `OAUTH` and `PASSKEY` sessions not yet migrated, - where the key was HPKE-sealed to the supplied `clientPublicKey`. - The `expiresAt` timestamp marks when the session expires. + OTP credentials, the response contains an `AuthSession`. Sending a + compressed `clientPublicKey` selects the recommended + client-held-key model: the client already holds the session + signing key — the private key it generated before authentication — + so no key material is returned and the deprecated + `encryptedSessionSigningKey` is omitted. Sending an uncompressed + `clientPublicKey` selects the deprecated legacy flow, where the + session signing key is HPKE-sealed to that key and returned as + `encryptedSessionSigningKey` for the client to decrypt. The + `expiresAt` timestamp marks when the session expires. operationId: verifyAuthCredential tags: - Embedded Wallet Auth @@ -126,7 +128,7 @@ post: value: type: OAUTH oidcToken: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.-3_ETmSGOl4wGNLR1QSOMlHk5IvADpX3YdHFmTH9KmRu6sEhM20RsURjKrI4-_EKj7J_HtsdS1tCHm0iw2J0qtoczYFQqEW_U9qJD6QsuvTFx8Fj9rFa3ieYhZKi3kkBu6cADogUiudP50kf9345ATys2GrYm-ba5esgReW1WzGJG3SgCyIDnHFfxmeLjE2YE9EFxT73To3mPYAk0ywPL2MpFFV9F8I3PsnbDAxinaY75GeA8vJXATr8weEIXqHD2lxmXVE95qd2ZlcuyLUaEYyp9GXcOnx7SjhdJG88jl5BZQvxOVgBMo42iGjK674lSwsMiHpzLX98j6C786Rd9Q - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 passkey: summary: Verify a passkey credential value: diff --git a/openapi/paths/auth/auth_sessions_{id}_refresh.yaml b/openapi/paths/auth/auth_sessions_{id}_refresh.yaml index 0f57a70f3..9cf9182aa 100644 --- a/openapi/paths/auth/auth_sessions_{id}_refresh.yaml +++ b/openapi/paths/auth/auth_sessions_{id}_refresh.yaml @@ -6,20 +6,22 @@ post: 1. Call `POST /auth/sessions/{id}/refresh` with the request body - `{ "clientPublicKey": "04..." }` and no signature headers. Grid builds a - Grid session-refresh payload, binds the supplied - `clientPublicKey` into that payload, persists it as a pending request, and - returns `202` with `payloadToSign`, `requestId`, and `expiresAt`. + `{ "clientPublicKey": "02..." }` and no signature headers. Send a + freshly generated client public key and retain its private key. Grid + binds the supplied `clientPublicKey` into the session-refresh payload, + persists it as a pending request, and returns `202` with + `payloadToSign`, `requestId`, and `expiresAt`. 2. Sign `payloadToSign` with the current session signing key, then retry the same request with the full API-key stamp as `Grid-Wallet-Signature`, the `requestId` echoed back as `Request-Id`, and the same `clientPublicKey` in - the request body. On success, Grid returns a new `AuthSession`. Under the - client-held-key model the client retains the new session signing key, so - no key material is returned; the deprecated `encryptedSessionSigningKey` - is sealed to the client public key only for legacy sessions not yet - migrated. + the request body. On success, Grid returns a new `AuthSession`. Sending a + compressed `clientPublicKey` selects the recommended client-held-key + model, where the client retains the new session signing key and no key + material is returned; sending an uncompressed key selects the + deprecated legacy flow, where the new key is sealed to it and returned + as `encryptedSessionSigningKey`. The original session must still be active on both steps so it can authorize @@ -69,7 +71,7 @@ post: refresh: summary: Refresh an active session value: - clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 + clientPublicKey: 02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31 responses: '201': description: New authentication session created successfully. @@ -101,7 +103,7 @@ post: challenge: summary: Session refresh challenge value: - payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}' + payloadToSign: '{"organizationId":"org_abc123","parameters":{"targetPublicKey":"02f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31"},"timestampMs":"1746736509954","type":"ACTIVITY_TYPE_CREATE_READ_WRITE_SESSION_V2"}' requestId: Request:019542f5-b3e7-1d02-0000-000000000010 expiresAt: '2026-04-08T15:35:00Z' '400': diff --git a/scripts/embedded-wallet-sign.js b/scripts/embedded-wallet-sign.js index c7bb8b6ff..e3b985772 100755 --- a/scripts/embedded-wallet-sign.js +++ b/scripts/embedded-wallet-sign.js @@ -8,11 +8,15 @@ * * Subcommands: * - * gen-keypair - * Generate an ephemeral P-256 keypair (TEK). Prints JSON with `pubHex` - * and `privHex`. The public key is encrypted inside the OTP bundle - * for EMAIL_OTP verification; the private key becomes the session - * signing key after successful verify. + * gen-keypair [--compressed] + * Generate an ephemeral P-256 keypair. Prints JSON with `pubHex` and + * `privHex`. By default `pubHex` is uncompressed SEC1 (`04...`): for + * EMAIL_OTP, it's encrypted inside the OTP bundle and the private key + * becomes the session signing key after successful verify. With + * `--compressed`, `pubHex` is compressed SEC1 (`02`/`03...`) — pass it + * as `clientPublicKey` on the OAuth/PASSKEY challenge or verify call to + * select the client-held-key flow, where this private key becomes the + * session signing key directly. * * encrypt-otp * HPKE-encrypt `{otp_code, public_key}` under the target bundle @@ -21,10 +25,11 @@ * `POST /auth/credentials/{id}/verify`. * * decrypt-bundle - * HPKE-open the `encryptedSessionSigningKey` returned by PASSKEY/OAUTH - * `POST /auth/credentials/{id}/verify`. (EMAIL_OTP does not return - * this field — the TEK private key IS the session signing key.) - * Prints the session API private key as hex. + * HPKE-open the `encryptedSessionSigningKey` returned by the legacy + * (uncompressed-key) PASSKEY/OAUTH `POST /auth/credentials/{id}/verify` + * flow. Not needed for EMAIL_OTP or the compressed-key client-held + * flow — in both, the generated private key IS the session signing + * key. Prints the session API private key as hex. * * stamp * Build a Grid wallet signature over a `payloadToSign`. Prints the value @@ -51,16 +56,21 @@ function jwkB64ToHex(b64url) { return Buffer.from(b64url, "base64url").toString("hex"); } -function genKeypair() { +function genKeypair({ compressed = false } = {}) { const { publicKey, privateKey } = generateKeyPairSync("ec", { namedCurve: "prime256v1", }); const privJwk = privateKey.export({ format: "jwk" }); + const privHex = jwkB64ToHex(privJwk.d); + if (compressed) { + // Compressed SEC1 (02/03 prefix) selects the client-held-key flow on + // the OAuth/PASSKEY challenge and verify endpoints. + return { pubHex: privHexToCompressedPubHex(privHex), privHex }; + } const pubJwk = publicKey.export({ format: "jwk" }); // Uncompressed SEC1: 0x04 || X || Y const pubHex = "04" + jwkB64ToHex(pubJwk.x) + jwkB64ToHex(pubJwk.y); - const privHex = jwkB64ToHex(privJwk.d); return { pubHex, privHex }; } @@ -102,7 +112,8 @@ async function main() { const [, , cmd, ...rest] = process.argv; switch (cmd) { case "gen-keypair": { - const out = genKeypair(); + const compressed = rest.includes("--compressed"); + const out = genKeypair({ compressed }); process.stdout.write(JSON.stringify(out, null, 2) + "\n"); return; } @@ -155,9 +166,9 @@ function usage(code) { "embedded-wallet-sign — signing helpers for the Grid offramp flow", "", "Subcommands:", - " gen-keypair Generate ephemeral P-256 keypair (TEK)", + " gen-keypair [--compressed] Generate ephemeral P-256 keypair; --compressed for the OAuth/PASSKEY client-held flow", " encrypt-otp HPKE-encrypt OTP for EMAIL_OTP verify", - " decrypt-bundle HPKE-open the session signing key (PASSKEY/OAUTH)", + " decrypt-bundle HPKE-open the session signing key (legacy PASSKEY/OAUTH flow)", " stamp Build a Grid-Wallet-Signature stamp", "", "Use - in place of any argument to read it from stdin.",