OpenChainGraph v0.3 · Integration How-To

Using OpenChainGraph with in-toto Envelopes

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.

§1

Why a bare hash is insufficient for audit_signature

A 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.

What is DSSE?

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.

§2

The OCG v0.3 audit_signature envelope structure

The 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:

DSSE in-toto Envelope · audit_signature
payloadType
A MIME-like string that identifies what 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.
payload
The canonical artifact body, base64url-encoded (RFC 4648 §5, no padding). This is the JSON object that contains all fields except 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.
signatures
An array of signature objects. Each entry has two fields: 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:

json OCG v0.3 artifact · full audit_signature envelope
{
  "@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)
      }
    ]
  }
}
audit_signature is excluded from the signed 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.

§3

What changed from v0.1

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
Backwards compatibility note

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.

§4

Pre-Authentication Encoding (PAE): what gets signed

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:

text PAE construction · DSSE spec (verbatim)
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.

javascript PAE construction in JavaScript · browser WebCrypto compatible
// 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);
}
Why PAE instead of signing the JSON directly?

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.

§5

Producing an audit_signature envelope

The 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.

javascript Produce a DSSE in-toto envelope · browser WebCrypto
/**
 * 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;
§6

Verifying an audit_signature envelope

Independent 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.

javascript Verify a DSSE in-toto envelope · browser WebCrypto
/**
 * 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));
Two independent checks: both must pass

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.

§7

Python verification: server-side and CI pipeline use

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.

python DSSE envelope verification · Python (cryptography >= 41.0)
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}")
§8

Multi-signer and key rotation patterns

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.
javascript Adding a co-signer entry to an existing envelope
/**
 * 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;
}
Never modify the payload when adding a co-signature

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.

§9

Composing with Sigstore and Cosign

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.

javascript Extract audit_signature as a standalone in-toto bundle (cosign-compatible)
/**
 * 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
shell Submit OCG in-toto bundle to Rekor transparency log
# 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
SLSA Level 2 via this path

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.

§10

Integration checklist

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
What you don't need for verification

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.