Guides
Proof of Agency
A Proof of Agency is a compact ES256 JWS issued only for allow in enforce
mode. In the current product, enforce mode is available only in sandbox.
Shadow and test decisions never carry a proof.
Signed claims
{
"iss": "https://app.graypass.org",
"ver": 1,
"jti": "poa_...",
"env": "sandbox",
"eid": "env_...",
"tid": "tenant_...",
"sub": "sub_...",
"act": "sub_...",
"act_t": "human",
"action": "payout.destination.change",
"res": { "type": "merchant_account", "id": "<tenant-scoped digest>" },
"ah": "<canonical action hash>",
"pol": "payout.destination.change@1",
"asr": "<assurance-profile digest>",
"aud": "payments-app",
"iat": 0,
"nbf": 0,
"exp": 0,
"apr": "sandbox_confirm",
"mode": "enforce"
}
tid and eid prevent proof confusion between tenants or between a tenant’s
sandbox and live environment. ah binds the exact environment, action,
resource, context, subject, actor, actor type, audience, and nonce. The
resource id itself is replaced by a tenant-scoped digest.
Agent-shaped proofs are rejected because no agent-mandate verifier exists.
Offline verification
Configure the tenant and environment independently. Do not copy expected values out of the proof you are trying to verify.
const result = await graypass.verify(proof, {
audience: 'payments-app',
expectedAction: 'payout.destination.change',
expectedActionHash: decision.verification.actionHash,
});
if (!result.valid) throw new Error(result.reason);
Both verifiers require:
- compact JWS structure;
- protected
alg,typ, andkid; - exactly one matching P-256 signing JWK;
- a fixed-width JOSE
R || Ssignature; - issuer and proof version;
- tenant and stable environment id;
- audience, action, and canonical action hash;
- human actor type;
- enforce mode;
- integer
iat,nbf, andexpwith valid ordering and bounded lifetime; - optional relying-party replay storage.
The cross-language test includes a separate jose library as a signature
interoperability oracle. AWS KMS returns DER-encoded ECDSA signatures; the
signer converts them to the fixed-width JOSE representation before building
the compact JWS.
Canonical action hash
Python uses the Trail of Bits rfc8785 package and TypeScript uses the
canonicalize RFC 8785 package. GrayPass applies a narrower profile:
- floats are rejected;
- integers outside the interoperable safe range are rejected;
- undefined values, lone surrogates, and circular structures are rejected;
- Unicode object keys follow RFC 8785 UTF-16 ordering.
Use strings for monetary decimal values.
Revocation and one-time consumption
Offline signature validity and online state are different:
- an offline proof can remain cryptographically valid after it was consumed;
- a scoped online status check reports active, consumed, revoked, expired, or unknown;
- one-time consumption uses one conditional tenant and environment scoped update that also checks revocation and expiry.
The online status endpoint requires a server credential. A foreign tenant sees unknown, not an existence oracle.
Key separation and rotation
Production and staging accept KMS signers only. Both live and sandbox KMS keys are required, must differ, and are self-tested at boot. Local PEM and ephemeral keys are development-only.
JWKS may publish configured previous KMS public keys during an overlap window so unexpired proofs continue to verify. Operators must retain previous key ids for at least the verifier cache window plus the maximum proof lifetime and clock tolerance.
The signing code path and the DER-to-JOSE conversion are covered by the repository's tests. Signing against a real AWS KMS key, including IAM permissions, key policy, and region, is verified separately by an operator-run canary before any live release.
Generated from the API contract in contracts/ and the source in docs/. The build fails if this page disagrees with the running API.