The API is defined first, in WIT, the WebAssembly Component Model interface language. Rust exports it from its
WebAssembly component; Kotlin follows it through fixed mapping rules. Requirement tags such as
[STT-COS-002] link into the graph explorer. For the same API as JavaScript callers see it, open the
TypeScript reference, generated by jco and TypeDoc.
| World | Imports (host provides) | Exports |
|---|---|---|
hcert-codec | — | types codec cose |
hcert-verifier | wasi | types codec cose subclaims dvc vhl payloads trust verifier |
hcert-issuer | wasi | types codec cose subclaims dvc vhl payloads issuer |
hcert | — | includes hcert-verifier, hcert-issuer |
types.wit typescodec.wit codeccose.wit cosetrust.wit trustvhl.wit vhlpayload.wit subclaims, dvc, payloadspipeline.wit issuer, verifierworlds.wit hcert-codec, hcert-verifier, hcert-issuer, hcerttypes.witpackage who:smart-trust@0.1.0;
/// Types shared by every interface.
///
/// WIT has no recursive types, so arbitrary CBOR never crosses the API
/// boundary as a tree: it crosses as encoded bytes (`cbor-bytes`). The typed
/// payload (DVCMin) crosses as a record.
interface types {
/// COSE algorithm. es256 = -7 (primary), ps256 = -37 (secondary).
/// Both MUST be implemented. [STT-COS-002]
enum cose-algorithm { es256, ps256 }
/// 8-byte key identifier: first 8 bytes of SHA-256 over the DER DSC.
/// [STT-KID-001]
type kid = list<u8>;
/// CWT NumericDate: integer seconds since the Unix epoch. [STT-CWT-002]
type numeric-date = s64;
/// Encoded CBOR data item.
type cbor-bytes = list<u8>;
/// An hcert subclaim number and what it carries. Negative numbers are
/// the development range. [STT-SEL-001, STT-SEL-003]
record subclaim {
key: s32,
title: string,
/// Canonical URL of the FHIR logical model, when there is one.
logical-model: option<string>,
}
/// Bounds applied while decoding untrusted input. [STT-CBR-003,
/// STT-ZIP-002]
record decode-limits {
max-input-chars: u32,
max-inflated-bytes: u32,
max-depth: u32,
max-items: u32,
max-string-bytes: u32,
}
enum severity { blocking, warning, information }
/// One payload validation finding. `path` names the element.
record validation-issue {
path: string,
code: string,
message: string,
severity: severity,
}
/// Verification steps, in the order they run. [STT-VER-001]
enum step {
prefix,
base45,
inflate,
cose,
header,
trust-lookup,
signature,
key-usage,
cwt,
time,
hcert,
payload,
}
/// Why verification stopped. [STT-VER-008]
enum failure-reason {
bad-prefix,
unsupported-context-version,
base45-invalid,
inflate-failed,
limit-exceeded,
cose-malformed,
header-invalid,
unsupported-algorithm,
kid-not-found,
signature-invalid,
key-usage-mismatch,
expired,
not-yet-valid,
hcert-missing,
multiple-subclaims,
payload-invalid,
}
/// A non-fatal observation made while verifying.
record diagnostic {
step: step,
severity: severity,
message: string,
}
}
codec.witpackage who:smart-trust@0.1.0;
/// Steps 4, 6, 7, 8: CBOR, ZLIB, Base45 and the HC1 context identifier.
/// Pure functions: no crypto, no I/O, no clock.
interface codec {
use types.{decode-limits, cbor-bytes};
variant codec-error {
/// Character outside the alphabet, dangling character or overflow.
/// [STT-B45-002]
base45-invalid(string),
inflate-failed(string),
/// A decode-limits bound was hit. [STT-CBR-003, STT-ZIP-002]
limit-exceeded(string),
cbor-malformed(string),
/// No `HC[1-9A-Z]:` context identifier. [STT-HC1-001]
bad-prefix,
/// A well-formed context identifier other than HC1, e.g. "HC2".
unsupported-context-version(string),
}
/// RFC 9285 Base45. [STT-B45-001]
base45-encode: func(data: list<u8>) -> string;
base45-decode: func(text: string) -> result<list<u8>, codec-error>;
/// ZLIB (RFC 1950) wrapping DEFLATE (RFC 1951). [STT-ZIP-001]
zlib-compress: func(data: list<u8>, level: u8) -> list<u8>;
zlib-inflate: func(data: list<u8>, max-out: u32) -> result<list<u8>, codec-error>;
/// Returns the context identifier version character (e.g. "1") and
/// the remainder after the colon. [STT-HC1-001]
detect-context: func(text: string) -> result<tuple<string, string>, codec-error>;
/// Steps 6 to 8: COSE bytes to "HC1:..." string. The result uses only
/// QR Alphanumeric characters. [STT-QR-001]
to-hc1: func(cose: list<u8>) -> string;
/// Steps 8 to 6 reversed: "HC1:..." string to COSE bytes.
from-hc1: func(text: string, limits: decode-limits) -> result<list<u8>, codec-error>;
/// Re-encode CBOR using RFC 8949 core deterministic encoding.
/// [STT-CBR-001]
cbor-deterministic: func(cbor: cbor-bytes, limits: decode-limits) -> result<cbor-bytes, codec-error>;
}
cose.witpackage who:smart-trust@0.1.0;
/// Step 5: COSE_Sign1, key identifiers and signature formats.
interface cose {
use types.{cose-algorithm, kid, decode-limits};
variant cose-error {
malformed(string),
/// Protected header failed type/length checks. [STT-VER-003]
header-invalid(string),
unsupported-algorithm(s32),
/// ES256 signature not exactly 64 bytes, or bad DER. [STT-COS-005]
bad-signature-format(string),
signature-invalid,
bad-certificate(string),
}
/// A parsed COSE_Sign1. `protected` is kept as received so the
/// Sig_structure is rebuilt from the original bytes. [STT-COS-004]
record sign1 {
protected: list<u8>,
payload: list<u8>,
signature: list<u8>,
alg: option<cose-algorithm>,
/// The protected kid if present, else the unprotected kid.
/// [STT-COS-003]
kid: option<kid>,
kid-in-protected: bool,
/// Whether the input carried CBOR tag 18. [STT-COS-001]
tagged: bool,
}
/// First 8 bytes of SHA-256 over the DER-encoded DSC. [STT-KID-001]
kid-from-dsc: func(dsc-der: list<u8>) -> kid;
enum base64-alphabet { standard, url-safe }
/// [STT-KID-003]
kid-to-base64: func(k: kid, alphabet: base64-alphabet) -> string;
kid-from-base64: func(text: string) -> result<kid, cose-error>;
parse-sign1: func(bytes: list<u8>, limits: decode-limits) -> result<sign1, cose-error>;
/// CBOR of ["Signature1", protected, h'', payload].
sig-structure: func(protected: list<u8>, payload: list<u8>) -> list<u8>;
/// Verify a parsed COSE_Sign1 against one DSC. Low level: does not
/// check EKU, time or trust. [STT-COS-002]
verify-sign1: func(s: sign1, dsc-der: list<u8>) -> result<_, cose-error>;
/// ASN.1 DER ECDSA-Sig-Value <-> fixed-width r||s. [STT-COS-005]
ecdsa-der-to-raw: func(der: list<u8>) -> result<list<u8>, cose-error>;
ecdsa-raw-to-der: func(raw: list<u8>) -> result<list<u8>, cose-error>;
}
trust.witpackage who:smart-trust@0.1.0;
/// Trust material. The host fetches it (GDHCN DID trustlist, pinned PEM)
/// and fills the store, so the core never does network I/O and stays
/// synchronous. [STT-TRU-001, STT-GEN-012]
interface trust {
use types.{kid, numeric-date};
variant trust-error {
parse(string),
/// A DID document names a trust domain the loaded GDHCN Trust Domain
/// code system does not hold. [STT-SEL-006]
unknown-trust-domain(string),
/// A DID document's proof does not verify, or the document is not
/// one the proof profile models. [STT-TRU-006]
proof-invalid(string),
}
record trusted-dsc {
der: list<u8>,
kid: kid,
country: option<string>,
not-before: numeric-date,
not-after: numeric-date,
/// Extended Key Usage OIDs; empty means valid for any type.
/// [STT-VER-004]
extended-key-usages: list<string>,
/// Trust domain and participant, when the DSC came from a DID
/// trustlist document.
domain: option<string>,
participant: option<string>,
}
resource trust-store {
constructor();
/// Each add-* returns the number of DSCs added. [STT-TRU-005]
add-pem: func(pem: string) -> result<u32, trust-error>;
add-der: func(der: list<u8>) -> result<u32, trust-error>;
/// A GDHCN DID trustlist v2 document, embedded or reference
/// variant. [STT-TRU-002]
add-did-document: func(json: string) -> result<u32, trust-error>;
/// Same, but only after the document's proof verifies against
/// `signer`, the did:web document of the proof's verificationMethod,
/// which the host fetched; on any failure nothing is added.
/// [STT-TRU-006]
add-verified-did-document: func(json: string, signer: string) -> result<u32, trust-error>;
/// The trust domains a DID document may name, read from a GDHCN
/// Trust Domain CodeSystem (FHIR JSON) the host fetched. Data, not
/// an enum: once loaded, a DID document naming another domain is
/// refused whole; before, any domain is kept as given. Loading
/// again replaces the set. Returns the number of codes.
/// [STT-SEL-006]
load-trust-domains: func(codesystem-json: string) -> result<u32, trust-error>;
/// The loaded trust domain codes; empty when none are loaded.
trust-domains: func() -> list<string>;
/// Every DSC with this kid (kids are not unique). [STT-KID-002]
candidates: func(k: kid) -> list<trusted-dsc>;
size: func() -> u32;
clear: func();
}
/// Verify a GDHCN trust list DID document's JsonWebSignature2020 proof,
/// as the TNG key distribution service makes it: URDNA2015 over the
/// document and the proof options with pinned contexts, a detached
/// b64:false ES256 JWS whose signature may be DER or r||s. `signer` is
/// the did:web document of the proof's verificationMethod. Fails closed
/// on any term the profile does not model. [STT-TRU-006]
verify-did-proof: func(document: string, signer: string) -> result<_, trust-error>;
}
vhl.witpackage who:smart-trust@0.1.0;
/// Verifiable Health Links, as the IHE ITI VHL profile specifies them, with
/// the SMART Health Links payload format. Pure functions: the host does the
/// HTTP (ITI-YY5 manifest search, ITI-68 document GETs) over mutual TLS, holds
/// the receiver's signing key and logs audit events. [STT-VHL-010]
interface vhl {
use types.{validation-issue, numeric-date};
/// The SMART Health Links payload format that IHE VHL reuses. At HCERT
/// claim -260 key 5 it is a CBOR map with these text keys
/// (fhir-base-url is extension.fhirBaseUrl). [STT-VHL-001, STT-VHL-002]
record vhl-payload {
/// Manifest URL: a FHIR List search with _id, code, status and
/// patient.identifier.
url: string,
/// 32 random bytes, 43 base64url characters.
key: string,
/// Expiry, epoch seconds.
exp: option<s64>,
/// L and/or P, in alphabetical order.
flag: option<string>,
/// At most 80 characters.
label: option<string>,
/// Protocol version; 1 when absent.
v: option<s32>,
/// extension.fhirBaseUrl, for the OAuth with SSRAA option.
fhir-base-url: option<string>,
}
variant vhl-error {
invalid(list<validation-issue>),
/// Not of the form vhlink:/<base64url JSON>.
not-a-vhlink(string),
manifest(string),
/// Wrong key, a bad tag, or a malformed JWE.
decrypt-failed(string),
/// A JWE alg or enc other than dir / A256GCM. [STT-VHL-007]
unsupported(string),
}
/// Every rule of the payload format; with `now`, an expired link too.
/// [STT-VHL-001, STT-VHL-009]
validate: func(p: vhl-payload, now: option<numeric-date>) -> list<validation-issue>;
/// [STT-VHL-003]
to-vhlink: func(p: vhl-payload) -> string;
from-vhlink: func(text: string) -> result<vhl-payload, vhl-error>;
/// A new key from the platform's secure random generator. [STT-VHL-004]
new-key: func() -> string;
/// The ITI-YY5 Retrieve Manifest request for the host to POST.
record manifest-request {
/// <base>/List/_search
url: string,
/// application/x-www-form-urlencoded
content-type: string,
body: string,
}
/// [STT-VHL-005]
build-manifest-request: func(p: vhl-payload, recipient: string, passcode: option<string>, embedded-length-max: option<u32>) -> result<manifest-request, vhl-error>;
/// One document listed by the manifest.
record manifest-document {
/// DocumentReference.content.attachment.url, when the DocumentReference
/// was included; the host GETs it (ITI-68).
url: option<string>,
/// List.entry.item.reference, when it was not; the host reads it first.
reference: option<string>,
/// The decrypted content type (attachment.contentType).
content-type: option<string>,
title: option<string>,
}
/// The ITI-YY5 searchset Bundle. [STT-VHL-006]
parse-manifest: func(bundle-json: string) -> result<list<manifest-document>, vhl-error>;
record decrypted {
/// The JWE cty header, when present.
content-type: option<string>,
content: list<u8>,
}
/// JWE Compact, dir / A256GCM, the payload key. [STT-VHL-007]
encrypt: func(content: list<u8>, content-type: string, key: string) -> result<string, vhl-error>;
decrypt: func(jwe: string, key: string) -> result<decrypted, vhl-error>;
/// What the receiver signs for the HTTP Message Signatures option.
record signature-request {
method: string,
authority: string,
path: string,
content-type: option<string>,
body: option<list<u8>>,
keyid: string,
/// e.g. ecdsa-p256-sha256
alg: string,
created: numeric-date,
}
record signature-base {
/// RFC 9530 Content-Digest header value, when there is a body.
content-digest: option<string>,
/// Signature-Input header value.
signature-input: string,
/// The RFC 9421 signature base: the bytes the host signs.
base: list<u8>,
}
/// [STT-VHL-008]
build-signature-base: func(r: signature-request) -> signature-base;
/// The Signature header value. An ecdsa-p256-sha256 DER signature is
/// converted to r||s.
signature-header: func(signature: list<u8>, alg: string) -> result<string, vhl-error>;
}
payload.witpackage who:smart-trust@0.1.0;
/// Subclaim registry. Its defaults are the WHO-listed subclaims; the
/// subclaim a payload type is issued under is configuration, so DVC can
/// move from -6 to an assigned number without an API break.
/// [STT-SEL-001, STT-SEL-002, STT-SEL-003]
interface subclaims {
use types.{subclaim};
/// Payload kinds that have a typed codec. Others travel as raw CBOR.
enum payload-kind { dvc-min, vhl }
resource registry {
/// Starts with the IG defaults: 1, 3, 4, 5, -6, -7; VHL is 5. [STT-SEL-001]
constructor();
lookup: func(key: s32) -> option<subclaim>;
register: func(s: subclaim);
all: func() -> list<subclaim>;
/// Which subclaim number carries the given payload kind.
subclaim-for: func(kind: payload-kind) -> option<s32>;
set-subclaim-for: func(kind: payload-kind, key: s32);
}
}
/// Step 2: the DVCMin payload (trust-phw DVCMin). [STT-PAY-010]
interface dvc {
use types.{validation-issue, cbor-bytes, decode-limits};
/// DVCMinVaccineDetails. [STT-PAY-002]
record dvc-min-vaccine-details {
/// Vaccine product identifier.
vp: string,
/// Date of vaccination (FHIR date).
dv: string,
/// Supervising clinician.
cn: option<string>,
/// Issuing organization identifier.
is: option<string>,
/// Batch or lot number.
bo: string,
}
/// DVCMin. vx is 1..1: one immunization event per certificate.
/// [STT-PAY-001, STT-PAY-003, STT-PAY-005]
record dvc-min {
n: string,
dob: string,
s: string,
/// ISO 3166-1 alpha-3. [STT-PAY-007]
nt: string,
id: option<string>,
dt: option<string>,
gn: option<string>,
vx: dvc-min-vaccine-details,
v: string,
/// Elements this version does not know, kept as CBOR map entries
/// (key, encoded value). [STT-PAY-008]
extensions: list<tuple<string, cbor-bytes>>,
}
/// Cardinality, invariant (must-have-issuer-or-clinician-name) and nt
/// checks. [STT-PAY-004, STT-PAY-007]
validate: func(p: dvc-min) -> list<validation-issue>;
to-cbor: func(p: dvc-min) -> cbor-bytes;
from-cbor: func(cbor: cbor-bytes, limits: decode-limits) -> result<dvc-min, list<validation-issue>>;
}
/// What an hcert entry carries: a typed DVCMin or VHL payload, or any other
/// subclaim as encoded CBOR. [STT-GEN-003, STT-PAY-009, STT-VHL-002]
interface payloads {
use types.{cbor-bytes};
use dvc.{dvc-min};
use vhl.{vhl-payload};
record raw-payload {
subclaim: s32,
cbor: cbor-bytes,
}
variant payload {
dvc-min(dvc-min),
vhl(vhl-payload),
raw(raw-payload),
}
}
pipeline.witpackage who:smart-trust@0.1.0;
/// End-to-end issuance (steps 3 to 8).
///
/// Signing is two-phase so that any key holder can sign without the core
/// calling back into the host: prepare() returns the Sig_structure bytes,
/// the caller signs them with an HSM, KMS, Android Keystore or WebCrypto,
/// and finish() assembles the certificate. [STT-ISS-004, STT-COS-006]
interface issuer {
use types.{cose-algorithm, kid, numeric-date, validation-issue};
use payloads.{payload};
use subclaims.{registry};
record validity {
iat: numeric-date,
/// Mandatory: exp is the only revocation mechanism. [STT-CWT-003]
exp: numeric-date,
}
record issue-request {
/// One or more payloads, each under its own subclaim in the hcert
/// claim, in subclaim-key order on the wire. [STT-SEL-004]
payloads: list<payload>,
/// CWT iss, alpha-2. [STT-CWT-005]
iss: option<string>,
validity: validity,
/// DSC whose key will sign; kid is derived from it. [STT-KID-001]
dsc-der: list<u8>,
alg: cose-algorithm,
/// Refuse negative subclaims unless true. [STT-SEL-003]
allow-development-subclaims: bool,
}
variant issue-error {
payload-invalid(list<validation-issue>),
/// The request carries no payload.
no-payload,
/// Two payloads map to the same subclaim. [STT-SEL-004]
duplicate-subclaim(s32),
development-subclaim-not-allowed(s32),
unknown-subclaim(s32),
/// iat before or exp after the DSC validity. [STT-CWT-004]
validity-outside-dsc,
bad-certificate(string),
bad-signature-format(string),
}
record issued-hcert {
/// QR-ready: QR Alphanumeric characters only. [STT-QR-001]
hc1: string,
/// Raw COSE_Sign1 for 8-bit-safe transports. [STT-GEN-009]
cose: list<u8>,
}
resource pending-signature {
/// Bytes to sign (CBOR Sig_structure). [STT-COS-004]
to-be-signed: func() -> list<u8>;
algorithm: func() -> cose-algorithm;
kid: func() -> kid;
}
/// Phase 1: validate, build CWT, CBOR and protected header.
prepare: func(req: issue-request, reg: borrow<registry>) -> result<pending-signature, issue-error>;
/// Phase 2: attach the signature. ES256 accepts r||s or DER and always
/// emits r||s. [STT-COS-005]
finish: func(pending: pending-signature, signature: list<u8>) -> result<issued-hcert, issue-error>;
}
/// Verification (QR Code Generation section 11): the steps in STT-VER-001
/// order, each fixed by the IG rather than a policy switch. The hcert claim
/// may carry several entries (HCERT 3.2.7) [STT-SEL-004]; trust-list presence
/// is the DSC validity check [STT-VER-006]; EKU is checked against each
/// entry's certificate type [STT-VER-004].
interface verifier {
use types.{kid, numeric-date, decode-limits, cbor-bytes, step, failure-reason, diagnostic};
use payloads.{payload};
use trust.{trust-store, trusted-dsc};
use subclaims.{registry};
record verifier-options {
limits: decode-limits,
clock-skew-seconds: u32,
/// Refuse an hcert claim with more entries than this
/// (multiple-subclaims): a deployment's strict profile. None: any
/// number. [STT-SEL-004]
max-payloads: option<u32>,
}
/// One entry of the hcert claim. [STT-SEL-004]
record hcert-entry {
subclaim: s32,
/// Raw CBOR for a subclaim the registry does not know. [STT-SEL-002]
payload: payload,
/// False when the DSC's EKU does not permit this entry's
/// certificate type; the entry is returned, marked, and a
/// diagnostic says so. [STT-VER-004]
key-usage-authorised: bool,
}
record verified-hcert {
iss: option<string>,
iat: numeric-date,
exp: numeric-date,
/// The valid entries, in subclaim-key order. An entry whose payload
/// fails its model check is left out and reported as a diagnostic;
/// verification fails at payload only when none is left, and at
/// key-usage only when no entry is authorised. [STT-SEL-004]
entries: list<hcert-entry>,
/// The DSC that verified the signature.
dsc: trusted-dsc,
/// Unknown CWT claims, as (encoded key, encoded value).
/// [STT-CWT-006]
other-claims: list<tuple<cbor-bytes, cbor-bytes>>,
diagnostics: list<diagnostic>,
}
record verification-failure {
step: step,
reason: failure-reason,
detail: string,
diagnostics: list<diagnostic>,
}
/// Payload content is only reachable through `verified`.
/// [STT-VER-002, STT-VER-008]
variant verification-result {
verified(verified-hcert),
failed(verification-failure),
}
/// Decoded, NOT verified: no trust lookup, no signature check. For
/// display and triage where verification is impossible (an offline
/// household visit, journey J2); never a basis for trust. [STT-VER-011]
/// One entry of an unverified hcert claim.
record unverified-entry {
subclaim: s32,
/// The payload, when it decoded and met its model.
payload: option<payload>,
}
record unverified-hcert {
kid: option<kid>,
iss: option<string>,
iat: option<numeric-date>,
exp: option<numeric-date>,
/// Every entry of the hcert claim, in subclaim-key order.
entries: list<unverified-entry>,
diagnostics: list<diagnostic>,
}
/// `now` is passed in: no ambient clock. [STT-VER-005]
verify: func(hc1: string, store: borrow<trust-store>, reg: borrow<registry>, now: numeric-date, options: verifier-options) -> verification-result;
/// Same, for raw COSE_Sign1 bytes. [STT-GEN-009]
verify-cose: func(cose: list<u8>, store: borrow<trust-store>, reg: borrow<registry>, now: numeric-date, options: verifier-options) -> verification-result;
/// [STT-VER-011]
read-unverified: func(hc1: string, reg: borrow<registry>, limits: decode-limits) -> result<unverified-hcert, verification-failure>;
}
worlds.witpackage who:smart-trust@0.1.0;
/// Pure encoding and decoding: the pipeline steps on their own. No host
/// imports. [STT-GEN-002]
world hcert-codec {
export types;
export codec;
export cose;
}
/// Verifier: what a scanning app or border system embeds. The host supplies
/// secure random bytes (wasi:random), which VHL keys and JWE IVs need; the
/// verifier itself draws none. [STT-VHL-004, STT-VHL-007]
world hcert-verifier {
import wasi:random/random@0.2.6;
export types;
export codec;
export cose;
export subclaims;
export dvc;
export vhl;
export payloads;
export trust;
export verifier;
}
/// Issuer: what a clinic or national issuance service embeds. Randomness
/// as for the verifier; signing stays two-phase, so no key enters.
world hcert-issuer {
import wasi:random/random@0.2.6;
export types;
export codec;
export cose;
export subclaims;
export dvc;
export vhl;
export payloads;
export issuer;
}
/// Everything.
world hcert {
include hcert-verifier;
include hcert-issuer;
}