In v0.1, audit_signature was a bare "sha256:…" string: a hash of the artifact with no metadata about what was signed, who signed it, or how to verify it. OCG v0.2 replaces this with a DSSE in-toto envelope: a self-describing structure that carries the payloadType, a base64url-encoded canonical payload, and a signatures[] array with named key IDs and Ed25519 signatures. This guide shows you exactly how the envelope is constructed, how to produce and verify it, and how it unlocks SLSA Level 2 attestation and Sigstore compatibility described in Guide #2.
audit_signatureA bare string like "sha256:a4c2f8e1b7d93…" tells a verifier almost nothing useful. It confirms that some hash was computed at some point, but it cannot answer the fundamental questions that an auditor or counterparty must ask:
The DSSE in-toto envelope answers all four questions in a single self-contained structure. It is the difference between a signature and an attestation: the envelope carries everything needed for independent verification, with no out-of-band information required.
Dead Simple Signing Envelope (DSSE) is the serialization format used by in-toto v1 attestations. It was designed as a minimal, unambiguous wrapper that avoids the pitfalls of earlier signing formats (JWS, PGP armored, etc.). The full spec is at github.com/secure-systems-lab/dsse. OCG v0.3 uses DSSE as the outer envelope and in-toto Statement as the inner structure, the same combination used by Sigstore's Cosign and the SLSA GitHub Generator.
audit_signature envelope structureThe audit_signature field in an OCG v0.3 artifact is a DSSE in-toto envelope with three top-level keys. Here is the complete structure, annotated:
payload contains. For OpenChainGraph artifacts the value is always "application/vnd.openchain.graph+json;version=0.2". A verifier checks this first: if the payloadType is not recognized, it must reject the envelope rather than attempt to parse the payload blindly.audit_signature itself, with keys sorted recursively. The Ed25519 signature in signatures[] is computed over the DSSE Pre-Authentication Encoding (PAE) of this payload, not the raw artifact JSON. See §4 for the exact PAE construction.keyid, a stable fingerprint identifying the signer's Ed25519 public key (SHA-256 of the raw 32-byte public key, hex-encoded); and sig, the base64url-encoded 64-byte Ed25519 signature over the PAE. Multiple entries allow co-signing by multiple vendors or key rotation without invalidating prior signatures.A complete OCG v0.3 artifact with a populated audit_signature envelope looks like this:
{
"@context": "https://ainumbers.co/chaingraph/context/v0.3",
"tool_id": "var_frtb_ima_calculator_v1",
"mandate_type": "market_risk",
"policy_parameters": {
"confidence_level": 0.99,
"holding_period_days": 10,
"portfolio_id": "FX-DESK-Q3"
},
"output_payload": {
"var_10d_99": 2847300,
"es_97_5": 3190500,
"breaches_250d": 4
},
"execution_hash": "sha256:f3a91e4b2c87d650...",
"buildType": "https://ainumbers.co/chaingraph/context/v0.2#WebCryptoSHA256",
"chain": {
"parent_hashes": ["sha256:a4c2f8e1b7d93..."],
"depth": 1,
"chain_name": "q3_market_risk_review"
},
"audit_signature": {
"payloadType": "application/vnd.openchain.graph+json;version=0.2",
"payload": "eyJAY29udGV4dCI6Imh0dHBzOi8vb3Bl...", // base64url of canonical artifact body
"signatures": [
{
"keyid": "sha256:8f3a2b1c94e7d60f5a8e...", // fingerprint of signing Ed25519 pubkey
"sig": "ZXhhbXBsZXNpZ25hdHVyZWJ5dGVzNjQ..." // base64url Ed25519 sig over PAE(payloadType, payload)
}
]
}
}
When constructing the payload bytes that get signed, the audit_signature field itself is stripped from the artifact before serialization. This avoids a circular dependency: the signature cannot be part of what is signed. The signing procedure is: strip audit_signature → sort keys recursively → JSON.stringify → UTF-8 encode → base64url encode → PAE wrap → Ed25519 sign. The execution_hash field is retained in the signed payload, so the hash integrity and the signature authenticity are independently verifiable but cryptographically linked.
In OCG v0.1, audit_signature held a bare SHA-256 hash string, identical in format to execution_hash. The v0.2 upgrade is a structural replacement, not a field rename. The table below shows exactly what moved and why:
| Property | OCG v0.1 (bare hash) | OCG v0.2 (DSSE envelope) |
|---|---|---|
| Type | String: "sha256:…" |
Object: { payloadType, payload, signatures[] } |
| What is signed? | Implicit: not declared. Verifiers had to guess. | Explicit: payload is the canonical artifact body, base64url-encoded |
| Signing algorithm | Not declared. Assumed SHA-256 hash, no asymmetric signature. | Ed25519, declared via keyid fingerprint in signatures[] |
| Multi-signer support | No: one hash, no key identity | Yes: signatures[] is an array; any entry verified by its keyid |
| Standard compatibility | None: proprietary format | DSSE / in-toto v1 / Sigstore Cosign / SLSA Level 2 |
| Independent verifiability | Requires contacting vendor to know what hash means | Self-describing: payloadType + payload + public key → verify offline |
| Key rotation | Breaks all prior signatures | Add new entry to signatures[]; old signatures remain valid |
OCG v0.1 artifacts with a bare string audit_signature remain valid OCG v0.1 artifacts. They do not satisfy OCG v0.2 conformance. Tools that must process both versions should inspect the @context field: "https://ainumbers.co/chaingraph/context/v0.2" guarantees the envelope structure; v0.1 does not. Do not attempt to parse a bare hash string as a DSSE envelope; treat it as an unverified legacy field.
Ed25519 signs a byte sequence, not a JSON object. DSSE specifies a deterministic encoding called Pre-Authentication Encoding (PAE) that constructs this byte sequence from payloadType and payload. Understanding PAE is necessary for independent verification: any verifier that re-constructs PAE incorrectly will reject valid signatures.
The PAE construction for an OCG envelope is:
PAE(type, body) =
"DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
Where:
SP = 0x20 (ASCII space)
LEN(x) = ASCII decimal encoding of len(x) in bytes
type = UTF-8 encoded payloadType string
body = raw bytes of the base64url-decoded payload
Example for an OCG artifact:
PAE("application/vnd.openchain.graph+json;version=0.2", <canonical_bytes>)
= "DSSEv1 46 application/vnd.openchain.graph+json;version=0.2 <N> <canonical_bytes>"
where 46 = len("application/vnd.openchain.graph+json;version=0.2")
and N = byte length of the canonical artifact JSON
The Ed25519 signature in signatures[].sig is always over the PAE byte sequence, never over the raw JSON or the base64url-encoded string. This is a common source of implementation errors: implementations that sign the base64url string rather than the decoded bytes will produce signatures that fail DSSE-compliant verifiers.
// Construct the PAE byte sequence from payloadType and canonical payload bytes function buildPae(payloadType, payloadBytes) { const enc = new TextEncoder(); const typeBytes = enc.encode(payloadType); const typeLen = enc.encode(String(typeBytes.length)); const bodyLen = enc.encode(String(payloadBytes.length)); const prefix = enc.encode("DSSEv1 "); const sp = enc.encode(" "); // Concatenate: "DSSEv1 " + typeLen + " " + typeBytes + " " + bodyLen + " " + payloadBytes const total = prefix.length + typeLen.length + 1 + typeBytes.length + 1 + bodyLen.length + 1 + payloadBytes.length; const pae = new Uint8Array(total); let offset = 0; for (const chunk of [prefix, typeLen, sp, typeBytes, sp, bodyLen, sp, payloadBytes]) { pae.set(chunk, offset); offset += chunk.length; } return pae; } // Canonical payload: artifact minus audit_signature, keys sorted, UTF-8 function canonicalize(artifact) { const body = { ...artifact }; delete body.audit_signature; return new TextEncoder().encode(sortedStringify(body)); } function sortedStringify(obj) { if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) { return JSON.stringify(obj); } const sorted = {}; for (const key of Object.keys(obj).sort()) { sorted[key] = JSON.parse(sortedStringify(obj[key])); } return JSON.stringify(sorted); }
Signing raw JSON directly is ambiguous: two JSON strings can be semantically identical but byte-different (whitespace, key order, Unicode escaping). PAE + canonical JSON removes that ambiguity: the exact byte sequence is deterministic regardless of which JSON library produced it. This is what allows any third party to independently verify an OCG signature without access to the tool that produced it.
audit_signature envelopeThe following function takes a conformant OCG v0.3 artifact (without audit_signature) and an Ed25519 CryptoKeyPair, and returns the complete envelope ready to attach as artifact.audit_signature. This runs entirely in the browser via the WebCrypto API: no server, no dependencies.
/** * Produce a DSSE in-toto audit_signature envelope for an OCG v0.3 artifact. * * @param {object} artifact - OCG artifact object (must NOT already have audit_signature) * @param {CryptoKey} privateKey - Ed25519 private key (WebCrypto CryptoKey, "sign" usage) * @param {CryptoKey} publicKey - Ed25519 public key (for keyid derivation) * @returns {Promise<object>} - The envelope object to assign to artifact.audit_signature */ async function produceEnvelope(artifact, privateKey, publicKey) { const PAYLOAD_TYPE = "application/vnd.openchain.graph+json;version=0.2"; // 1. Canonical payload bytes (artifact without audit_signature, keys sorted) const payloadBytes = canonicalize(artifact); // 2. base64url-encode the payload (no padding) const payloadB64 = base64urlEncode(payloadBytes); // 3. Construct PAE byte sequence const pae = buildPae(PAYLOAD_TYPE, payloadBytes); // 4. Sign PAE with Ed25519 private key const sigBytes = new Uint8Array( await crypto.subtle.sign("Ed25519", privateKey, pae) ); // 5. Derive keyid: SHA-256 of raw 32-byte public key, hex-encoded const pubKeyRaw = new Uint8Array(await crypto.subtle.exportKey("raw", publicKey)); const keyidHash = new Uint8Array(await crypto.subtle.digest("SHA-256", pubKeyRaw)); const keyid = "sha256:" + Array.from(keyidHash).map(b => b.toString(16).padStart(2, "0")).join(""); // 6. Return the complete envelope return { payloadType: PAYLOAD_TYPE, payload: payloadB64, signatures: [{ keyid: keyid, sig: base64urlEncode(sigBytes) }] }; } // Utility: base64url encode (RFC 4648 §5, no padding) function base64urlEncode(bytes) { return btoa(String.fromCharCode(...bytes)) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); } // Usage const keyPair = await crypto.subtle.generateKey( { name: "Ed25519" }, true, ["sign", "verify"] ); const envelope = await produceEnvelope(artifact, keyPair.privateKey, keyPair.publicKey); artifact.audit_signature = envelope;
audit_signature envelopeIndependent verification requires only the artifact and the signer's Ed25519 public key. No contact with the vendor, no network call, no server. The verification procedure is the inverse of §5: decode the payload, re-construct PAE, verify the Ed25519 signature, and confirm the decoded payload matches the artifact body.
/** * Verify the audit_signature envelope of an OCG v0.3 artifact. * * @param {object} artifact - Complete OCG artifact including audit_signature * @param {CryptoKey} publicKey - Ed25519 public key of the expected signer * @returns {Promise<{valid: boolean, keyid: string, payloadMatch: boolean, details: object}>} */ async function verifyEnvelope(artifact, publicKey) { const env = artifact.audit_signature; if (!env || !env.payloadType || !env.payload || !env.signatures?.length) { return { valid: false, details: { error: "Missing or malformed envelope" } }; } // 1. Check payloadType if (env.payloadType !== "application/vnd.openchain.graph+json;version=0.2") { return { valid: false, details: { error: "Unexpected payloadType: " + env.payloadType } }; } // 2. Decode payload bytes from base64url const payloadBytes = base64urlDecode(env.payload); // 3. Reconstruct PAE const pae = buildPae(env.payloadType, payloadBytes); // 4. Derive expected keyid from supplied public key const pubKeyRaw = new Uint8Array(await crypto.subtle.exportKey("raw", publicKey)); const keyidHash = new Uint8Array(await crypto.subtle.digest("SHA-256", pubKeyRaw)); const expectedKeyid = "sha256:" + Array.from(keyidHash).map(b => b.toString(16).padStart(2, "0")).join(""); // 5. Find matching signature entry const sigEntry = env.signatures.find(s => s.keyid === expectedKeyid); if (!sigEntry) { return { valid: false, keyid: expectedKeyid, details: { error: "No signature entry matches supplied public key" } }; } // 6. Verify Ed25519 signature over PAE const sigBytes = base64urlDecode(sigEntry.sig); const sigValid = await crypto.subtle.verify("Ed25519", publicKey, sigBytes, pae); // 7. Confirm decoded payload matches artifact body (without audit_signature) const expectedBytes = canonicalize(artifact); const payloadMatch = bytesEqual(payloadBytes, expectedBytes); return { valid: sigValid && payloadMatch, keyid: expectedKeyid, payloadMatch: payloadMatch, sigValid: sigValid, details: { payloadLengthBytes: payloadBytes.length, sigLengthBytes: sigBytes.length } }; } // Utility: base64url decode function base64urlDecode(str) { const b64 = str.replace(/-/g, "+").replace(/_/g, "/").padEnd(str.length + ((4 - str.length % 4) % 4), "="); return Uint8Array.from(atob(b64), c => c.charCodeAt(0)); } function bytesEqual(a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; } // Usage const result = await verifyEnvelope(artifact, vendorPublicKey); console.log(result.valid ? "✓ Valid signature" : "✗ Invalid: " + JSON.stringify(result.details));
sigValid and payloadMatch are checked separately and both must be true for valid to be true. A valid signature over a payload that doesn't match the artifact body indicates the envelope was produced from a different version of the artifact: a tampered or substituted artifact. A payload match with an invalid signature indicates the envelope's signing key is wrong or the artifact was re-signed after the payload was set. Both failures are significant and should be surfaced to the auditor with separate error codes.
For server-side audit systems, compliance platforms, and CI pipelines that ingest OCG artifacts, a Python implementation of the verification procedure is provided. This uses only the standard library's hashlib and base64 modules, plus cryptography (PyCA) for Ed25519.
import json, hashlib, base64 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey def sorted_stringify(obj): """Deterministic canonical JSON — sort keys recursively.""" if not isinstance(obj, dict): return json.dumps(obj, separators=(',', ':')) sorted_obj = {k: json.loads(sorted_stringify(obj[k])) for k in sorted(obj)} return json.dumps(sorted_obj, separators=(',', ':')) def build_pae(payload_type: str, payload_bytes: bytes) -> bytes: """Construct PAE(type, body) per DSSE spec.""" t = payload_type.encode() return ( b"DSSEv1 " + str(len(t)).encode() + b" " + t + b" " + str(len(payload_bytes)).encode() + b" " + payload_bytes ) def verify_ocg_envelope(artifact: dict, public_key: Ed25519PublicKey) -> dict: """ Verify the audit_signature DSSE envelope of an OCG v0.3 artifact. Returns dict with keys: valid, sig_valid, payload_match, keyid, error (if any). """ env = artifact.get("audit_signature", {}) if not env or env.get("payloadType") != "application/vnd.openchain.graph+json;version=0.2": return {"valid": False, "error": "Missing or wrong payloadType"} # 1. Decode payload payload_bytes = base64.urlsafe_b64decode(env["payload"] + "==") # 2. Build PAE pae = build_pae(env["payloadType"], payload_bytes) # 3. Derive expected keyid from public key pub_raw = public_key.public_bytes_raw() keyid = "sha256:" + hashlib.sha256(pub_raw).hexdigest() # 4. Find matching signature entry sig_entry = next((s for s in env.get("signatures", []) if s["keyid"] == keyid), None) if not sig_entry: return {"valid": False, "keyid": keyid, "error": "No matching keyid in signatures[]"} # 5. Verify Ed25519 signature sig_bytes = base64.urlsafe_b64decode(sig_entry["sig"] + "==") sig_valid = False try: public_key.verify(sig_bytes, pae) sig_valid = True except Exception: pass # 6. Confirm payload matches artifact body (without audit_signature) body = {k: v for k, v in artifact.items() if k != "audit_signature"} expected_bytes = sorted_stringify(body).encode() payload_match = (payload_bytes == expected_bytes) return { "valid": sig_valid and payload_match, "sig_valid": sig_valid, "payload_match": payload_match, "keyid": keyid, } # Example usage import json artifact = json.load(open("artifact.json")) # public_key loaded from PEM or raw bytes via cryptography library result = verify_ocg_envelope(artifact, public_key) print("✓ Valid" if result["valid"] else f"✗ Invalid: {result}")
Because signatures[] is an array, the envelope natively supports two important operational patterns: co-signing and key rotation. Both are common in regulated environments where dual-control signatures or periodic key replacement are compliance requirements.
| Pattern | How it works | Verification behavior |
|---|---|---|
| Co-signing | Two or more signers (e.g., vendor and auditor) each produce an Ed25519 signature over the same PAE. Both entries are added to signatures[], each with their own keyid. |
Verifier checks each entry independently. A dual-control policy can require both to be valid. A threshold policy can require any N of M. |
| Key rotation | Old key is retired; new key re-signs the same payload. Both signatures[] entries can coexist: old keyid (for historical audit trail) and new keyid (for current trust anchor). |
Verifiers holding the new public key verify the new entry. Verifiers holding the old key can still verify the old entry. No information is lost. |
| Additive signing | A downstream processor (e.g., a clearing system) adds its own signature to an artifact it has validated and is forwarding. It appends to signatures[] without modifying existing entries. |
Each entry is independently verifiable. The original vendor signature remains valid alongside the downstream co-signature. |
/** * Add a co-signer's signature to an existing audit_signature envelope. * The original signatures[] entries are preserved unchanged. */ async function coSign(artifact, coSignerPrivateKey, coSignerPublicKey) { const env = artifact.audit_signature; if (!env) throw new Error("No envelope to co-sign"); // Payload bytes are already encoded in the envelope — decode and re-sign const payloadBytes = base64urlDecode(env.payload); const pae = buildPae(env.payloadType, payloadBytes); const sigBytes = new Uint8Array(await crypto.subtle.sign("Ed25519", coSignerPrivateKey, pae)); const pubKeyRaw = new Uint8Array(await crypto.subtle.exportKey("raw", coSignerPublicKey)); const keyidHash = new Uint8Array(await crypto.subtle.digest("SHA-256", pubKeyRaw)); const keyid = "sha256:" + Array.from(keyidHash).map(b => b.toString(16).padStart(2, "0")).join(""); // Append — do not replace existing signatures env.signatures.push({ keyid, sig: base64urlEncode(sigBytes) }); return artifact; }
When co-signing, always re-use the payload already in the envelope. Do not re-canonicalize the artifact body and replace payload. If the payload bytes change, even by a single bit, all prior signatures[] entries will become invalid. The co-signer is attesting to the same artifact the original signer attested to; the shared payload makes that binding explicit.
The in-toto envelope format used by OCG v0.3 is identical to the format consumed by Cosign (Sigstore's signing tool) and Rekor (Sigstore's transparency log). An OCG artifact's audit_signature envelope can be submitted to Rekor directly, making the artifact's provenance tamper-evident and timestamped in an append-only public log.
The full end-to-end flow from artifact to Rekor entry is described in Guide #2 (buildType / SLSA, §7). What is specific to this guide is the extraction step: extracting the audit_signature envelope into a standalone in-toto bundle file that Cosign can consume.
/** * Extract the audit_signature envelope as a cosign-compatible in-toto bundle. * The resulting file can be passed to `cosign attest --predicate` or * submitted to Rekor as a `hashedrekord` entry. */ function extractIntotoBundle(artifact) { const env = artifact.audit_signature; if (!env) throw new Error("No audit_signature envelope present"); // in-toto v1 Statement wrapped around the envelope return { "_type": "https://in-toto.io/Statement/v1", "predicateType": "application/vnd.openchain.graph+json;version=0.2", "subject": [{ "name": artifact.tool_id, "digest": { "sha256": artifact.execution_hash.replace(/^sha256:/, "") } }], "predicate": { "envelope": env, "buildType": artifact.buildType, "toolId": artifact.tool_id, "mandateType": artifact.mandate_type, "chainDepth": artifact.chain?.depth ?? 0 } }; } // Write to file for cosign / Rekor submission const bundle = extractIntotoBundle(artifact); const bundleJSON = JSON.stringify(bundle, null, 2); // Node: fs.writeFileSync("bundle.intoto.json", bundleJSON) // Browser: trigger download via Blob/createObjectURL
# Submit the extracted bundle as a Rekor hashedrekord entry rekor-cli upload \ --type intoto \ --artifact bundle.intoto.json \ --pki-format x509 \ --public-key cosign.pub # Verify via cosign cosign verify-attestation \ --type intoto \ --certificate-identity-regexp ".*ainumbers.co.*" \ --rekor-url https://rekor.sigstore.dev \ "${ARTIFACT_URI}" # Or: verify the local file directly against a known public key cosign verify-blob \ --key cosign.pub \ --signature <(jq -r '.predicate.envelope.signatures[0].sig' bundle.intoto.json | base64 -d) \ bundle.intoto.json
Once an OCG artifact's audit_signature envelope has been submitted to Rekor and the Rekor entry UUID has been recorded, the artifact satisfies SLSA Level 2: the provenance is both present (Level 1, satisfied by buildType + execution_hash) and authenticated: the Ed25519 signature in the envelope, verified by a third-party transparency log, constitutes authenticated provenance not controlled solely by the vendor. See Guide #2 for the full SLSA level mapping.
Seven steps to integrate OCG v0.3 in-toto envelopes into your audit and attestation pipeline. Each is independently verifiable.
| Step | Action | Verified by |
|---|---|---|
| 1 | Confirm the artifact has @context set to "https://ainumbers.co/chaingraph/context/v0.3"; the envelope structure is only guaranteed in v0.3 |
artifact["@context"].endsWith("v0.3") returns true |
| 2 | Check that audit_signature is an object, not a string. A string indicates a v0.1 artifact; see §3 for migration notes |
typeof artifact.audit_signature === "object" |
| 3 | Confirm payloadType is exactly "application/vnd.openchain.graph+json;version=0.2"; reject unknown types rather than attempting to parse them |
audit_signature.payloadType === "application/vnd.openchain.graph+json;version=0.2" |
| 4 | Run the verification function from §6 (JS) or §7 (Python) with the vendor's published Ed25519 public key. Both sigValid and payloadMatch must be true |
result.valid === true |
| 5 | Cross-check execution_hash independently using the buildType procedure from Guide #2 §6. The hash must match the subject[].digest.sha256 in your extracted in-toto bundle |
verifyOcgArtifact(artifact).valid === true |
| 6 | If submitting to Rekor: extract the in-toto bundle using extractIntotoBundle() from §9, sign with Cosign, and record the Rekor UUID alongside the artifact in your audit log |
rekor-cli get --uuid <uuid> returns the entry; body matches the submitted bundle |
| 7 | For multi-signer flows (co-signing, key rotation): verify each signatures[] entry independently against its keyid. Apply your organization's threshold or dual-control policy on top of the individual signature results |
Each entry with a known keyid returns sigValid: true when verified against the matching public key |
Independent verification of an OCG v0.3 in-toto envelope requires exactly: the artifact JSON, and the signer's Ed25519 public key. You do not need the OCG specification document, the vendor's server, a network connection, the original tool, or any OCG-specific library. The PAE construction, base64url decoding, and Ed25519 verification are all implemented in browser WebCrypto, Python's cryptography library, and every other major cryptographic toolkit. The envelope was designed to be verifiable by anyone, with no proprietary dependency.