Request and Response Signatures
Overview
Request and response signatures help protect Stitch API bodies from tampering. When this feature is enabled, your integration signs each API request, and Stitch signs each API response.
Both signatures use JWS Compact Serialization with an
unencoded, detached payload. Request signatures are sent in the
Client-Signature API header, and response signatures are sent in the Stitch-Signature API header. Both use PS512.
Request and response signatures must be explicitly enabled for your client. Contact support@stitch.money to learn more.
Signature enforcement
When request and response signatures are enabled for your client, every API request must include a valid
Client-Signature header. Stitch rejects requests and returns an error when the Client-Signature header is missing,
malformed, uses an unsupported algorithm, is signed with an unknown key, has an invalid signature, or does not match the
request body.
Stitch signs every API response with the Stitch-Signature header. If your integration cannot verify the response
signature, treat the response as failed and do not trust the response body.
Before you start
Request and response signatures use asymmetric key pairs. Your integration signs requests with its private key, and Stitch verifies those requests with the corresponding public key. Stitch signs responses with its private key, and your integration verifies those responses with Stitch's public keys.
Before enabling signatures, generate an RSA key pair with a modulus of at least 2048 bits and provide the public key
details to Stitch during onboarding.
Each request-signing key must have a key ID (kid), which identifies the key used to generate or verify a signature.
Your integration defines the kid; use a stable value that is unique across your active and recently rotated signing
keys.
Your integration should use the active private key and kid when signing requests. Do not share your private key with
Stitch or any system that does not need to sign requests.
Signature format
Both signature headers contain a compact JWS with a detached payload. A compact JWS has three period-separated segments. The HTTP body is the JWS payload, but it is not included in the header value, so the middle segment is empty:
Client-Signature: <base64url(protected-header)>..<base64url(signature)>
Stitch-Signature: <base64url(protected-header)>..<base64url(signature)>
The JWS protected header must contain:
{
"alg": "PS512",
"kid": "key-id",
"b64": false,
"crit": ["b64"]
}
Setting b64 to false means that the JWS signing input contains the body bytes directly, without base64url-encoding
them.
Use these rules when signing or verifying a body:
algmust bePS512.kididentifies the private key used to sign the body and the public key used to verify it.b64must befalse, andcritmust contain onlyb64.- Sign the exact uncompressed application body bytes. For requests, these are the bytes supplied to the HTTP client before HTTP content encoding. For responses, verify the decoded bytes returned by the HTTP client after HTTP content encoding has been removed.
- Do not parse, reformat, or re-encode the body before verification. Even a whitespace change produces a different signature.
- For a request or response with no body, sign an empty byte sequence.
Client public key format
Your public signing key must be represented as an RSA JWK:
{
"kty": "RSA",
"kid": "key-id",
"use": "sig",
"alg": "PS512",
"n": "base64url-encoded-modulus",
"e": "AQAB"
}
Stitch public keys
Stitch publishes its public signing keys in JWKS format:
GET https://api.stitch.money/.well-known/jwks.json
{
"keys": [
{
"kty": "RSA",
"kid": "stitch-signing-8ac7323838e6",
"use": "sig",
"alg": "PS512",
"n": "base64url-encoded-modulus",
"e": "AQAB"
}
]
}
Stitch currently signs API responses with the private key corresponding to the public JWKS key whose kid is
stitch-signing-8ac7323838e6.
Create a request signature
Generate a signing key pair
Use a secure key management system that can generate an RSA key pair with a modulus of at least 2048 bits. For local setup, OpenSSL can generate a PKCS#8 private key that matches the Node.js signing example below:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out client-signing-private-key.pem
openssl pkey -in client-signing-private-key.pem -pubout -out client-signing-public-key.pem
Store the private key securely and use it only when signing requests. Share the public key details with Stitch during
onboarding, including the same kid your integration will use in request signatures.
Create the request body bytes once, sign those bytes, and send the same bytes as the HTTP body. Do not sign a parsed JSON
object and then serialize it separately, as even small serialization differences produce an invalid signature. Include
the detached compact JWS in the Client-Signature request header.
curl --request POST 'https://api.stitch.money/graphql' \
--header 'Authorization: Bearer <client_access_token>' \
--header 'Content-Type: application/json' \
--header 'Client-Signature: <detached_compact_jws>' \
--data '{"query":"query Ping { client { id } }"}'
- Node.js
import { CompactSign, importPKCS8 } from "jose";
const keyId = process.env.CLIENT_SIGNATURE_KEY_ID;
const privateKeyPem = process.env.CLIENT_SIGNATURE_PRIVATE_KEY_PEM;
async function loadPrivateKey() {
if (!privateKeyPem) {
throw new Error("Missing CLIENT_SIGNATURE_PRIVATE_KEY_PEM");
}
return importPKCS8(privateKeyPem.replace(/\\n/g, "\n"), "PS512");
}
async function createRequestSignature({ rawBody }) {
if (!keyId) {
throw new Error("Missing CLIENT_SIGNATURE_KEY_ID");
}
const privateKey = await loadPrivateKey();
return new CompactSign(rawBody)
.setProtectedHeader({
alg: "PS512",
kid: keyId,
b64: false,
crit: ["b64"],
})
.sign(privateKey);
}
const url = "https://api.stitch.money/graphql";
const method = "POST";
const body = JSON.stringify({
query: "query Ping { client { id } }",
});
const rawBody = Buffer.from(body, "utf8");
const signature = await createRequestSignature({ rawBody });
const response = await fetch(url, {
method,
headers: {
Authorization: `Bearer ${process.env.STITCH_CLIENT_ACCESS_TOKEN}`,
"Content-Type": "application/json",
"Client-Signature": signature,
},
body: rawBody,
});
Verifying responses
Verify the Stitch-Signature response header before parsing or trusting the response body. Stitch signs the uncompressed
application response bytes before HTTP content encoding is applied. Verify the decoded bytes returned by your HTTP
client after it removes any HTTP content encoding.
Use the kid in the JWS protected header to select the correct Stitch public key from the JWKS. Stitch response
signatures currently use kid stitch-signing-8ac7323838e6.
For a valid response signature:
- The compact JWS must have exactly three segments, and the detached payload segment must be empty.
- The JWS must be signed with
PS512. - The JWS must use a known
kidfrom Stitch's JWKS. - The protected header must contain
b64: falseandcrit: ["b64"]. - The signature must verify against the exact decoded application response bytes.
- Node.js
import {
createRemoteJWKSet,
decodeProtectedHeader,
flattenedVerify,
} from "jose";
const stitchJwks = createRemoteJWKSet(
new URL("https://api.stitch.money/.well-known/jwks.json"),
);
function parseDetachedCompactJws(signature) {
const segments = signature.split(".");
if (segments.length !== 3) {
throw new Error("Invalid Stitch response signature format");
}
const [protectedHeader, payload, encodedSignature] = segments;
if (!protectedHeader || !encodedSignature) {
throw new Error("Invalid Stitch response signature format");
}
if (payload !== "") {
throw new Error("Stitch response signature payload is not detached");
}
return {
protected: protectedHeader,
signature: encodedSignature,
};
}
async function verifyResponseSignature({ signature, rawBody }) {
if (!signature) {
throw new Error("Missing Stitch response signature");
}
const detachedJws = parseDetachedCompactJws(signature);
const header = decodeProtectedHeader(signature);
if (header.alg !== "PS512") {
throw new Error("Unsupported Stitch signature algorithm");
}
if (typeof header.kid !== "string" || header.kid.length === 0) {
throw new Error("Missing Stitch signature key ID");
}
if (
header.b64 !== false ||
!Array.isArray(header.crit) ||
header.crit.length !== 1 ||
header.crit[0] !== "b64"
) {
throw new Error("Unsupported Stitch signature payload encoding");
}
const { protectedHeader } = await flattenedVerify(
{
...detachedJws,
payload: rawBody,
},
stitchJwks,
{
algorithms: ["PS512"],
},
);
return protectedHeader;
}
const responseBody = Buffer.from(await response.arrayBuffer());
const responseSignature = response.headers.get("Stitch-Signature");
await verifyResponseSignature({
signature: responseSignature,
rawBody: responseBody,
});