BCP

Signing and identity

A signed manifest is the difference between 'someone parked some JSON at a URL' and 'the legal entity that owns this domain authored this manifest'. The signature envelope is a detached JWS over the canonicalised manifest body. The strongest signing primitive is a vLEI role credential, but the spec supports self-signed and DNS-pinned alternatives.

Updated today

Why sign

The threat model BCP signing addresses.

Without signatureWith signature
Anyone who can write to the well-known path can publish a manifest claiming any commerce primitives, MCP endpoints, allow-actions, etc.Only the holder of the signing private key can mint a valid manifest. Compromise of the web server alone is insufficient.
A man-in-the-middle on the CDN edge could rewrite the manifest before it reaches the consumer.The consumer verifies the signature on its end after fetching. Edge rewrites are detected.
No proof that the legal entity in identity.legalName is the same party as the manifest publisher.vLEI binding cryptographically chains the signing key to a GLEIF Level-1 LEI record.
No proof against the manifest being replayed from a stale snapshot.signedAt plus bcp:expires plus the manifest's bcp:updated timestamp together give monotonic freshness.
Note
BCP signing is not a substitute for transport security. HTTPS is still required. The signature defends against compromises that HTTPS does not cover: edge intermediaries, host take-overs that retain the cert, and stale-cache poisoning.

vLEI primer

Verifiable Legal Entity Identifier. The strongest signing primitive.

The vLEI standard (ISO 17442-3, published 2024) issues cryptographic credentials that bind a legal entity to a signing key, anchored in the GLEIF root of trust. Issuance happens through accredited Qualified vLEI Issuers (QVIs). The credential format is W3C Verifiable Credentials over KERI. ISO 5009 role codes (OOR) extend the model with credentials issued to individuals acting on behalf of an entity (CEO, CFO, authorised signatory).

For BCP, the vLEI gives us two things: a verifiable chain from the signing key to the LEI in the manifest's identity block, and a documented mechanism for credential revocation via GLEIF's witness mesh.

At a glance
Standard
GLEIF vLEI
Format
W3C Verifiable Credentials, KERI key event log, signed by accredited QVI.
Algorithms
Ed25519 native, ES256 supported.
Issuance cost
USD 100-500 / year via accredited QVI. Verification is free.
Active credentials (Q1 2026)
3.02M live LEIs, single-digit-thousands vLEIs. Adoption still early.
Revocation
KERI key event log entries. Witnesses publish revocations to GLEIF mesh.

Signature envelope

The JWS shape and how it sits inside the manifest.

Top-level block

manifest signature blockjson
1{
2 "signature": {
3 "alg": "ES256",
4 "kid": "vlei:5493000IBP32UQZ0KL24:ofcr:CEO:2026-Q2",
5 "signedAt": "2026-05-19T14:30:00Z",
6 "value": "eyJhbGciOiJFUzI1NiIsImtpZCI6Inps..."
7 }
8}

kid resolution table

kid prefixResolves toTrust level
vlei:<LEI>:ofcr:<role>:<period>GLEIF KERI witness mesh resolves the role credential.L3
dnskey:<domain>:<fp>Public key whose fingerprint matches DNS TXT _bcp.<domain>L2
selfsigned:<fp>Public key embedded in the manifest's identity.publicKey field.L1
x5u:<url>Certificate chain fetched from the x5u URL.L2 / L3 depending on chain trust anchor

JWS value

The value field is a JWS in detached-payload form. The protected header is included; the payload is empty (the canonicalised manifest body is the implicit payload). This shape is RFC 7797 (JWS Unencoded Payload Option) with the payload omitted from the wire format.

JWS structuretxt
1<base64url(protected header)>..<base64url(signature)>
2
3protected header (decoded):
4 {
5 "alg": "ES256",
6 "kid": "vlei:5493000IBP32UQZ0KL24:ofcr:CEO:2026-Q2",
7 "b64": false,
8 "crit": ["b64"]
9 }
10
11signature input:
12 utf8(protected header base64url) '.' JCS(manifest body without signature)

Canonicalisation

JCS over the manifest body. The signature block is excluded.

JSON Canonicalization Scheme (JCS, RFC 8785) gives the manifest body a deterministic byte representation. Both the signer and the verifier must arrive at the same byte string regardless of indentation, key order, or whitespace differences.

  1. 01
    Take the manifest body
    The full JSON object as stored or transmitted.
  2. 02
    Remove the signature block
    Delete the top-level signature property if present.
  3. 03
    Apply JCS
    Recursively sort keys lexicographically, normalise numbers per ECMAScript JSON.stringify, escape strings per RFC 8259, no insignificant whitespace.
  4. 04
    Hash and sign
    SHA-256 the JCS output for ES256. Sign with the private key. The base64url-encoded signature becomes the value.
  5. 05
    Insert signature block
    Add the signature back to the manifest with the freshly-computed value, alg, kid, and signedAt.
signing.ts (reference)ts
1import { canonicalize } from "@ultron/jcs"
2import { sign } from "@ultron/jose"
3
4export async function signManifest(
5 body: BcpManifest,
6 signingKey: KeyLike,
7 kid: string,
8): Promise<BcpManifest> {
9 const { signature: _omit, ...withoutSig } = body
10 const canonical = canonicalize(withoutSig)
11 const sig = await sign(canonical, signingKey, { alg: "ES256", kid })
12 return {
13 ...withoutSig,
14 signature: {
15 alg: "ES256",
16 kid,
17 signedAt: new Date().toISOString(),
18 value: sig,
19 },
20 }
21}

Key rotation

How to rotate signing keys without breaking consumers.

  1. 01
    Announce the next key
    Add a next-key tag to the DNS TXT record with the fingerprint of the upcoming key. Wait at least 24 hours (one TTL cycle) before the swap.
  2. 02
    Mint a transitional manifest
    Re-sign the manifest with the current key but include both fingerprints in DNS. Consumers that read DNS during this window will pre-trust the next key.
  3. 03
    Swap
    Replace the key tag with the new fingerprint. Re-sign all subsequent manifests with the new key.
  4. 04
    Keep next-key as old-key for revocation grace
    Once the swap completes, the previous key fingerprint remains in DNS as old-key for 7 days so any cached manifest signed by the old key still verifies until consumers re-fetch.
Warning
Never rotate without overlapping windows. A hard cutover guarantees that any consumer with a cached manifest cannot verify on next probe, which downgrades trust silently.

Fallback without vLEI

What to do when the publisher has no vLEI credential.

vLEI is not free and not universally adopted. The spec supports two fallbacks that still produce verifiable manifests at L1 or L2, without requiring a vLEI.

DNS-pinned self-signed key (L2)

The publisher generates an Ed25519 or P-256 key pair, publishes the fingerprint in_bcp.<domain> TXT, and signs manifests with the private key. Trust derives from control of the DNS zone. Most Ultron customers will be here.

X.509 chain (L2 or L3 depending on root)

The publisher signs with a key whose certificate chain is reachable via x5u. Useful for entities that already have an enterprise PKI, do not yet have a vLEI, but want machine-verifiable trust without standing up a new DNS workflow.

Verification flow

Consumer-side: how to verify a signature.

  1. 01
    Parse the signature block
    Extract alg, kid, signedAt, value. Reject the manifest if any are missing.
  2. 02
    Resolve the public key
    Use the kid prefix to dispatch: vlei: → query GLEIF, dnskey: → DNS TXT, selfsigned: → identity.publicKey, x5u: → fetch chain.
  3. 03
    Canonicalise the body
    Remove the signature block, apply JCS, compute the signature input.
  4. 04
    Verify the JWS
    Run JOSE verification with the resolved public key. Reject on failure.
  5. 05
    Check freshness
    Confirm signedAt is within an acceptable skew (default ±300s) and bcp:expires has not passed.
  6. 06
    Assign trust level
    Based on the kid prefix and DNS-binding state, assign L0-L3. Surface this to the calling agent.

Agent-issued credentials

Open question: how does an agent-formed entity sign its own manifest?

Unresolved
The vLEI standard assumes a human officer holds the role credential. An agent-formed corporation (the Manfred case from the early-2026 literature) has no human signatory. GLEIF has not standardised an agent-issued role credential. BCP v1 treats this as out of scope. v2 will add anautonomous primitive once GLEIF or FIDO ship a credential format for it.

For now, autonomous entities should sign at L1 with a self-signed key, declareidentity.autonomous: true as a vendor extension, and rely on operational reputation rather than chained identity. We expect to revisit this within a year as the underlying credential standards mature.