# Signed receipts

> 

Some operations need **more than one signature** — notarized approvals, multi-party contracts, M-of-N quorum operations, signed audit trails. The compact JWS has one signature slot; multi-signature JWS ([RFC 7515 §7.2](https://www.rfc-editor.org/rfc/rfc7515#section-7.2)) has as many as you need.

This example walks through a **notarized document** use case — Alice issues a document, and a neutral Notary witnesses it — then generalizes to quorum approval.

## The scenario

A user authorizes a bank transfer. Your system requires:

<steps level="4">

#### The **user's** signature (proves they authorized it).

#### Your **service's** signature (proves it was processed by you, not a replay).

#### A **notary's** signature (third-party attestation for audit).

</steps>

All three signatures cover the same payload. Any verifier can check any subset; a court can verify all three independently.

## Setup — three signers

```ts [keys.ts]
import { generateJWK } from "unjwt/jwk";

// User's key (e.g. on their device, RSA from their YubiKey)
const userKey = await generateJWK("ES256", { kid: "alice@example.com" });

// Your service's key (persisted, long-lived)
const serviceKey = await generateJWK("Ed25519", { kid: "banktransfer-svc-2025" });

// Notary's key (third party)
const notaryKey = await generateJWK("RS256", { kid: "notary.gov/2025-q1" });
```

Each signer brings their own algorithm — multi-signature doesn't require everyone to agree on `alg`.

## Signing

```ts [sign.ts]
import { signMulti } from "unjwt/jws";

const payload = {
  txId: "tx_4aX9",
  from: "alice@example.com",
  to: "bob@example.com",
  amount: 500,
  currency: "EUR",
  iss: "https://banktransfer.example.com",
};

const jws = await signMulti(
  payload,
  [
    { key: userKey.privateKey, protectedHeader: { typ: "authorization+jwt" } },
    { key: serviceKey.privateKey, protectedHeader: { typ: "transfer+jwt" } },
    { key: notaryKey.privateKey, unprotectedHeader: { "x-role": "notary" } },
  ],
  { expiresIn: "30D" }, // iat and exp are shared across all signatures
);

// jws: {
//   payload: "base64url(JSON)",
//   signatures: [
//     { protected: "...", signature: "..." },    // user
//     { protected: "...", signature: "..." },    // service
//     { protected: "...", header: { "x-role": "notary" }, signature: "..." },
//   ]
// }
```

Each signature is computed independently by each signer — in a real deployment, this would typically be **three separate ceremonies**:

<steps level="4">

#### User signs their part on their device.

#### Sends the signed piece to your service.

#### Service signs their part, forwards to notary.

#### Notary signs and returns the complete envelope.

</steps>

Multi-signature JWS supports this: you can build the structure incrementally by `push`ing new `signatures[]` entries. For this example we sign all three at once for clarity.

## Policy-driven verification

Now consumers of the signed envelope need to decide what "verified" means for *their* use case. `verifyMultiAll` returns the status of every signature so the caller applies their own policy — [RFC 7515 §7.2](https://www.rfc-editor.org/rfc/rfc7515#section-7.2) deliberately leaves this to the application.

### Policy 1 — all signatures must verify (strict)

```ts [policy-all.ts]
import { verifyMultiAll } from "unjwt/jws";

const outcomes = await verifyMultiAll(jws, async (header) => {
  // Lookup each signer's public key by kid
  return keyStoreFor(header.kid!);
});

if (!outcomes.every((o) => o.verified)) {
  const failed = outcomes.filter((o) => !o.verified);
  throw new Error(
    `Not all signatures valid: ${failed.map((o) => `${o.signerIndex}:${o.error.code}`).join(", ")}`,
  );
}

// All three signatures verified — safe to process
```

### Policy 2 — required signers (notary MUST sign)

```ts [policy-notary.ts]
const outcomes = await verifyMultiAll(jws, keyLookup);

const verifiedKids = new Set(outcomes.filter((o) => o.verified).map((o) => o.protectedHeader.kid));

if (!verifiedKids.has("notary.gov/2025-q1")) {
  throw new Error("Missing notary signature — cannot audit");
}
if (!verifiedKids.has("alice@example.com")) {
  throw new Error("Missing user authorization");
}
```

### Policy 3 — M-of-N quorum

For an "any 2 of our 3 backup signers can authorize" setup:

```ts [policy-quorum.ts]
const AUTHORIZED_KIDS = new Set([
  "backup-1@example.com",
  "backup-2@example.com",
  "backup-3@example.com",
]);

const outcomes = await verifyMultiAll(jws, keyLookup);

const verifiedAuthorized = outcomes
  .filter((o) => o.verified && AUTHORIZED_KIDS.has(o.protectedHeader.kid ?? ""))
  .map((o) => o.protectedHeader.kid);

if (new Set(verifiedAuthorized).size < 2) {
  throw new Error("Quorum not met (need 2 authorized signatures)");
}
```

### Policy 4 — audit log

Even when verification fails, record every outcome for forensics:

```ts [policy-audit.ts]
const outcomes = await verifyMultiAll(jws, keyLookup);

for (const o of outcomes) {
  await audit.log({
    txId: (o.verified ? o.payload : {}).txId,
    signerIndex: o.signerIndex,
    signerKid: o.protectedHeader?.kid,
    verified: o.verified,
    error: o.verified ? null : o.error.code,
  });
}
```

## Lightweight — first valid signature

If you don't care which of the three signed, only that *some* authorized key signed, [`verifyMulti`](/jwt/jws/multi-signature#verifymulti) is enough:

```ts [policy-any.ts]
import { verifyMulti } from "unjwt/jws";

const allPublicKeys = {
  keys: [userKey.publicKey, serviceKey.publicKey, notaryKey.publicKey],
};

const { payload, signerIndex } = await verifyMulti(jws, allPublicKeys);
// Returns on first valid signature — others are not attempted
```

## Other use cases

The same pattern scales to:

- **Contract co-signing.** A lease signed by tenant, landlord, and guarantor. All three must verify for the contract to be valid.
- **Key rotation overlap.** During a rotation window, sign with both the old and new key. Either verifies; callers choose.
- **Hybrid classical + post-quantum.** Sign with a classical scheme (e.g. Ed25519) and a PQC scheme. Systems that support PQC check that signature; older systems use the classical one.
- **Signed audit trails.** Append-only structures where each new entry is signed by a monotonically larger set of signers.

## See also

- [JWS multi-signature →](/jwt/jws/multi-signature)
- [JWK Sets →](/jwk/jwk-sets) — for managing many signer keys.
- [Authentication basics →](/examples/authentication-basics) — the single-signer case.
