Skip to main content
Version: v0.25.0 (Latest)

SAID Verification

Every OCA object carries a Self-Addressing Identifier (SAID) under its d field. The SAID is a content-derived hash that lets any consumer verify that the object has not been altered since it was issued, without needing a separate signature or trust anchor. The EDK's OcaSaidVerifier provides three progressively strict verification levels: format, structural, and cryptographic.

A SAID begins with a one-character algorithm prefix (E for SHA-256, F for SHA-512, G for BLAKE3-256, H for BLAKE2b-256, I for BLAKE2s-256) followed by the Base64url-encoded hash. The total length depends on the algorithm. The hash itself is computed over the object's canonical JSON with the SAID field temporarily replaced by a placeholder of the same length, so two different objects whose only difference is the SAID value can be told apart by recomputing.

Format Verification​

The cheapest check looks only at the SAID string itself:

val ok = OcaSaidVerifier.isValidSaidFormat(said)

It verifies that the SAID starts with a known algorithm prefix, has the correct length for that algorithm, and contains only Base64url characters (A-Z, a-z, 0-9, -, _). It does not look at any content and cannot detect tampering. Use this when you only need to reject obviously malformed identifiers before doing more expensive work.

Structural Verification​

The next level checks that the bundle and its overlays are wired together correctly:

val ok = OcaSaidVerifier.verifyBundle(parsed)

Or, with detailed failure reasons:

val result = OcaSaidVerifier.verifyBundleDetailed(parsed)
if (!result.valid) {
println("Bundle invalid: ${result.reason}")
}

Structural verification confirms that the bundle SAID and capture base SAID are present and formatted correctly, every overlay has a SAID, and every overlay's captureBaseSaid reference matches the bundle's capture base. This catches the common "we glued two unrelated bundles together" failure mode and the "this overlay was authored for a different capture base" failure mode without recomputing any hashes.

OcaBundleService.verifySaid is the same as OcaSaidVerifier.verifyBundle: it returns a boolean for the structural check. It is the default sanity check the EDK runs before importing an OCA bundle.

Cryptographic Verification​

The strictest level recomputes each SAID and compares it against the value embedded in the object:

val hasher: SaidHasher = SaidHasherImpl()  // SHA-256 implementation from lib-data-oca-impl
val result = OcaSaidVerifier.verifyBundleCryptographic(parsed, hasher)
if (!result.valid) {
println("Cryptographic verification failed: ${result.reason}")
}

This catches any tampering with the content of any overlay, the capture base, or the bundle itself. The algorithm follows the OCA specification:

  1. Replace the SAID field with # placeholder characters matching the Base64url output length of the hash algorithm (44 characters for SHA-256).
  2. Compute the hash over the canonical (sorted-keys) JSON serialization.
  3. Prepend the version byte 0x00.
  4. Encode as Base64 URL-safe without padding.
  5. Replace the first character with the algorithm prefix (E for SHA-256).

OcaSaidVerifier.verifyBundleCryptographic runs the structural check first and then verifies each overlay's SAID, the capture base SAID, and the bundle-level SAID in turn. The first failure short-circuits and is reported with a human-readable reason.

The SaidHasher Contract​

The OCA public module does not carry a crypto dependency. Cryptographic verification requires the caller to supply a SaidHasher implementation:

fun interface SaidHasher {
fun hash(input: ByteArray): ByteArray
}

The expected algorithm is implied by the SAID prefix being verified, so a SaidHasher that computes SHA-256 is sufficient for verifying any SAID with prefix E. The default implementation SaidHasherImpl in lib-data-oca-impl provides SHA-256 on every supported Kotlin Multiplatform target. Custom algorithms are supplied by injecting an alternative SaidHasher.

Computing and Applying SAIDs​

When constructing OCA objects programmatically, the writer pattern is:

// Build the object with a placeholder in `d`
val withPlaceholder = buildJsonObject {
put("d", JsonPrimitive("#".repeat(44)))
put("type", JsonPrimitive("spec/capture_base/2.0.0"))
put("attributes", JsonObject(...))
}

// Recompute the SAID and write it into `d`
val finalised = OcaSaidVerifier.applySaid(withPlaceholder, hasher)

applySaid does the compute step (computeSaid) and the field-overwrite step in one call. The resulting object is structurally identical to the input, except the d field now holds a cryptographically valid SAID. For multi-level objects (bundles containing capture bases containing overlays), apply SAIDs from the innermost level outwards: overlays first, then the capture base, then the bundle.

Choosing a Verification Level at Import Time​

The EDK's design import flow uses structural verification by default: it is fast, it catches the obvious mistakes (mismatched capture base references, missing SAIDs), and it does not require a SaidHasher to be present in the DI graph for every consumer.

When you need stronger guarantees (importing bundles from an untrusted source, accepting bundles in a federation where any participant can publish), upgrade to cryptographic verification by injecting a SaidHasher and calling verifyBundleCryptographic explicitly before passing the parsed bundle to the design mapper. The cost is a SHA-256 over the canonical JSON of every object, which is negligible for any realistic bundle size.

Format verification is rarely useful on its own; it is a building block for the other levels and for input sanitisation in admin UIs that surface SAIDs as user-editable strings.