Skip to content

Verification

Status and BCP 14 terminology are defined in Status of This Document.

This document defines the OSAP verification algorithm and verification-result format. A result reports signature validity, hash matching, revocation status, and trust interpretation separately; it does not produce an aggregate "verified" result.

1. Inputs and Outputs

Inputs:

  1. An attestation envelope (REQUIRED).
  2. The asset bytes (OPTIONAL — without them, hash matching is skipped and reported as unknown).
  3. Issuer metadata for the envelope's issuer (fetched or cached; see identity.md).
  4. The issuer's revocation list (fetched or cached; OPTIONAL).
  5. A user-selected trust bundle (OPTIONAL; see trust-bundles.md).
  6. Optionally, transparency log data for earlier-source checks (informative; see transparency-logs.md).

Output: a verification result validating against osap-verification-result.schema.json, with fields defined in data-model.md § 8 and labels drawn from ui-labels.md.

A verifier MUST be able to run this algorithm fully offline when inputs 1–5 are available locally (see privacy.md).

2. Result Semantics

  • Boolean result fields are three-valued: true, false, or null ("could not be checked"). A verifier MUST distinguish a failed check from an unperformed one.
  • Each step below states its outcomes. Steps marked fatal stop the algorithm; subsequent unperformed checks are reported as null. Non-fatal negative outcomes are recorded and the algorithm continues.

Note (informative): Continuing after a non-fatal outcome preserves independent results, such as signature validity for a revoked attestation or an invalid signature from a trusted issuer.

3. Algorithm

The verifier MUST perform the following steps in order.

Step 1 — Schema validation

Validate the envelope against osap-envelope.schema.json, including "osap": "0.1".

  • Failure is fatal: report valid_signature: null and an error label "Attestation is not a valid OSAP 0.1 document". No further steps run.

Step 2 — Canonicalization

Compute the JCS (RFC 8785) canonical form of the envelope with proof.proofValue removed, per envelope.md § 6.2.

  • Failure (document cannot be canonicalized) is fatal and reported like Step 1 failure.

Step 3 — Asset hash match

If asset bytes are available: compute SHA-256 over the exact bytes and compare the lowercase hex digest against each subject.asset_hashes entry with alg: "sha256", scope: "exact-bytes".

  • Match: asset_hash_match: true; success label "Exact asset matches signed fingerprint".
  • No match: asset_hash_match: false; error label "Asset hash does not match signed fingerprint". Not fatal — the signature may still verify, proving the issuer signed some other bytes, which the result makes explicit.
  • Asset bytes unavailable, or envelope has no subject (e.g. RevocationAttestation): asset_hash_match: null; unknown label "Asset not available: fingerprint match not checked" (omit for envelopes without a subject).

Step 4 — Issuer key discovery

Resolve proof.verificationMethod via issuer metadata (identity.md): fetch or load https://<domain>/.well-known/osap.json, confirm its id matches the issuer DID, and locate a usable Multikey entry whose id equals proof.verificationMethod.

Then evaluate key validity at the attestation's issued_at time using the key's valid_from/valid_until (identity.md § 5).

  • Key found and valid at issued_at: issuer_key_valid: true.
  • Key found but not valid at issued_at: issuer_key_valid: false; error label "Signing key was not valid when attestation was issued". Continue to Step 5 (the cryptographic check is still informative).
  • Metadata unreachable, id mismatch, or key not found: issuer_key_valid: null (mismatch/not-found MAY be reported as false); this is fatal for the signature check onlyvalid_signature: null, unknown label "Issuer key not found: signature not checked". Continue at Step 6.

Step 5 — Signature verification

Verify the Ed25519 signature per envelope.md § 6.2, and check issuer.key_id equals proof.verificationMethod.

  • Valid: valid_signature: true; success label "Signature valid: signed by {issuer name or domain}".
  • Invalid (bad signature, malformed proofValue, key_id mismatch, or unsupported type/cryptosuite/proofPurpose): valid_signature: false; error label "Signature invalid". Not fatal.

Step 6 — Revocation check

Consult the issuer's revocation list (from revocation.status_url in the envelope or revocations_url in issuer metadata; fetched or cached), and any known RevocationAttestation whose revokes equals this envelope's id.

  • Revoked: revoked: true; warning label "Attestation revoked by issuer" (append the reason when present). A revoked attestation remains historical evidence; verifiers MUST NOT suppress it.
  • Not listed: revoked: false; success label "Attestation not revoked".
  • List unavailable: revoked: null; unknown label "Revocation status could not be checked". Verifiers SHOULD also warn when only a stale cached list is available (see threat-model.md, revocation suppression).

Additionally, if the signing key appears in revoked_keys, apply the rules of identity.md § 6.

If a CorrectionAttestation correcting this envelope is known, the verifier MUST add the info label "Correction issued by source" with a reference to it.

Step 7 — Trust-bundle evaluation

If the user selected no bundle: set trust_result to { "status": "no_bundle", "bundle": null } and add the info label "No trust bundle selected: issuer not evaluated". Skip Step 8.

Otherwise evaluate the issuer against the selected bundle per trust-bundles.md § 5:

  • Issuer has no entry in the bundle: status: "outside_bundle", historical_status: null, current_status: null; warning label "Issuer outside selected trust bundle".
  • Issuer has an entry: proceed to Step 8.

In all cases where claims are present and the historical status is not trusted (including outside_bundle and no_bundle), the verifier MUST include the warning label "Claim is made by issuer, not proven as true". Verifiers MAY include it unconditionally.

Step 8 — Temporal trust evaluation

Per temporal-trust.md, evaluate the matched entry's trust_periods twice:

  1. Historical: find the period(s) covering the attestation's issued_at; the resulting status is historical_status and is also reported as trust_result.status.
  2. Current: find the period(s) covering the verification time; the resulting status is current_status.

Scope matching (which periods apply to which claims) is defined in trust-bundles.md § 5. issued_at not covered by any period yields historical_status: null; likewise for current time.

Both statuses MUST be reported. Current distrust MUST NOT erase or hide historical trust, and historical trust MUST NOT hide current distrust. When the two differ, the verifier MUST label both, e.g. info label "Published during previously trusted period" together with warning label "Source currently marked high caution by selected trust bundle".

Optional step — Earlier source check (informative)

A verifier with access to transparency log data may look up the asset's exact-bytes hash and report the earliest known attestation of the same fingerprint by a different issuer in earlier_source, with the warning label "Earlier matching source found from {issuer}". See transparency-logs.md and example ../examples/verification-result-earlier-source-found.json.

4. Verification Result Examples

Note (informative): In the second example, the signature and asset checks succeed while the issuer remains outside the selected trust bundle. The result therefore reports "Signature valid: signed by trustme.example" separately from "Issuer outside selected trust bundle"; neither result establishes the truth of the issuer's claims. See threat-model.md.

5. Error and Warning Conditions

The full normative catalogue of labels, levels, and display requirements is in ui-labels.md. Verifiers MUST use labels that name the specific check performed and MUST NOT emit any aggregate "verified" indicator.