OpenChainGraph v0.3 · Integration How-To

Using OpenChainGraph with Ed25519

OCG v0.3 audit_signature envelopes require an Ed25519 signing key. This guide covers everything from key generation and storage to in-browser signing with WebCrypto, independent verification in Python, multi-signer patterns, and key rotation, all without leaving the client-side, zero-egress architecture that OCG runs on.

§1

Why Ed25519 for OCG artifacts

OpenChainGraph v0.3 specifies Ed25519 (RFC 8032) as the mandatory signature algorithm for audit_signature envelopes. The choice is deliberate and not interchangeable with RSA or ECDSA without a version bump.

Property Ed25519 RSA-2048 ECDSA P-256
Key size (private) 32 bytes 256 bytes 32 bytes
Signature size 64 bytes 256 bytes 64–72 bytes (DER)
Deterministic Yes: same key + message always produces the same signature Yes (PKCS#1 v1.5) / No (PSS) No: requires a random nonce; nonce reuse breaks security
Side-channel resistance By design: no timing variation from secret-dependent branches Requires careful implementation Timing side-channels documented in ECDSA
WebCrypto support Native: Ed25519 algorithm in all modern browsers Native (RSASSA-PKCS1-v1_5) Native (ECDSA)
Cosign / SLSA compatibility First-class: Sigstore defaults to Ed25519 Supported but not default Supported

The determinism property is critical for OCG. Because OCG tools run client-side and may be invoked by automated agents, the signing step must not introduce non-determinism that would change the signed payload. Ed25519's deterministic construction means a given execution_hash + private key pair always produces identical bytes in the sig field, making the audit trail reproducible.

WebCrypto availability note

The Ed25519 named curve is available in crypto.subtle on Chrome 113+, Firefox 130+, and Safari 17+ (all 2023–2024 releases). If you need to support older environments, a WASM-compiled @noble/ed25519 fallback is byte-compatible with WebCrypto's output; the signature bytes are identical. The verify() call works interchangeably between WebCrypto, Python's cryptography library, and go/crypto/ed25519.

§2

Key pair structure and the keyid field

An Ed25519 key pair used with OCG consists of a 32-byte private scalar and a 32-byte public key (the compressed Edwards point). OCG uses base64url-encoded DER or raw bytes, plus a keyid that appears in each signatures[] entry to identify which public key should be used for verification.

Ed25519 key pair · OCG usage
private key
32 bytes (256 bits), generated from a CSPRNG. Never exported, never leaves the signing environment. In browser flows, generated by crypto.subtle.generateKey() and stored in a non-extractable CryptoKey object. In CI/CD, stored as an encrypted secret.
public key
32 bytes: the compressed Edwards25519 point. Published as a base64url string. All verifiers use this to check audit_signature.signatures[].sig. Safe to embed in documentation, API responses, and the OCG manifest.
keyid
A stable, human-readable identifier for the key, typically the SHA-256 fingerprint of the public key bytes, truncated to 16 hex chars, or a semantic label like ocg-prod-2026-01. Used to select the right public key when verifying a multi-signer envelope. Not a secret.
sig
64 bytes: the Ed25519 signature over the DSSE PAE (Pre-Authentication Encoding) of the envelope payload. Stored as base64url in signatures[].sig. See §3 for what is actually signed.

The keyid is not authenticated by the signature itself; it is a routing hint for the verifier. Always look up the public key from a trusted store using the keyid, rather than trusting the keyid to describe the key.

Fingerprint convention

OCG recommends computing keyid as the lowercase hex of the first 8 bytes of SHA-256(publicKeyRawBytes), for example 3f9a1b2c4d5e6f7a. This is compatible with how Cosign and Sigstore compute key fingerprints. If you use a semantic label instead, ensure it is unique within your key store and changes on rotation.

§3

What Ed25519 signs: the DSSE PAE construction

OCG v0.3 follows the DSSE (Dead Simple Signing Envelope) specification for constructing the bytes that Ed25519 signs. The signature is not over the raw JSON payload; it is over a Pre-Authentication Encoding (PAE) that binds the payloadType string to the payload bytes. This prevents a signature valid for one content type from being replayed against another.

text DSSE PAE construction · what Ed25519 actually signs
# PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
# Where LEN is the ASCII decimal byte length, SP is a single space (0x20)

payloadType = "application/vnd.openchain.graph+json;version=0.2"
payload     = base64url_decode(audit_signature.payload)

pae = (
  "DSSEv1"
  + " " + str(len(payloadType.encode()))
  + " " + payloadType
  + " " + str(len(payload))
  + " "
).encode() + payload

# Ed25519.sign(privateKey, pae) → 64-byte signature → base64url → sig field
# Ed25519.verify(publicKey, pae, base64url_decode(sig)) → bool

The payload field in audit_signature is the base64url encoding of the OCG artifact's core fields (the fields that contributed to execution_hash). The PAE wraps this with the content type, ensuring the signature is self-describing. See Guide #3 (in-toto) §3 for the full payload serialization procedure.

Why not sign the raw JSON?

Signing raw JSON is dangerous because JSON has no canonical form: key ordering, whitespace, and Unicode normalization all affect the byte representation. The PAE approach, combined with OCG's deterministic key-sorted JSON serialization of the payload, ensures that any implementation can reconstruct the exact bytes that were signed, regardless of language or JSON library.

§4

Generating an Ed25519 key pair

Three environments where OCG signing keys are commonly generated: in-browser (for single-session or ephemeral keys), in Node.js/Python (for pipeline signing), and via cosign (for Sigstore-integrated workflows).

javascript (WebCrypto) Browser · generate a non-extractable Ed25519 key pair
// Generate a non-extractable Ed25519 key pair in the browser.
// The private key never leaves the WebCrypto key store.
const keyPair = await crypto.subtle.generateKey(
  { name: "Ed25519" },
  false,         // extractable = false — private key stays in the engine
  ["sign", "verify"]
);

// Export the public key as raw bytes (32 bytes) for distribution
const pubRaw     = await crypto.subtle.exportKey("raw", keyPair.publicKey);
const pubB64url  = btoa(String.fromCharCode(...new Uint8Array(pubRaw)))
                    .replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");

// Compute keyid: first 8 bytes of SHA-256(pubRaw) → lowercase hex
const hashBuf = await crypto.subtle.digest("SHA-256", pubRaw);
const keyid   = Array.from(new Uint8Array(hashBuf).slice(0, 8))
                  .map(b => b.toString(16).padStart(2, "0")).join("");

console.log("Public key (base64url):", pubB64url);
console.log("keyid:", keyid);

// Store keyPair.privateKey in memory; import it back via importKey() in future sessions.
python Server / CI · generate and export an Ed25519 key pair
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import (
    Encoding, PrivateFormat, PublicFormat, NoEncryption
)
import hashlib, base64

# Generate private key
private_key = Ed25519PrivateKey.generate()

# Export private key as raw bytes (32 bytes) — store encrypted at rest
priv_raw = private_key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption())

# Export public key as raw bytes (32 bytes) — safe to publish
pub_raw  = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
pub_b64url = base64.urlsafe_b64encode(pub_raw).rstrip(b"=").decode()

# Compute keyid: first 8 bytes of SHA-256(pub_raw) → lowercase hex
keyid = hashlib.sha256(pub_raw).digest()[:8].hex()

print(f"Public key (base64url): {pub_b64url}")
print(f"keyid:                  {keyid}")

# Write priv_raw to a secrets vault or environment variable, NEVER to a file in the repo.
shell Via cosign · generate key pair compatible with Sigstore workflows
# Generate key pair; cosign will prompt for a password to encrypt the private key
cosign generate-key-pair --output-key-prefix ocg-signing

# This produces:
#   ocg-signing.key      — encrypted PEM private key  (protect this)
#   ocg-signing.pub      — PEM public key             (publish this)

# Export the raw public key bytes for the OCG keyid computation
openssl pkey -in ocg-signing.pub -pubin -outform DER | tail -c 32 | xxd -p | tr -d '\n' | cut -c1-16
# → first 8 bytes of DER-encoded pub key (= first 8 bytes of raw public key bytes)

# Or: compute keyid from the PEM public key directly
openssl pkey -in ocg-signing.pub -pubin -outform DER \
  | sha256sum | cut -c1-16
# Prefix matches what WebCrypto and Python cryptography produce for the same key.
§5

Signing an OCG artifact (in-browser)

Once a key pair exists, signing a completed OCG artifact means constructing the PAE bytes, calling crypto.subtle.sign(), and writing the result into audit_signature.signatures[]. The full function:

javascript Complete in-browser signing function for OCG v0.3 artifacts
/**
 * signOcgArtifact(artifact, privateKey, keyid)
 *   artifact   — the completed OCG artifact object (must have execution_hash)
 *   privateKey — WebCrypto CryptoKey (Ed25519, usage: ["sign"])
 *   keyid      — string identifying this key (see §2)
 *
 * Returns the artifact with audit_signature fully populated.
 */
async function signOcgArtifact(artifact, privateKey, keyid) {
  const PAYLOAD_TYPE = "application/vnd.openchain.graph+json;version=0.2";

  // 1. Build the payload object: only the fields that contribute to execution_hash
  const payloadObj = {
    tool_id:          artifact.tool_id,
    execution_hash:   artifact.execution_hash,
    policy_parameters: sortKeysDeep(artifact.policy_parameters),
    output_payload:   sortKeysDeep(artifact.output_payload),
    mandate_type:     artifact.mandate_type,
    timestamp:        artifact.timestamp
  };
  const payloadBytes = new TextEncoder().encode(JSON.stringify(payloadObj));
  const payloadB64   = bytesToBase64url(payloadBytes);

  // 2. Build the DSSE PAE bytes
  const typeBytes = new TextEncoder().encode(PAYLOAD_TYPE);
  const pae = concat(
    new TextEncoder().encode("DSSEv1 "
      + typeBytes.length + " " + PAYLOAD_TYPE + " "
      + payloadBytes.length + " "),
    payloadBytes
  );

  // 3. Sign with Ed25519
  const sigBytes = await crypto.subtle.sign("Ed25519", privateKey, pae);
  const sigB64   = bytesToBase64url(new Uint8Array(sigBytes));

  // 4. Attach envelope to the artifact
  artifact.audit_signature = {
    payloadType: PAYLOAD_TYPE,
    payload:     payloadB64,
    signatures:  [{ keyid, sig: sigB64 }]
  };

  return artifact;
}

// Helper: recursively sort object keys (needed for payload determinism)
function sortKeysDeep(obj) {
  if (Array.isArray(obj)) return obj.map(sortKeysDeep);
  if (obj === null || typeof obj !== "object") return obj;
  return Object.fromEntries(
    Object.keys(obj).sort().map(k => [k, sortKeysDeep(obj[k])])
  );
}

// Helper: Uint8Array → base64url string (no padding)
function bytesToBase64url(bytes) {
  return btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}

// Helper: concat two Uint8Arrays
function concat(a, b) {
  const r = new Uint8Array(a.length + b.length);
  r.set(a); r.set(b, a.length); return r;
}
Key ordering in payloadObj

The payload serialized into audit_signature.payload must use the same key-sorted, deterministic JSON as the execution_hash preimage construction from Guide #2. The sortKeysDeep() call above ensures this. Any verifier that re-derives the payload from the artifact fields will produce identical bytes, which means the signature check is also a hash check.

§6

Verifying a signature in JavaScript (WebCrypto)

Independent verification requires only the artifact JSON and the signer's public key. No OCG-specific library, no network call, no access to the tool that generated the artifact.

javascript Independent signature verification · WebCrypto, no dependencies
/**
 * verifyOcgSignature(artifact, publicKeyB64url)
 *   artifact       — the OCG artifact object (v0.3, with audit_signature)
 *   publicKeyB64url — base64url-encoded 32-byte Ed25519 public key
 *
 * Returns { valid: bool, keyid: string, error?: string }
 */
async function verifyOcgSignature(artifact, publicKeyB64url) {
  const env = artifact.audit_signature;
  if (!env || typeof env !== "object") {
    return { valid: false, error: "No audit_signature object (v0.1 artifact?)" };
  }

  const PAYLOAD_TYPE = "application/vnd.openchain.graph+json;version=0.2";
  if (env.payloadType !== PAYLOAD_TYPE) {
    return { valid: false, error: "Unexpected payloadType: " + env.payloadType };
  }

  // 1. Decode payload bytes from the envelope
  const payloadBytes = base64urlToBytes(env.payload);

  // 2. Reconstruct PAE
  const typeBytes = new TextEncoder().encode(PAYLOAD_TYPE);
  const pae = concat(
    new TextEncoder().encode("DSSEv1 "
      + typeBytes.length + " " + PAYLOAD_TYPE + " "
      + payloadBytes.length + " "),
    payloadBytes
  );

  // 3. Import the public key
  const pubRaw = base64urlToBytes(publicKeyB64url);
  const pubKey = await crypto.subtle.importKey(
    "raw", pubRaw, { name: "Ed25519" }, false, ["verify"]
  );

  // 4. Verify each signature entry
  const results = await Promise.all(
    (env.signatures || []).map(async entry => {
      const sigBytes = base64urlToBytes(entry.sig);
      const ok = await crypto.subtle.verify("Ed25519", pubKey, sigBytes, pae);
      return { keyid: entry.keyid, sigValid: ok };
    })
  );

  // 5. Cross-check: re-derive payload from artifact fields and compare
  const rePayload = JSON.stringify({
    tool_id:          artifact.tool_id,
    execution_hash:   artifact.execution_hash,
    policy_parameters: sortKeysDeep(artifact.policy_parameters),
    output_payload:   sortKeysDeep(artifact.output_payload),
    mandate_type:     artifact.mandate_type,
    timestamp:        artifact.timestamp
  });
  const rePayloadBytes = new TextEncoder().encode(rePayload);
  const payloadMatch   = rePayload === new TextDecoder().decode(payloadBytes);

  const sigValid = results.some(r => r.sigValid);
  return {
    valid:        sigValid && payloadMatch,
    sigValid,
    payloadMatch,
    signatures:   results,
    keyid:        results.find(r => r.sigValid)?.keyid ?? null
  };
}

// Helper: base64url string → Uint8Array
function base64urlToBytes(b64url) {
  const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
  const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
  return Uint8Array.from(atob(padded), c => c.charCodeAt(0));
}
§7

Verifying a signature in Python

The same verification procedure, implemented using Python's cryptography package. Byte-compatible with the WebCrypto implementation above: given the same artifact and public key, both return the same result.

python Independent signature verification · cryptography package, no OCG library needed
import base64, json, hashlib
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature

def sort_keys_deep(obj):
    """Recursively sort dict keys — must match OCG payload construction."""
    if isinstance(obj, dict):
        return {k: sort_keys_deep(obj[k]) for k in sorted(obj)}
    if isinstance(obj, list):
        return [sort_keys_deep(i) for i in obj]
    return obj

def b64url_decode(s):
    s = s.replace("-", "+").replace("_", "/")
    return base64.b64decode(s + "=" * ((4 - len(s) % 4) % 4))

def verify_ocg_signature(artifact: dict, public_key_b64url: str) -> dict:
    """
    Verify an OCG v0.3 audit_signature envelope.

    Returns a dict:
      valid        — True only if sig AND payload both check out
      sig_valid    — True if at least one signatures[] entry verified
      payload_match— True if the envelope payload matches re-derived artifact fields
      error        — error message if invalid, else None
    """
    PAYLOAD_TYPE = "application/vnd.openchain.graph+json;version=0.2"
    env = artifact.get("audit_signature")

    if not isinstance(env, dict):
        return {"valid": False, "error": "No audit_signature object (v0.1 artifact?)"}

    if env.get("payloadType") != PAYLOAD_TYPE:
        return {"valid": False, "error": f"Unexpected payloadType: {env.get('payloadType')}"}

    # 1. Decode envelope payload
    payload_bytes = b64url_decode(env["payload"])

    # 2. Build PAE
    type_bytes = PAYLOAD_TYPE.encode()
    pae = (
        f"DSSEv1 {len(type_bytes)} {PAYLOAD_TYPE} {len(payload_bytes)} ".encode()
        + payload_bytes
    )

    # 3. Load public key from raw bytes
    pub_raw = b64url_decode(public_key_b64url)
    pub_key = Ed25519PublicKey.from_public_bytes(pub_raw)

    # 4. Verify each signature entry
    sig_valid = False
    for entry in env.get("signatures", []):
        sig_bytes = b64url_decode(entry["sig"])
        try:
            pub_key.verify(sig_bytes, pae)
            sig_valid = True
        except InvalidSignature:
            pass

    # 5. Cross-check: re-derive payload from artifact fields
    re_payload_obj = {
        "execution_hash":    artifact["execution_hash"],
        "mandate_type":      artifact.get("mandate_type"),
        "output_payload":    sort_keys_deep(artifact.get("output_payload", {})),
        "policy_parameters": sort_keys_deep(artifact.get("policy_parameters", {})),
        "timestamp":         artifact.get("timestamp"),
        "tool_id":           artifact["tool_id"],
    }
    re_payload_bytes = json.dumps(re_payload_obj, separators=(",", ":")).encode()
    payload_match = re_payload_bytes == payload_bytes

    return {
        "valid":         sig_valid and payload_match,
        "sig_valid":     sig_valid,
        "payload_match": payload_match,
        "error":         None if (sig_valid and payload_match) else "Signature or payload mismatch"
    }

# Usage:
# with open("artifact.json") as f:
#     artifact = json.load(f)
# result = verify_ocg_signature(artifact, "base64url_encoded_public_key_here")
# assert result["valid"], result["error"]
re_payload_obj key order

Python dictionaries preserve insertion order since 3.7, but the key ordering in the re-derived payload must match what the signing code produced. Both the JavaScript (sortKeysDeep) and the Python (sort_keys_deep) implementations sort all keys before serializing, which makes the payload canonical regardless of the original insertion order. Ensure you use separators=(",", ":") in json.dumps() to suppress whitespace; the signing code serializes without whitespace too.

§8

Multi-signer patterns: co-signing and dual-control

The signatures[] array in the DSSE envelope is designed to carry multiple signatures over the same payload. OCG v0.3 supports two production patterns for organizations that require dual-control or multi-party authorization.

Pattern A: Sequential co-signing. A second signer receives the artifact after the first signature, verifies the payload, and appends their own entry to signatures[] without changing any other field. The PAE is the same bytes for both signers.

javascript Appending a co-signature to an existing audit_signature envelope
async function coSignOcgArtifact(artifact, secondPrivateKey, secondKeyid) {
  const env = artifact.audit_signature;
  if (!env?.signatures) throw new Error("No existing envelope to co-sign");

  // Reconstruct the same PAE that the first signer used
  const PAYLOAD_TYPE = "application/vnd.openchain.graph+json;version=0.2";
  const payloadBytes = base64urlToBytes(env.payload);
  const typeBytes    = new TextEncoder().encode(PAYLOAD_TYPE);
  const pae = concat(
    new TextEncoder().encode("DSSEv1 "
      + typeBytes.length + " " + PAYLOAD_TYPE + " "
      + payloadBytes.length + " "),
    payloadBytes
  );

  // Sign and push new entry
  const sigBytes = await crypto.subtle.sign("Ed25519", secondPrivateKey, pae);
  env.signatures.push({
    keyid: secondKeyid,
    sig:   bytesToBase64url(new Uint8Array(sigBytes))
  });

  return artifact;  // audit_signature.signatures[] now has 2 entries
}

// Verification: check that BOTH keyids signed the same payload
async function verifyDualControl(artifact, publicKeys) {
  // publicKeys: { [keyid]: base64urlPublicKey }
  const results = await Promise.all(
    Object.entries(publicKeys).map(([kid, pub]) =>
      verifyOcgSignature(artifact, pub).then(r => ({ ...r, expectedKeyid: kid }))
    )
  );
  const allValid = results.every(r => r.valid);
  return { dualControlPassed: allValid, details: results };
}

Pattern B: Threshold signing. For organizations using Shamir's Secret Sharing or threshold Ed25519 (e.g., FROST), each participant produces a partial signature. The combiner assembles the final 64-byte Ed25519 signature and writes a single signatures[] entry. From the OCG verifier's perspective, a threshold-combined signature is indistinguishable from a single-signer signature; verification is identical. The threshold coordination happens outside the OCG envelope.

keyid policy enforcement

OCG envelopes carry keyid fields but do not enforce which keyids are required. Your verification policy (for example "both risk-officer-2026 and compliance-system-2026 must appear in signatures[] and both must verify") is implemented in the application layer on top of the envelope, not inside it. This is intentional: the envelope is a transport format; access control belongs in your policy engine.

§9

Key rotation without breaking audit trails

Because OCG artifacts are immutable after signing (the execution_hash and audit_signature are fixed at artifact generation time), rotating signing keys does not require re-signing historical artifacts. Verifiers simply need access to the key that was active when the artifact was signed, identified by keyid.

T−30
Generate new key pair
Generate the successor key pair. Compute its keyid. Publish the new public key to your key manifest at a stable URL. Do not revoke the old key yet.
T−7
Dual-sign with both keys
Begin appending co-signatures with the new key alongside the old key during the overlap window. Artifacts in this window will have two signatures[] entries. Verifiers that only know the old key still pass; verifiers that know the new key also pass.
T=0
Cut over to new key
New artifacts are signed with the new key only. The old private key is destroyed or archived offline. The old public key remains in the key manifest indefinitely; it is needed to verify artifacts signed before T=0.
T+90
Mark old key as rotated
Update the key manifest to flag the old keyid as "status": "rotated" with a rotated_at timestamp. Verifiers can use this to warn when encountering artifacts signed with the rotated key, but they should still accept valid signatures from it.
json Recommended key manifest format · publish at a stable URL
{
  "@context":  "https://ainumbers.co/chaingraph/context/v0.3",
  "type":      "OcgKeyManifest",
  "publisher": "Post Oak Labs",
  "keys": [
    {
      "keyid":      "3f9a1b2c4d5e6f7a",
      "algorithm": "Ed25519",
      "public_key": "base64url_encoded_32_bytes",
      "status":    "active",
      "valid_from": "2026-01-01T00:00:00Z",
      "comment":  "OCG production signing key 2026-Q1"
    },
    {
      "keyid":      "8b7c2a1d9e4f3b6c",
      "algorithm": "Ed25519",
      "public_key": "base64url_encoded_32_bytes",
      "status":    "rotated",
      "valid_from": "2025-01-01T00:00:00Z",
      "rotated_at": "2026-01-01T00:00:00Z",
      "comment":  "OCG production signing key 2025 — rotated, public key retained for historical verification"
    }
  ]
}
Never delete old public keys

Removing a rotated public key from your key manifest breaks the ability to verify historical artifacts signed with that key. This is an irreversible audit trail gap. Mark rotated keys with "status": "rotated" and keep their public key bytes permanently. Private keys can and should be destroyed on rotation; the 32-byte public key is all that's needed for future verification.

§10

Composing with Cosign and Fulcio (keyless signing)

If your organization already uses Sigstore's Cosign and Fulcio for image signing, OCG artifacts can use the same key material and transparency log without any additional key management infrastructure.

shell Keyless signing with Cosign · binds signature to OIDC identity
# Keyless flow: Cosign generates an ephemeral Ed25519 key, uses Fulcio to issue
# a short-lived certificate binding the key to an OIDC identity (e.g. GitHub Actions OIDC),
# and submits the signature + cert chain to Rekor.

# 1. Sign the extracted OCG payload file (from Guide #3 §9)
cosign sign-blob \
  --bundle ocg-artifact.bundle \
  --identity-token "${OIDC_TOKEN}" \
  artifact-payload.json

# 2. Verify independently (auditor side) — no pre-shared key required
cosign verify-blob \
  --bundle ocg-artifact.bundle \
  --certificate-identity "[email protected]" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  artifact-payload.json

# 3. Record the Rekor UUID in your audit log alongside the artifact
#    rekor-cli get --uuid <uuid from bundle> → confirms timestamp + transparency

In the keyless flow, the keyid field in audit_signature.signatures[] should be set to the Fulcio certificate fingerprint or the Rekor entry UUID, so that verifiers know where to look up the short-lived certificate chain rather than a long-lived public key.

When to use keyless vs. long-lived keys

Keyless signing is best for CI/CD pipelines that run in ephemeral environments with OIDC identity (GitHub Actions, Google Cloud Build, etc.). Long-lived Ed25519 keys are better for in-browser signing where OIDC infrastructure is not available, and for regulated environments where the signing key must be controlled by a specific named individual or HSM. OCG v0.3 supports both: the envelope format is the same; only the key management and key manifest structure differ.

§11

Integration checklist

Eight steps to integrate Ed25519 signing and verification into an OCG v0.3 deployment. Each is independently testable.

Step Action Verified by
1 Generate an Ed25519 key pair using the appropriate method for your environment (§4). Record the keyid computed from the public key fingerprint. keyid is a 16-char lowercase hex string; publicKeyB64url is a 43-char base64url string (32 bytes, no padding)
2 Publish the public key and keyid to your key manifest at a stable, publicly accessible URL. Add the manifest URL to your OCG tool configuration. HTTP GET on the manifest URL returns valid JSON with a keys[] array containing the new keyid and "status": "active"
3 Confirm that audit_signature on generated artifacts is an object (not a string). A string indicates v0.1; see Guide #3 §3 for migration notes. typeof artifact.audit_signature === "object" returns true
4 Run signOcgArtifact() from §5 (or the equivalent in your language) against a test artifact. Inspect the resulting signatures[0].sig: it should be a 86-char base64url string (64 bytes, no padding). artifact.audit_signature.signatures[0].sig.length === 86
5 Run verifyOcgSignature() from §6 against the signed test artifact using the public key from step 1. Both sigValid and payloadMatch must return true. result.valid === true and result.payloadMatch === true
6 Cross-verify using the Python implementation from §7 with the same artifact and public key. Results must be identical to step 5, confirming cross-language byte compatibility. result["valid"] == True in Python on the same artifact that passed step 5 in JavaScript
7 If dual-control is required: implement co-signing using the pattern from §8. Verify that each required keyid appears in signatures[] and that each entry individually verifies against its corresponding public key. dualControlPassed === true in the verifyDualControl() result; both details[] entries show valid: true
8 Document your rotation schedule and publish it alongside the key manifest. At first rotation, verify that historical artifacts (signed with the old key) still verify correctly using the retained public key. A historical artifact with the rotated keyid still returns valid: true when verified against the public key in the manifest (even after status is set to "rotated")
What you don't need for Ed25519 verification

Independent verification of an OCG v0.3 Ed25519 signature requires exactly two things: the artifact JSON, and the signer's 32-byte public key. You do not need the OCG specification, the vendor's server, the original tool, a network connection, a certificate authority, or any OCG-specific library. Ed25519 is implemented natively in browser WebCrypto, Python's cryptography package, Go's crypto/ed25519, Rust's ed25519-dalek, and Java's BouncyCastle: any of these can verify an OCG signature produced by any other, with zero adaptation.