@zkp2p/zkp2p-attestation
v2.0.0
Published
Browser, React Native, and Node verifier/encrypter for ZKP2P Nitro attested uploads
Downloads
15,537
Keywords
Readme
@zkp2p/zkp2p-attestation
Client-side verifier and encryptor for ZKP2P Nitro identity attestations, seller credential uploads, and buyer TEE session-material requests.
The package runs in Chrome MV3 service workers, browser DOM contexts, React Native with a Web Crypto polyfill, and Node >= 20. Library entrypoints use fetch, SubtleCrypto, crypto.getRandomValues, and Uint8Array; they do not import node:* modules or use Buffer.
Install
yarn add @zkp2p/zkp2p-attestationDuring monorepo development, point clients at attestation-service/package/zkp2p-attestation.
Environment Init
Use the client factory when the host owns an environment variable:
import { createNitroAttestationClient } from "@zkp2p/zkp2p-attestation";
const nitro = createNitroAttestationClient({
environment: process.env.ZKP2P_ATTESTATION_ENV === "production" ? "production" : "staging",
});Environment defaults:
| Environment | Service URL | PCR8 source |
|---|---|---|
| staging | https://attestation-service-staging.zkp2p.xyz | bundled staging pin |
| production | https://attestation-service.zkp2p.xyz | bundled production pin |
Callers can still pass attestationServiceUrl and trust.expectedPcr8Hex directly. Explicit trust pins always win. The preprod hostname is pinned to the isolated preprod enclave; it is not a production alias.
Fast Path
Wise uploads use the server-derived payee id path:
const encryptedUpload = await nitro.createEncryptedSellerCredentialUpload({
platform: "wise",
sessionMaterial: {
apiToken: "<wise-api-token>",
// Optional. Omit for single-profile PATs; include after a
// WISE_PROFILE_SELECTION_REQUIRED response for multi-profile PATs.
profileId: "41246868",
},
});
await fetch(`${attestationServiceUrl}/seller/credentials/wise`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ encryptedUpload }),
});This fetches GET /attestation?nonce=..., verifies the Nitro document to the pinned AWS root and PCR8, extracts the attested seller-upload RSA SPKI, and returns a compact JWE for POST /seller/credentials/:platform.
For the same flow without hand-rolling the POST:
const credentialBundle = await nitro.uploadSellerCredential({
platform: "wise",
sessionMaterial: {
apiToken: "<wise-api-token>",
profileId: "41246868",
},
});
const verifiedCredentialBundle = verifySellerCredentialBundle(credentialBundle, {
trustedSigners,
expectedPlatform: "wise",
expectedPayeeIdHash: hashUtf8("<wise-recipient-id>"),
});Venmo, Cash App, and PayPal still require a top-level payeeId; existing callers can keep using the same call shape:
const encryptedUpload = await nitro.createEncryptedSellerCredentialUpload({
payeeId: "1130030979",
sessionMaterial: venmoSessionMaterial,
});Venmo seller uploads can use either the existing session material or Google OAuth. For Google OAuth, pass the numeric Venmo account id as payeeId and use sessionMaterial: { credentialSource: "google_oauth", authorizationCode, redirectUri }; the service validates Gmail access against venmo.com DKIM-signed incoming payment receipts. To onboard through a partner-owned Google OAuth app instead of Peer's default app, include oauthClient: { clientId, clientSecret } inside that encrypted sessionMaterial. The client secret is seller credential material: the package only places it in the compact JWE upload plaintext, it reaches the service enclave through that JWE, and the service persists it only inside the KMS-envelope-sealed seller credential bundle that the curator holds as ciphertext. It rides the same encrypted credential envelope as the Gmail refresh token and is reused only for later Gmail token refresh.
PayPal Gmail OAuth seller uploads use the same app-selection model. Pass the seller PayPal email as payeeId and use sessionMaterial: { credentialSource: "google_oauth", authorizationCode, redirectUri }; the service validates Gmail access against paypal.com DKIM-signed payment receipts. To onboard through a partner-owned Google OAuth app, include oauthClient: { clientId, clientSecret } inside the encrypted sessionMaterial. The PayPal partner client secret is handled as seller credential material: it rides only in the compact JWE upload plaintext, is sealed with the Gmail refresh token in the KMS-envelope seller credential bundle, and is reused only to refresh that seller's Gmail access token.
Buyer TEE requests use the same attested upload key and compact JWE envelope. The typed helper encrypts the captured
session material, posts { encryptedSessionMaterial, params, chainId, intent } to
POST /buyer/verify/:platform/:actionType, unwraps the service envelope, and returns a typed AttestationOutput.
The service does not enforce capture-age or one-use replay limits for buyer TEE session material; verification depends
on the upstream session still being active. A leaked encrypted JWE is therefore valid for the upstream session
lifetime — treat any accidental disclosure as equivalent to leaking the underlying upstream credential (cookies, PAT,
etc.) and rotate the upstream session.
const attestation = await nitro.verifyBuyerTeePayment({
platform: "venmo",
actionType: "transfer_venmo",
sessionMaterial: {
Cookie: "<captured-cookie-header>",
"User-Agent": "<captured-user-agent>",
},
params: { SENDER_ID: "<venmo-account-id>", index: 0 },
chainId,
intent,
});
const verifiedAttestation = verifyBuyerTeePaymentAttestation(attestation, {
trustedSigners,
expectedPlatform: "venmo",
expectedActionType: "transfer_venmo",
expectedDomain: { chainId, verifyingContract },
expectedIntentHash: intent.intentHash,
});The helper is a thin wrapper over the existing wire format; callers can still hand-roll the final POST:
const encryptedSessionMaterial = await nitro.createEncryptedBuyerTeeSessionMaterial({
platform: "wise",
actionType: "transfer_wise",
sessionMaterial: { "X-Access-Token": "<captured-token>" },
});
await fetch(`${attestationServiceUrl}/buyer/verify/wise/transfer_wise`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
encryptedSessionMaterial,
params: { PROFILE_ID: "41246868", TRANSACTION_ID: "123456789" },
chainId,
intent,
}),
});Buyer TEE platform matrix:
| Platform | actionType | Required encrypted session material | Public params fields |
|---|---|---|---|
| venmo | transfer_venmo | Cookie | SENDER_ID, index |
| cashapp | transfer_cashapp | Cookie, x-csrf-token, x-device-name, x-request-signature, x-request-uuid, cash-web-request, x-web-device-info, x-web-context, x-bt-id | SENDER_ID, index |
| monzo | transfer_monzo | Authorization | TX_ID |
| wise | transfer_wise | Cookie or X-Access-Token | PROFILE_ID, TRANSACTION_ID |
| revolut | transfer_revolut | Cookie, x-device-id | index |
| chime | transfer_chime | Cookie, body | {} |
| zelle | transfer_zelle_bofa | Cookie | index |
| zelle | transfer_zelle_chase | Cookie, x-jpmc-channel, x-jpmc-csrf-token, Referer, Origin | index |
| zelle | transfer_zelle_citi | Cookie | index |
| paypal | transfer_paypal | Cookie | PAYMENT_ID |
Zelle buyer verification uses only platform: "zelle" with bank-specific action types. Every Zelle action attests
intent.paymentMethod = hashUtf8("zelle"); direct bank-specific platforms are unsupported.
Per-platform session-material types require these headers using the canonical names shown above while still allowing additional captured headers. Captured request bodies are session material because they can contain sensitive data and are encrypted before being sent to the service.
Identity requests use the same attested upload key and compact JWE plaintext shape as buyer TEE, but with a narrower
platform/action matrix. The typed helper encrypts captured session material, posts
{ platform, actionType, callerAddress, encryptedSessionMaterial, params } to POST /identity, unwraps the service response, and
returns an IdentityAttestationOutput. verifyIdentityAttestation checks the EIP-712 signature, trusted signer,
platform/action binding, expected caller address, expected payee hash, canonical identity dataHash, and validUntil,
then returns normalized identity details.
const identityPayload = await nitro.requestIdentityAttestation({
platform: "wise",
actionType: "register_wise",
callerAddress: "0x0000000000000000000000000000000000000002",
sessionMaterial: {
Cookie: "<captured-cookie-header>",
"X-Access-Token": "<captured-token>",
},
params: { PROFILE_ID: "41246868" },
});
const identity = verifyIdentityAttestation(identityPayload, {
trustedSigners,
expectedPlatform: "wise",
expectedActionType: "register_wise",
expectedCallerAddress: "0x0000000000000000000000000000000000000002",
expectedPayeeIdHash: hashUtf8("<wise-recipient-id>"),
});
// identity: { payeeIdHash, payeeId, username, metadata, ... }Identity platform matrix:
| Platform | actionType | Required encrypted session material | Public params fields |
|---|---|---|---|
| venmo | register_venmo | Cookie | SENDER_ID |
| paypal | register_paypal | Cookie | {} |
| wise | register_wise | Cookie, X-Access-Token | PROFILE_ID |
| cashapp | register_cashapp | sessionCookie, requestPayload, optional requestHeaders | {} |
For Venmo identity, pass SENDER_ID in public params and include a replayable Cookie header in encrypted session
material. The service verifies the authenticated account id, derives the stories URL from SENDER_ID, and requires
Venmo to return a valid stories response with an explicit stories array.
For Cash App identity, pass the replayable Cash App session cookie as sessionMaterial.sessionCookie and the canonical
MY_ACTIVITY_WEB_V2 activity request body as sessionMaterial.requestPayload. Include captured replayable headers in
sessionMaterial.requestHeaders when needed; the service ignores public params for this flow.
Buyer TEE and identity expose discriminated unions keyed by platform and actionType, including per-platform
params, so invalid pairings fail during TypeScript compilation. Seller credential upload keeps its existing
platform narrowing (wise derives payee id from the credential; Venmo, Cash App, and PayPal require payeeId) but
does not add exhaustive sessionMaterial typing: each seller platform can accept multiple credential sources, such as
cookie headers or Google OAuth with optional partner OAuth client material, and encoding all combinations would make
the SDK brittle without improving the wire contract.
Cache-Friendly Path
const verified = await nitro.fetchAndVerifyAttestation();
const encryptedUpload = await nitro.encryptSellerCredentialUpload({
key: verified.attestedSellerUploadKey,
platform: "wise",
sessionMaterial: {
apiToken: "<wise-api-token>",
},
});For buyer TEE flows with a cached attestation:
const encryptedSessionMaterial = await nitro.encryptBuyerTeeSessionMaterial({
key: verified.attestedSellerUploadKey,
platform: "venmo",
actionType: "transfer_venmo",
sessionMaterial: {
Cookie: "<captured-cookie-header>",
},
});For identity flows with a cached attestation:
const encryptedSessionMaterial = await nitro.encryptIdentitySessionMaterial({
key: verified.attestedSellerUploadKey,
platform: "paypal",
actionType: "register_paypal",
sessionMaterial: {
Cookie: "<captured-cookie-header>",
},
});The attested SPKI is stable within one enclave process. If the enclave restarts, cached keys become stale and upload decrypt will fail; fetch a fresh attestation for a new session.
React Native Wiring
React Native must provide Web Crypto-compatible primitives when they are not on globalThis.crypto:
const nitro = createNitroAttestationClient({
environment: "staging",
subtle: webCrypto.subtle,
getRandomValues: (out) => webCrypto.getRandomValues(out),
fetch,
});Any conforming SubtleCrypto works. The package does not require Buffer.
Direct Entrypoints
import {
createEncryptedBuyerTeeSessionMaterial,
createEncryptedIdentitySessionMaterial,
createEncryptedSellerCredentialUpload,
encryptBuyerTeeSessionMaterial,
encryptIdentitySessionMaterial,
encryptSellerCredentialUpload,
fetchAndVerifyAttestation,
getUnifiedPaymentVerifierDomainSeparator,
hashUtf8,
requestIdentityAttestation,
uploadSellerCredential,
verifyBuyerTeePaymentAttestation,
verifyBuyerTeePayment,
verifyIdentityAttestation,
verifySellerCredentialBundle,
} from "@zkp2p/zkp2p-attestation";Direct functions accept attestationServiceUrl, trust, fetch, subtle, getRandomValues, now, onWarning, and timeoutMs overrides. If no caller or bundled PCR8 pin exists and trust.strictPin !== true, the verifier falls back to the service-advertised PCR8 and emits TRUST_PIN_NOT_PROVIDED; production callers should pin.
PCR8 Rotation Runbook
- Update
src/trust/pins.tsfrom the deploymentSTATE.mdpublished by the attestation-service team. - Refresh
tests/golden/staging-2026-05-05.raw.jsonand expected metadata if staging rotates. - Run
yarn test:coverageandSTAGING_E2E=true yarn test:integration. - Release a patch version for PCR8-only rotations. Use a major version for wire-format or algorithm changes.
CLI
The existing verifier is preserved on top of the library:
yarn verify --service https://attestation-service-staging.zkp2p.xyzOptional EIP-712 signer cross-check remains available through --verify-signature.
