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:
- Replace the SAID field with
#placeholder characters matching the Base64url output length of the hash algorithm (44 characters for SHA-256). - Compute the hash over the canonical (sorted-keys) JSON serialization.
- Prepend the version byte
0x00. - Encode as Base64 URL-safe without padding.
- Replace the first character with the algorithm prefix (
Efor 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.