Content Credentials

C2PA 2.1 content provenance — sign assets with a manifest declaring origin, edits, AI involvement.

California SB-942 (Cal. Bus. & Prof. Code §22757) + AB-853, effective 2026-08-02, require generative-AI providers to embed a latent disclosure carrying provider name, system identifier, system version, content timestamp, and a unique content ID in every AI-generated image / video / audio asset distributed in California. SB-942 names C2PA as an acceptable format.

The framework can't push bytes into format-specific muxers (JPEG XMP / PNG iTXt / MP4 boxes vary per codec). What it does ship: build a C2PA-shaped manifest with the SB-942 required fields, sign it with the audit-sign keypair (ML-DSA-87 by default), record a tamper-evident audit row, and verify inbound manifests on the receive side. Operators hand the signed manifest to their format-specific embedder.

b.contentCredentials.build(opts) #

0.8.44
{
  provider:          string,             // e.g. "Acme AI Inc."
  providerContact:   string,             // optional contact URL
  system:            string,             // model id, e.g. "acme-image-v3"
  systemVersion:     string,             // semver
  contentId:         string,             // unique per asset
  contentType:       string,             // IANA media type (optional)
  contentSha3:       string,             // SHA3-512 hex (optional)
  generatedAt:       number,             // ms epoch (optional)
  visibleDisclosure: string,             // operator display text (optional)
}

Build an unsigned C2PA 2.1-shaped manifest carrying the SB-942 §22757(a) required fields (provider, system, system version, content ID) plus optional content type, SHA3-512 digest, and a visible-disclosure string. Returns a frozen object so downstream code can't mutate the claims before signing. generatedAt defaults to Date.now() so the manifest carries a real timestamp unless the operator pins one for testing.

var manifest = b.contentCredentials.build({
  provider:      "Acme AI Inc.",
  system:        "acme-image-v3",
  systemVersion: "3.2.1",
  contentId:     "img-2026-05-08-abc123",
  contentType:   "image/png",
  generatedAt:   Date.UTC(2026, 4, 8),
});
manifest.aiGenerated;        // → true
manifest.system.id;          // → "acme-image-v3"
manifest.content.id;         // → "img-2026-05-08-abc123"

b.contentCredentials.required(opts) #

0.8.44
{
  provider:      string,                 // required
  system:        string,                 // required
  systemVersion: string,                 // required
  contentId:     string,                 // required
}

Pre-flight check that returns the list of SB-942 §22757(a) fields missing from a candidate input — useful for operator UIs that surface "what's needed before we can disclose" without round- tripping through build and catching the throw. Returns [] when every required field is present and non-empty.

b.contentCredentials.required({
  provider:      "Acme AI Inc.",
  system:        "acme-image-v3",
  systemVersion: "3.2.1",
  contentId:     "img-001",
});
// → []

b.contentCredentials.required({ provider: "Acme AI Inc." });
// → ["missing-system", "missing-systemVersion", "missing-contentId"]

b.contentCredentials.sign(manifest, opts) #

0.8.44
{
  privateKeyPem: string,                 // PEM-encoded signing key
  audit:         boolean,                // default true
}

Canonicalize the manifest (RFC 8785 JCS via b.canonicalJson) and sign it with b.crypto.sign using the operator's private-key PEM — typically the ML-DSA-87 audit-sign keypair. Returns an envelope with the original manifest plus the base64-encoded signature. Audits the disclosure under contentcredentials.signed unless the caller passes audit:false.

var pair = b.crypto.generateSigningKeyPair("ml-dsa-87");
var manifest = b.contentCredentials.build({
  provider:      "Acme AI Inc.",
  system:        "acme-image-v3",
  systemVersion: "3.2.1",
  contentId:     "img-2026-05-08-abc123",
});
var envelope = b.contentCredentials.sign(manifest, {
  privateKeyPem: pair.privateKey,
});
typeof envelope.signature;   // → "string"

b.contentCredentials.verify(envelope, publicKeyPem, opts) #

0.8.44
{
  audit: boolean,                        // default true
}

Verify a signed envelope produced by sign. Re-canonicalizes the manifest, checks the signature with b.crypto.verify against the operator-supplied public-key PEM, and re-runs the SB-942 required- field presence check on the verified claims so a manifest with a valid signature but missing fields fails closed. Never throws — returns { valid, claims, reason }. Audits successful verifications under contentcredentials.verified unless audit:false.

var pair = b.crypto.generateSigningKeyPair("ml-dsa-87");
var manifest = b.contentCredentials.build({
  provider:      "Acme AI Inc.",
  system:        "acme-image-v3",
  systemVersion: "3.2.1",
  contentId:     "img-001",
});
var envelope = b.contentCredentials.sign(manifest, {
  privateKeyPem: pair.privateKey,
});
var result = b.contentCredentials.verify(envelope, pair.publicKey);
result.valid;   // → true

b.contentCredentials.signCose(manifest, opts) #

stable0.8.77soc2
{
  {
    privateKeyPem: string,            // required
    alg?:          "ed25519" | "es256" | "es384" | "es512" |
                   "ml-dsa-44" | "ml-dsa-65" | "ml-dsa-87" |
                   "slh-dsa-shake-256f",   // default "ml-dsa-87"
    certChain?:    Buffer[],          // X.509 DER buffers; emitted as x5chain (header label 33)
    timestamp?:    {                  // RFC 3161 sigTst2 countersignature (default ON when present)
      token?:           Buffer,       //   a DER TimeStampToken to attach (mode a)
      signature?:       string,       //   the base64 the request call returned — pins the
                                      //   randomized COSE signature so the imprint matches
      trustAnchorsPem?: string|string[], // anchors echoed for later verifyCose
      hashAlg?:         string,       //   default "SHA-512"; one of b.tsa.IMPRINT_HASHES
    } | false,                        // false = explicit opt-out (requires timestampOptOutReason)
    timestampOptOutReason?: string,   // required when timestamp:false — audited
    audit?:        boolean,           // default true
  }
}

C2PA 2.x interop sign — wraps the manifest in a COSE_Sign1 CBOR envelope (RFC 9052) so the result interops with c2patool / JPEG Trust / Adobe / external C2PA verifiers. The simpler sign() primitive ships a blamejs-internal envelope shape; this one ships COSE bytes.

An RFC 3161 timestamp countersignature (sigTst2, RFC 9921 / C2PA 2.x Technical Spec) is attached when an opts.timestamp context is present, proving the manifest was signed before the timestamp authority's asserted time. The countersignature imprint is computed over the CounterSignature ToBeSigned — [ "CounterSignature", body_protected, sign_protected (empty), external_aad (empty), payload, other (= the COSE_Sign1 signature) ] — hashed with timestamp.hashAlg (default SHA-512; the allowed set is b.tsa.IMPRINT_HASHES), and the digest is handed to b.tsa.buildRequest as a PRE-HASHED imprint ({ hashed: true, hashAlg }). Two modes: pass timestamp.token (a DER TimeStampToken already obtained from a TSA) to attach it directly, or omit it to get back a timestampRequest (the DER bytes to POST as application/timestamp-query, the nonce to keep, and the ToBeSigned imprint) so the operator can fetch a token and re-call with it. Once attached, the token sits under COSE unprotected-header label 35 (sigTst2) alongside the x5chain (label 33).

Timestamping is fail-closed: when no TSA context is supplied the call does NOT silently emit an un-timestamped signature — set timestamp: false with timestampOptOutReason to record an audited, deliberate opt-out. An un-timestamped C2PA claim is vulnerable to the key-compromise backdating class ([CVE-2025-52556](https://nvd.nist.gov/vuln/detail/CVE-2025-52556), timestamp-validation bypass) — opting out is an operator decision, not a default.

Returns { manifest, coseSign1: Buffer, alg, timestamped, timestampRequest? }. Operators embed the coseSign1 Buffer in the image's C2PA box (JPEG XT marker, PNG iTXt chunk, MP4 'jumb' box per C2PA §13).

var pair = b.crypto.generateSigningKeyPair("ml-dsa-87");
var manifest = b.contentCredentials.build({
  provider: "Acme AI", system: "acme-v3",
  systemVersion: "3.2.1", contentId: "img-001",
});
// Request-builder mode: get the TSA query bytes to POST.
var req = b.contentCredentials.signCose(manifest, {
  privateKeyPem: pair.privateKey, alg: "ml-dsa-87", timestamp: {},
});
// POST req.timestampRequest.der to the TSA, then re-call, re-supplying
// the same signature so the countersigned imprint still matches:
var cose = b.contentCredentials.signCose(manifest, {
  privateKeyPem: pair.privateKey, alg: "ml-dsa-87",
  timestamp: { token: tsaTokenDer, signature: req.timestampRequest.signature },
});
// cose.coseSign1 is the CBOR bytes to embed in the image's C2PA box.

b.contentCredentials.verifyCose(coseSign1, publicKeyPem, opts) #

stable0.14.11soc2
{
  {
    requireTimestamp?:          boolean,           // default true — refuse a token-less COSE_Sign1
    timestampHashAlg?:          string,            // default "SHA-512"; one of b.tsa.IMPRINT_HASHES
    timestampTrustAnchorsPem?:  string|string[],   // anchors → b.tsa.verifyToken chain check
    timestampNonce?:            Buffer,            // require the token nonce to match
    audit?:                     boolean,           // default true
  }
}

Verify a COSE_Sign1 produced by signCose and, when present, its RFC 3161 sigTst2 timestamp countersignature. The COSE_Sign1 bytes are decoded through the bounded b.cbor codec; the Sig_structure (RFC 9052 §4.4) is reconstructed and the signature verified with b.crypto.verify against the operator-supplied public-key PEM. When a timestamp token sits under unprotected-header label 35, its imprint is recomputed over the CounterSignature ToBeSigned with the same hashAlg (default SHA-512; the allowed set is b.tsa.IMPRINT_HASHES) and the digest is handed to b.tsa.verifyToken as a PRE-HASHED imprint ({ hash, hashAlg }).

The ONLY timestamp-verification path is b.tsa.verifyToken, which performs the full RFC 3161 §2.4.2 / §2.3 check — the CMS signature over the signed attributes, the messageDigest recompute, and the critical, sole id-kp-timeStamping EKU — NOT a chain-only shortcut. A chain-only timestamp check is the [CVE-2025-52556](https://nvd.nist.gov/vuln/detail/CVE-2025-52556) / [CWE-347](https://cwe.mitre.org/data/definitions/347.html) improper- signature-verification class and is never done here. b.tsa.verifyToken throws on every failure; this primitive wraps that call and converts a thrown TsaError into { timestamp: { valid: false, reason } } so verifyCose NEVER throws — it returns { valid, reason, claims, alg, timestamp } fail-closed.

opts.requireTimestamp (default true) fails closed when the COSE_Sign1 carries no sigTst2 token; set it false only when the operator deliberately accepts un-timestamped claims (mirrors signCose's timestamp:false opt-out). opts.timestampTrustAnchorsPem enables the timestamp cert-chain + validity check inside b.tsa.verifyToken.

var res = b.contentCredentials.verifyCose(cose.coseSign1, pair.publicKey, {
  timestampTrustAnchorsPem: tsaRootPem,
});
res.valid;               // → true
res.timestamp.valid;     // → true
res.timestamp.genTime;   // → Date (the TSA-asserted signing time)

b.contentCredentials.attachIdentityAssertion(opts) #

stable0.14.11soc2gdpr
{
  {
    binding:          "x509" | "identity-claims-aggregator",  // required
    subject:          object,         // required — the asserted identity fields (name, id, org, …)
    referencedAssertions: object[],   // required — the manifest assertions this identity binds
    privateKeyPem:    string,         // required — claim signing key
    audit:            boolean,        // default true
  }
}

Build a CAWG Identity Assertion v1.2 — a signed creator/organization identity bound to a C2PA manifest's other assertions. The signer_payload hash-binds each referenced assertion (a SHA3-512 digest over its RFC 8785 canonical form) so the identity statement cannot be transplanted onto a different manifest. Two binding paths: "x509" for a signed organization identity and "identity-claims-aggregator" for an individual whose claims an aggregator attests. The claim signature is produced with b.crypto.sign (ML-DSA-87 by default).

Self-asserted identity carries NO trust by itself — verification (verifyIdentityAssertion) only reports verified:true for an x509 binding when a trust anchor is supplied and the chain verifies. This matches the CAWG model: the assertion records a claim; trust comes from the verifier's anchors, never from the claim's own bytes.

var pair = b.crypto.generateSigningKeyPair("ml-dsa-87");
var ia = b.contentCredentials.attachIdentityAssertion({
  binding: "x509",
  subject: { name: "Acme Newsroom", org: "Acme Media", id: "acme-001" },
  referencedAssertions: [{ label: "c2pa.actions", data: { action: "c2pa.created" } }],
  privateKeyPem: pair.privateKey,
});
ia.signer_payload.referenced_assertions.length;   // → 1
typeof ia.signature;                               // → "string"

b.contentCredentials.verifyIdentityAssertion(assertion, publicKeyPem, opts) #

stable0.14.11soc2gdpr
{
  {
    referencedAssertions:    object[],          // required — re-confirm the hash-binding
    identityTrustAnchorsPem: string|string[],   // x509 leaf-chain anchors (enables verified:true)
    identityCertChainPem:    string|string[],   // x509 leaf + intermediates to chain-check
    audit:                   boolean,           // default true
  }
}

Verify a CAWG Identity Assertion v1.2 produced by attachIdentityAssertion. Re-canonicalizes the signer_payload, checks the claim signature with b.crypto.verify, and re-confirms the hash-binding of every referenced assertion the operator re-supplies in opts.referencedAssertions (so a valid signature over transplanted assertions still fails closed). Never throws — returns { valid, verified, binding, subject, reason }.

valid means the signature and assertion hash-binding check out. verified is stricter and applies the CAWG trust model: it is true ONLY for an x509 binding when opts.identityTrustAnchorsPem is supplied AND the leaf certificate chain verifies to a supplied anchor. A self-asserted identity (no anchor, or the identity-claims-aggregator path) is reported verified:false even when valid:true — self-asserted data never yields verified:true without a verified trust anchor ([CVE-2026-34677](https://nvd.nist.gov/vuln/detail/CVE-2026-34677), the unverified-identity-assertion trust-confusion class). Surfaced / audited identity fields pass through b.redact.redact (PII minimization).

var res = b.contentCredentials.verifyIdentityAssertion(ia, pair.publicKey, {
  referencedAssertions: [{ label: "c2pa.actions", data: { action: "c2pa.created" } }],
  identityTrustAnchorsPem: orgRootPem,
  identityCertChainPem:    orgLeafPem,
});
res.valid;      // → true  (signature + hash-binding)
res.verified;   // → true  (x509 leaf chained to a trusted anchor)

b.contentCredentials.cacImplicitLabel(opts) #

stable0.10.8cac-genai-label
{
  providerName:   string,         // UTF-8 ≤256 bytes
  providerCode:   string,         // 18-char 统一社会信用代码 (Chinese USCC)
  contentId:      string,         // globally-unique asset id
  contentKind:    string,         // "text"|"image"|"audio"|"video"|"virtual-scene"|"other"
  generatedAt:    string,         // ISO 8601 UTC
}

Build the GB 45438-2025 "Cybersecurity Technology — Labeling Method for Content Generated by Artificial Intelligence" implicit metadata block (effective 2025-09-01 per CAC Measures for Labeling AI- Generated Synthetic Content). The framework owns the implicit lane (metadata); the visible explicit label is application-layer rendering. Operators co-emit alongside the C2PA-COSE manifest by declaring cac-genai-label posture on b.contentCredentials.build.

var label = b.contentCredentials.cacImplicitLabel({
  providerName: "Example AI",
  providerCode: "91110000600037341A",
  contentId:    "asset-2026-05-17-abc123",
  contentKind:  "image",
  generatedAt:  "2026-05-17T20:00:00Z",
});
// → { aigcMarker: "AIGC", providerName, providerCode, contentId, contentKind, generatedAt }

b.contentCredentials.cacImplicitLabelRead(bytesOrObject) #

stable0.10.8cac-genai-label

Reverse parser for the GB 45438-2025 implicit label. Accepts either a Buffer / string containing the JSON-serialized block (as the sender embedded in XMP / EXIF / MP4-box / etc.) or the already- parsed object. Returns the validated label shape or throws on any field that fails the same gate cacImplicitLabel({...}) enforces.

var label = b.contentCredentials.cacImplicitLabelRead(jsonBuf);
// → { aigcMarker: "AIGC", providerName, providerCode, ... }

Last updated 2026-08-08T16:39:49.652Z by seeder.