Skip to content

ECDSA Cryptography & Document Integrity ​

The Object Interactive Media (OIM) ecosystem employs asymmetric cryptography to guarantee the authenticity, author intent, and byte-level integrity of every .oim and .ovm document. This document provides the authoritative technical specification for the cryptographic algorithms, canonical serialization rules, signature encoding, public key distribution, and verification procedures.


1. Cryptographic Architecture Overview ​

Every packaged .oim binary embeds an Elliptic Curve Digital Signature Algorithm (ECDSA) signature directly within its container metadata. This cryptographic seal establishes three core guarantees:

  1. Document Integrity: Detects any alteration to interactive slots, localized text dictionaries, coordinates, animations, vector shapes, or visual styles.
  2. Base Media Binding: Cryptographically bonds the Dynamic Layer (+1) metadata to the baked image (Layer −1) via SHA-256 hashing.
  3. Re-edition Enforcement: Cryptographically underpins the document lifecycle (public, author_only, sealed), preventing unauthorized tampering or overwrites.
text
┌────────────────────────────────────────────────────────────────────────┐
│                        .oim Container (PNG)                            │
│  ┌───────────────────────────┐      ┌───────────────────────────────┐  │
│  │   Layer −1: Baked Pixels  │      │  Layer +1: Dynamic Metadata   │  │
│  │   (Lossless Raster Data)  │      │  (zTXt chunk: "OIM_Data")     │  │
│  └─────────────┬─────────────┘      └───────────────┬───────────────┘  │
│                │ SHA-256                            │                  │
│                ▼                                    ▼                  │
│          [image_hash] ──────────────────► [Canonical Serialization]    │
│                                                     │                  │
│                                                     ▼                  │
│                                            ECDSA NIST P-256 Signature  │
│                                            (IEEE P1363 Raw 64 Bytes)   │
└────────────────────────────────────────────────────────────────────────┘

2. Cryptographic Primitives ​

2.1. Curve Parameters: NIST P-256 (secp256r1) ​

OIM mandates the NIST P-256 elliptic curve (standardized in FIPS 186-4, ANSI X9.62, and SECG SEC 2; OID 1.2.840.10045.3.1.7).

The curve is defined over the prime field $\mathbb{F}_p$ by the short Weierstrass equation: $$y^2 \equiv x^3 + a x + b \pmod{p}$$

ParameterNotationHexadecimal Value
Field Prime$p$0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
Coefficient $a$$a$0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC ($p - 3$)
Coefficient $b$$b$0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
Base Point $G_x$$G_x$0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
Base Point $G_y$$G_y$0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
Order $n$$n$0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
Cofactor$h$0x01

2.2. Digest Function: SHA-256 ​

The message digest algorithm is SHA-256 (FIPS 180-4). A 256-bit hash is computed over the canonical UTF-8 bytes of the document metadata.


3. Signature Encoding: IEEE P1363 Raw Format ​

ECDSA signatures mathematically consist of a pair of large integers $(r, s)$ modulo $n$. In software implementations, two primary serialization standards exist:

  1. ASN.1 DER Sequence (RFC 3279): Variable-length encoding (typically 70–72 bytes) wrapping $r$ and $s$ with length tags and sign-bit zero padding.
  2. IEEE P1363 Raw Format: Fixed-length 64-byte binary structure consisting of $r$ and $s$ concatenated as fixed-width, big-endian unsigned integers.

3.1. IEEE P1363 Byte Structure ​

OIM standardizes exclusively on IEEE P1363:

text
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                    r-coordinate (32 bytes)                    +
|                 Big-Endian Unsigned Integer                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                    s-coordinate (32 bytes)                    +
|                 Big-Endian Unsigned Integer                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • Byte 0 to 31 (32 bytes): The $r$ coordinate, zero-padded on the left if less than 32 bytes.
  • Byte 32 to 63 (32 bytes): The $s$ coordinate, zero-padded on the left if less than 32 bytes.
  • Base64 String: The 64 raw bytes are encoded as an 88-character Base64 string ending with =, stored in metadata.signature.

Why IEEE P1363?

The W3C Web Cryptography API (window.crypto.subtle.verify) natively expects IEEE P1363 raw signatures for ECDSA. Emitting IEEE P1363 directly avoids requiring heavy ASN.1 parsing libraries in the browser runtime.


4. Canonical Recursive Serialization (serialize_canonical) ​

Because JSON does not enforce key ordering and floating-point representations differ across runtimes (e.g. Python vs. JavaScript V8), digital signatures require a strict, deterministic normalization process.

4.1. Normalization Rules ​

The canonical serialization algorithm executes the following steps in sequence:

  1. Exclude Dynamic & Ephemeral Envelope Fields: The following top-level keys are excluded from the signed payload:

    • "signature": Prevents circular dependency.
    • "key_version": Ephemeral key identifier envelope.
    • "timestamp": Packaging timestamp envelope.
  2. Exclude Personal Identifiers (creator_user_id):

    • creator_user_id is explicitly stripped before signing and omitted from the published file.
    • GDPR Compliance: Author identity is personal data. Immutably sealing personal IDs into a distributed binary violates the GDPR Right to Erasure ("Right to be Forgotten"). Authorship is tracked in the revocable backend registry, not sealed inside the immutable binary.
  3. Numerical Normalization (clean_floats):

    • Integer Floats: Any floating-point value that represents an integer (e.g. 50.0, 100.00) is converted to an integer primitive (50, 100).
    • Fractional Floats: Any non-integer float is rounded to 6 decimal places (round(val, 6)).
    • This eliminates serialization discrepancies across IEEE 754 float formatters.
  4. Recursive Key Sorting:

    • All object keys at every level of the hierarchy are sorted lexicographically in ascending ASCII order.
    • Array element order is strictly preserved; however, objects nested within arrays have their keys recursively sorted.
  5. Compact Formatting & UTF-8 Encoding:

    • Delimiters use zero whitespace: comma , and colon : without surrounding spaces.
    • Characters outside ASCII are encoded as direct UTF-8 octets (ensure_ascii=False), never as \uXXXX escape sequences.

4.2. Reference Implementation (TypeScript & Python) ​

typescript
export function serializeCanonical(data: Record<string, any>): Uint8Array {
  function sortKeys(obj: any): any {
    if (Array.isArray(obj)) {
      return obj.map(sortKeys);
    } else if (obj !== null && typeof obj === 'object') {
      const sorted: Record<string, any> = {};
      const keys = Object.keys(obj).sort();
      for (const k of keys) {
        sorted[k] = sortKeys(obj[k]);
      }
      return sorted;
    }
    return obj;
  }

  function cleanFloats(obj: any): any {
    if (Array.isArray(obj)) {
      return obj.map(cleanFloats);
    } else if (obj !== null && typeof obj === 'object') {
      const cleaned: Record<string, any> = {};
      for (const [k, v] of Object.entries(obj)) {
        cleaned[k] = cleanFloats(v);
      }
      return cleaned;
    } else if (typeof obj === 'number') {
      if (Number.isInteger(obj)) return obj;
      return Number(obj.toFixed(6));
    }
    return obj;
  }

  const cleaned: Record<string, any> = {};
  const excluded = new Set(["signature", "key_version", "timestamp", "creator_user_id"]);
  const keys = Object.keys(data).filter(k => !excluded.has(k)).sort();

  for (const k of keys) {
    cleaned[k] = sortKeys(cleanFloats(data[k]));
  }

  return new TextEncoder().encode(JSON.stringify(cleaned));
}
python
import json

def clean_floats(obj):
    if isinstance(obj, dict):
        return {k: clean_floats(v) for k, v in obj.items()}
    elif isinstance(obj, list):
        return [clean_floats(x) for x in obj]
    elif isinstance(obj, float):
        if obj.is_integer():
            return int(obj)
        return round(obj, 6)
    return obj

def serialize_canonical(data: dict) -> bytes:
    excluded = {"signature", "key_version", "timestamp", "creator_user_id"}
    cleaned = {k: v for k, v in data.items() if k not in excluded}
    cleaned = clean_floats(cleaned)
    return json.dumps(
        cleaned,
        sort_keys=True,
        separators=(',', ':'),
        ensure_ascii=False
    ).encode('utf-8')

5. Public Key Distribution & RFC 7517 JWK ​

Verification public keys are distributed as standard JSON Web Key (JWK) sets conforming to RFC 7517.

5.1. Key Directory Endpoint ​

http
GET https://api.oim.media/v1/media/keys

Response Format (200 OK):

json
{
  "keys": [
    {
      "kty": "EC",
      "crv": "P-256",
      "x": "5Qavr4M_npVognquI5k5D9K8wcXBXM9Z0mZHjqyehAs",
      "y": "oh7rz5Q7SlZT_ms2buyOw0bPX77QsXimc0X92Ymj2_o",
      "kid": "v1"
    }
  ]
}

5.2. JWK Field Schema ​

  • kty (Key Type): Strictly "EC".
  • crv (Curve): Strictly "P-256".
  • x: Unsigned 32-byte big-endian affine X-coordinate, Base64URL-encoded (unpadded).
  • y: Unsigned 32-byte big-endian affine Y-coordinate, Base64URL-encoded (unpadded).
  • kid (Key Identifier): Version string matching metadata.key_version in .oim files (e.g. "v1", "v2").

5.3. Multi-Tier Resolution Strategy in <oim-player> ​

The OIM Web Player implements a 4-tier caching architecture to guarantee offline verification and resilience against network failures:

  1. Tier 1 — Pinned Fallback: The current production root key (kid: "v1") is bundled directly in the player runtime for zero-latency startup.
  2. Tier 2 — Memory Cache: Keys resolved during runtime are stored in an in-memory table _cachedKeys.
  3. Tier 3 — IndexedDB Storage: Keys fetched from the server are persisted in the browser IndexedDB database oim-player-keys (store keys, keyPath kid). Rotated keys remain verifiable offline across browser sessions.
  4. Tier 4 — Network Query: If a document specifies an unknown kid or verification against a pinned key fails, the player queries /v1/media/keys.

6. Document Re-Edition Permissions & Lifecycle ​

Every .oim document declares its re-edition governance policy. These permissions are evaluated both client-side by visual tools and enforced server-side by the packaging API.

6.1. The Three Re-Edition States ​

StateDeclaration in MetadataBackend Registry LockDescriptionPlayer Verification Badge
author_only (Default)reedit: "author_only"
immutable_lock: false
immutable_lock: falseOnly the authenticated creator account may package updates under this oim_id. Any attempt by another user is rejected with 403 Forbidden.verified (Green Seal)
publicreedit: "public"
immutable_lock: false
immutable_lock: falseOpen community template/asset. Any user can open, modify, and re-export under the same ID without ownership checks.verified_authorless (Open Community Seal)
sealedreedit: "sealed"
immutable_lock: true
immutable_lock: truePermanently finalized binary. Re-edition is permanently disabled for all users, including the original author. Irreversible.verified (Sealed Document)

6.1.1. The image seal ​

The signature covers the metadata and the base image too. When packaging, the backend computes the SHA-256 of the final PNG's image data (the concatenated IDAT chunks) and signs it inside the metadata as image_hash, with image_hash_alg: "sha256-idat". The player recomputes that hash over the file it receives and compares.

OutcomeStatusShown?
Valid signature, image matches the sealverified / verified_authorlessYes
Valid signature, no seal (files before 11 Sept 2026)image_unsealedYes, with a notice
Valid signature, image does not matchimage_mismatchNo
Invalid signatureinvalid_signatureNo
Signed with a key past its verify_until date (device clock)expired_keyBase image only

An altered file is not shown: neither its image nor its layers. The base image is painted before verification, for fast loading, and removed if the verdict is tampering.

Only seals that declare image_hash_alg are enforced. In June 2026 a client sent its own image_hash and the backend signed it unchecked (some files carry the SHA-256 of empty input), so an image_hash without an algorithm proves nothing and is treated as image_unsealed.

Recompressing a PNG without changing a pixel also changes its IDAT: for the seal, that is no longer the file OIM produced.

6.2. Fail-Closed Fallback Rules ​

To prevent manipulated or corrupted metadata from weakening document permissions:

  • Unknown String Fallback: If reedit contains an unrecognized string (e.g. "open", "PUBLIC"), it falls back strictly to "author_only". It never falls back to "public".
  • Legacy names (until 2.0.0): files signed before 1.23.0 carry reedicion with the Spanish values publico, solo_autor and sellado. They are still read, as public, author_only and sealed; reedit wins when both are present. Documents written from 1.23.0 carry reedit only.
  • Precedence of immutable_lock: If immutable_lock: true is present, it overrides any value in reedit (even if reedit: "public"). The document is treated strictly as "sealed".
  • Registry Bypass: Documents marked public bypass ownership registry checks (registryQuery(permiso) returns false), preventing malicious actors from locking community assets.

7. Dual-Layer Cryptographic Binding (image_hash) ​

A primary vulnerability in media-embedding formats is the risk of an attacker altering base image pixels while retaining interactive overlays, or vice-versa. OIM eliminates this vulnerability through mutual cryptographic binding:

  1. Baked Image Hash (Layer −1): At packaging time, the SHA-256 digest of the raw baked raster image is computed and embedded in the metadata:
    json
    {
      "image_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
      "slots": []
    }
  2. Signature Coverage: Because image_hash is an included field in serialize_canonical, the ECDSA signature cryptographically commits to the exact pixel contents of the baked image (Layer −1).
  3. Verification Enforcement:
    • Modifying the baked raster pixels (Layer −1) causes the computed hash to diverge from image_hash.
    • Modifying image_hash in the metadata causes the ECDSA signature verification to fail.

8. Complete Verification Examples ​

8.1. Web Browser (Native Web Crypto API) ​

typescript
import { serializeCanonical } from '@oim-media/core';

async function verifyOimDocument(metadata: any): Promise<boolean> {
  if (!metadata.signature || !metadata.key_version) {
    console.warn('Document is unsigned.');
    return false;
  }

  // 1. Fetch public keys
  const res = await fetch('https://api.oim.media/v1/media/keys');
  const { keys } = await res.json();
  const jwk = keys.find((k: any) => k.kid === metadata.key_version);

  if (!jwk) {
    throw new Error(`Public key not found for kid: ${metadata.key_version}`);
  }

  // 2. Import JWK into Web Crypto API
  const publicKey = await window.crypto.subtle.importKey(
    "jwk",
    jwk,
    { name: "ECDSA", namedCurve: "P-256" },
    false,
    ["verify"]
  );

  // 3. Decode IEEE P1363 64-byte signature
  const rawSignature = Uint8Array.from(atob(metadata.signature), c => c.charCodeAt(0));

  // 4. Compute canonical bytes
  const canonicalBytes = serializeCanonical(metadata);

  // 5. Verify signature
  const isValid = await window.crypto.subtle.verify(
    { name: "ECDSA", hash: { name: "SHA-256" } },
    publicKey,
    rawSignature,
    canonicalBytes
  );

  return isValid;
}

8.2. Python Verification Script ​

python
import base64
import requests
from cryptography.hazmat.primitives.asymmetric import ec, utils
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey

# Import your canonical serialization implementation
from core.utils import serialize_canonical

def verify_oim_metadata(metadata: dict) -> bool:
    signature_b64 = metadata.get("signature")
    key_version = metadata.get("key_version")
    if not signature_b64 or not key_version:
        return False

    # 1. Retrieve public keys
    keys_resp = requests.get("https://api.oim.media/v1/media/keys").json()
    jwk = next((k for k in keys_resp.get("keys", []) if k.get("kid") == key_version), None)
    if not jwk:
        raise ValueError(f"Key version {key_version} not found in directory")

    # 2. Decode Base64URL coordinates to EllipticCurvePublicKey
    def b64url_decode(s: str) -> bytes:
        s += '=' * (-len(s) % 4)
        return base64.urlsafe_b64decode(s)

    x_bytes = b64url_decode(jwk["x"])
    y_bytes = b64url_decode(jwk["y"])
    public_numbers = ec.EllipticCurvePublicNumbers(
        x=int.from_bytes(x_bytes, byteorder='big'),
        y=int.from_bytes(y_bytes, byteorder='big'),
        curve=ec.SECP256R1()
    )
    public_key = public_numbers.public_key()

    # 3. Decode IEEE P1363 raw signature (64 bytes) to (r, s)
    raw_sig = base64.b64decode(signature_b64)
    if len(raw_sig) != 64:
        raise ValueError("Invalid signature length: must be 64 bytes IEEE P1363")
    r = int.from_bytes(raw_sig[:32], byteorder='big')
    s = int.from_bytes(raw_sig[32:], byteorder='big')
    der_signature = utils.encode_dss_signature(r, s)

    # 4. Serialize canonical payload
    canonical_bytes = serialize_canonical(metadata)

    # 5. Verify ECDSA signature
    try:
        public_key.verify(
            der_signature,
            canonical_bytes,
            ec.ECDSA(hashes.SHA256())
        )
        return True
    except Exception:
        return False