Skip to content

Implementation Guide

Universal Manifest (UM) is a specification. You can implement it in any language. The TypeScript helper and um-typescript repository are reference implementations, not normative dependencies.

This guide takes you from first parse to conformance-ready implementation.

Prerequisites:

  • JSON parser + RFC 3339 timestamp parsing.
  • JCS + Ed25519 libraries if targeting v0.2.
  • Access to spec and conformance artifacts.

Normative references:

  • /spec/v01/
  • /spec/v02/
  • /conformance/v01/
  • /conformance/v02/

Conformance decision tree

Text fallback:

  1. Consume only -> v0.1-baseline consumer.
  2. Produce manifests -> include v0.1-baseline issuer behavior.
  3. Need tamper protection -> v0.2-baseline.
  4. Need revocation checks -> v0.2-extended.

Conformance levels:

  • v0.1-baseline: required fields, TTL, unknown-field tolerance.
  • v0.2-baseline: v0.1 baseline plus JCS + Ed25519 profile verification/signing.
  • v0.2-extended: v0.2 baseline plus revocation-aware policy checks.

Required fields:

  • @context
  • @id
  • @type (includes um:Manifest)
  • manifestVersion
  • subject
  • issuedAt
  • expiresAt

Required behavior:

  1. Parse as JSON object.
  2. Reject missing required fields.
  3. Parse and validate timestamps.
  4. Reject for use when now > expiresAt.
  5. Safely ignore unknown fields.

TypeScript example:

function validateForUse(manifest: Record<string, unknown>, now = new Date()): void {
const required = ['@context', '@id', '@type', 'manifestVersion', 'subject', 'issuedAt', 'expiresAt']
for (const key of required) {
if (!(key in manifest)) throw new Error(`missing ${key}`)
}
const t = manifest['@type']
const hasManifestType = typeof t === 'string'
? t === 'um:Manifest'
: Array.isArray(t) && t.includes('um:Manifest')
if (!hasManifestType) throw new Error('invalid @type')
const issued = new Date(String(manifest['issuedAt']))
const expires = new Date(String(manifest['expiresAt']))
if (Number.isNaN(issued.getTime()) || Number.isNaN(expires.getTime())) throw new Error('invalid timestamp')
if (issued > expires || now > expires) throw new Error('manifest is not valid for use')
}

Python pseudocode:

required = ["@context", "@id", "@type", "manifestVersion", "subject", "issuedAt", "expiresAt"]
for key in required:
if key not in manifest:
raise ValueError(f"missing {key}")
if now() > parse_rfc3339(manifest["expiresAt"]):
raise ValueError("expired")

Go pseudocode:

// Parse required fields and enforce TTL; ignore unknown fields by default.

Issuer essentials:

  • Set unique @id (recommended urn:uuid:<uuidv4>).
  • Set stable subject URI.
  • Set issuedAt / expiresAt with explicit TTL.
  • Set exact manifestVersion.

Example (v0.1):

{
"@context": "https://universalmanifest.net/ns/universal-manifest/v0.1/schema.jsonld",
"@id": "urn:uuid:af58f76e-a8f8-4b3a-bf2f-c8b8bb76a8de",
"@type": "um:Manifest",
"manifestVersion": "0.1",
"subject": "did:key:z6Mki...",
"issuedAt": "2026-03-02T00:00:00Z",
"expiresAt": "2026-03-02T12:00:00Z",
"shards": []
}

Profile requirements:

  • signature.algorithm = "Ed25519"
  • signature.canonicalization = "JCS-RFC8785"

Signing flow:

  1. Remove signature from payload.
  2. Canonicalize with JCS.
  3. Sign canonical bytes with Ed25519.
  4. Attach signature object.

TypeScript sketch:

const unsigned = { ...manifest }
delete unsigned.signature
const canonical = canonicalize(unsigned)
const sig = await sign(new TextEncoder().encode(canonical), privateKey)

Verification flow:

  1. Run baseline checks.
  2. Validate profile pair (Ed25519, JCS-RFC8785).
  3. Resolve/read public key.
  4. Recompute canonical signing bytes.
  5. Verify signature; reject on failure.

Get runner and execute:

Terminal window
git clone https://github.com/grigb/universal-manifest.git
cd universal-manifest/conformance/runner
npm ci
node ./cli.mjs --mode command --adapter-command "python3 /path/to/adapter.py" --report ./conformance-report.json

Adapter response contract:

{
"result": "accept",
"reason": "validated"
}

Pitfalls:

  • Rejecting unknown fields.
  • Skipping TTL checks.
  • Signing non-canonical JSON.
  • Accepting unsupported signature profiles.

FAQ:

  1. What JSON-LD processing do I need?
  • None for baseline conformance.
  1. Can I use a different signature algorithm for v0.2 conformance?
  • No. v0.2 baseline requires Ed25519 + JCS.
  1. How do I handle unknown fields?
  • Ignore safely.
  1. What minimum manifest size should I support?
  • At least 1 MB.
  1. How do I test implementation correctness?
  • Run conformance fixtures and compare with expected.json.
  1. Can I extend the manifest with custom fields?
  • Yes.
  1. What about privacy/GDPR?
  • Use opaque IDs and minimal disclosure.
  1. How do I resolve a UMID?
  • GET https://myum.net/{UMID}.
  1. What if the resolver is down?
  • Use cached manifests only while still within TTL.

Submission package:

  1. conformance-report.json.
  2. Implementation metadata (name/version/language).
  3. Runner command and adapter reference.
  4. CI/local evidence links.