SMART Trust HCERT library tools
SMART Trust Tools
Kotlin Multiplatform · generated from the WIT contract

Kotlin API

Declarations only, with no behaviour, implemented by hcert-core. tools/wit2kotlin/wit2kotlin.py generates them from the contract, so the Kotlin and Rust libraries cannot drift apart, but the Kotlin API names no WIT: records become data classes, variants sealed interfaces, resources AutoCloseable interfaces, and a call that can fail returns HcertResult<T, E>. Each exported interface becomes a Kotlin interface the library implements, for example Codec or Verifier; imported ones (Signer, Terminology) are implemented by the host application. The entry point is object Hcert: Hcert.verifier, Hcert.issuer and so on.

The module targets the JVM (Java 11+), which Android uses as-is, and Node.js through Kotlin/JS IR with generated TypeScript declarations. Regenerate with python3 tools/wit2kotlin/wit2kotlin.py; CI fails if the API is out of date with the contract.

Files

HcertResult.kt

source
// GENERATED by tools/wit2kotlin/wit2kotlin.py. Do not edit.
package who.smart.trust

/**
 * The outcome of a call that can fail in an expected way: the value, or an
 * error saying which step failed and why.
 *
 * Expected failures are values, not exceptions [STT-VER-008], so the JVM and
 * Android API returns this instead of throwing. Kotlin's own `Result` cannot
 * carry a typed error, hence this type. The Kotlin/JS facade may throw, to keep
 * one TypeScript surface [STT-GEN-011].
 */
sealed interface HcertResult<out T, out E> {
    data class Ok<out T>(val value: T) : HcertResult<T, Nothing>

    data class Err<out E>(val error: E) : HcertResult<Nothing, E>
}

inline fun <T, E, R> HcertResult<T, E>.fold(onOk: (T) -> R, onErr: (E) -> R): R = when (this) {
    is HcertResult.Ok -> onOk(value)
    is HcertResult.Err -> onErr(error)
}

fun <T, E> HcertResult<T, E>.getOrNull(): T? = (this as? HcertResult.Ok)?.value

codec/Codec.kt

source
// GENERATED by tools/wit2kotlin/wit2kotlin.py from api/wit/codec.wit (interface codec). Do not edit.
package who.smart.trust.codec

import who.smart.trust.HcertResult
import who.smart.trust.types.CborBytes
import who.smart.trust.types.DecodeLimits

sealed interface CodecError {
    /**
     * Character outside the alphabet, dangling character or overflow.
     * [STT-B45-002]
     */
    data class Base45Invalid(val value: String) : CodecError

    data class InflateFailed(val value: String) : CodecError

    /** A decode-limits bound was hit. [STT-CBR-003, STT-ZIP-002] */
    data class LimitExceeded(val value: String) : CodecError

    data class CborMalformed(val value: String) : CodecError

    /** No `HC[1-9A-Z]:` context identifier. [STT-HC1-001] */
    data object BadPrefix : CodecError

    /** A well-formed context identifier other than HC1, e.g. "HC2". */
    data class UnsupportedContextVersion(val value: String) : CodecError
}

/**
 * Steps 4, 6, 7, 8: CBOR, ZLIB, Base45 and the HC1 context identifier.
 * Pure functions: no crypto, no I/O, no clock.
 *
 * Implemented by the library.
 */
interface Codec {
    /** RFC 9285 Base45. [STT-B45-001] */
    fun base45Encode(data: ByteArray): String

    fun base45Decode(text: String): HcertResult<ByteArray, CodecError>

    /** ZLIB (RFC 1950) wrapping DEFLATE (RFC 1951). [STT-ZIP-001] */
    fun zlibCompress(data: ByteArray, level: Int): ByteArray

    fun zlibInflate(data: ByteArray, maxOut: Int): HcertResult<ByteArray, CodecError>

    /**
     * Returns the context identifier version character (e.g. "1") and
     * the remainder after the colon. [STT-HC1-001]
     */
    fun detectContext(text: String): HcertResult<Pair<String, String>, CodecError>

    /**
     * Steps 6 to 8: COSE bytes to "HC1:..." string. The result uses only
     * QR Alphanumeric characters. [STT-QR-001]
     */
    fun toHc1(cose: ByteArray): String

    /** Steps 8 to 6 reversed: "HC1:..." string to COSE bytes. */
    fun fromHc1(text: String, limits: DecodeLimits): HcertResult<ByteArray, CodecError>

    /**
     * Re-encode CBOR using RFC 8949 core deterministic encoding.
     * [STT-CBR-001]
     */
    fun cborDeterministic(cbor: CborBytes, limits: DecodeLimits): HcertResult<CborBytes, CodecError>
}

cose/Cose.kt

source
// GENERATED by tools/wit2kotlin/wit2kotlin.py from api/wit/cose.wit (interface cose). Do not edit.
package who.smart.trust.cose

import who.smart.trust.HcertResult
import who.smart.trust.types.CoseAlgorithm
import who.smart.trust.types.DecodeLimits
import who.smart.trust.types.Kid

sealed interface CoseError {
    data class Malformed(val value: String) : CoseError

    /** Protected header failed type/length checks. [STT-VER-003] */
    data class HeaderInvalid(val value: String) : CoseError

    data class UnsupportedAlgorithm(val value: Int) : CoseError

    /** ES256 signature not exactly 64 bytes, or bad DER. [STT-COS-005] */
    data class BadSignatureFormat(val value: String) : CoseError

    data object SignatureInvalid : CoseError

    data class BadCertificate(val value: String) : CoseError
}

/**
 * A parsed COSE_Sign1. `protected` is kept as received so the
 * Sig_structure is rebuilt from the original bytes. [STT-COS-004]
 */
data class Sign1(
    val protected: ByteArray,
    val payload: ByteArray,
    val signature: ByteArray,
    val alg: CoseAlgorithm? = null,
    /**
     * The protected kid if present, else the unprotected kid.
     * [STT-COS-003]
     */
    val kid: Kid? = null,
    val kidInProtected: Boolean,
    /** Whether the input carried CBOR tag 18. [STT-COS-001] */
    val tagged: Boolean,
)

enum class Base64Alphabet {
    STANDARD,
    URL_SAFE,
}

/**
 * Step 5: COSE_Sign1, key identifiers and signature formats.
 *
 * Implemented by the library.
 */
interface Cose {
    /** First 8 bytes of SHA-256 over the DER-encoded DSC. [STT-KID-001] */
    fun kidFromDsc(dscDer: ByteArray): Kid

    /** [STT-KID-003] */
    fun kidToBase64(k: Kid, alphabet: Base64Alphabet): String

    fun kidFromBase64(text: String): HcertResult<Kid, CoseError>

    fun parseSign1(bytes: ByteArray, limits: DecodeLimits): HcertResult<Sign1, CoseError>

    /** CBOR of ["Signature1", protected, h'', payload]. */
    fun sigStructure(protected: ByteArray, payload: ByteArray): ByteArray

    /**
     * Verify a parsed COSE_Sign1 against one DSC. Low level: does not
     * check EKU, time or trust. [STT-COS-002]
     */
    fun verifySign1(s: Sign1, dscDer: ByteArray): HcertResult<Unit, CoseError>

    /** ASN.1 DER ECDSA-Sig-Value <-> fixed-width r||s. [STT-COS-005] */
    fun ecdsaDerToRaw(der: ByteArray): HcertResult<ByteArray, CoseError>

    fun ecdsaRawToDer(raw: ByteArray): HcertResult<ByteArray, CoseError>
}

dvc/Dvc.kt

source
// GENERATED by tools/wit2kotlin/wit2kotlin.py from api/wit/payload.wit (interface dvc). Do not edit.
package who.smart.trust.dvc

import who.smart.trust.HcertResult
import who.smart.trust.types.CborBytes
import who.smart.trust.types.DecodeLimits
import who.smart.trust.types.ValidationIssue

/** DVCMinVaccineDetails. [STT-PAY-002] */
data class DvcMinVaccineDetails(
    /** Vaccine product identifier. */
    val vp: String,
    /** Date of vaccination (FHIR date). */
    val dv: String,
    /** Supervising clinician. */
    val cn: String? = null,
    /** Issuing organization identifier. */
    val `is`: String? = null,
    /** Batch or lot number. */
    val bo: String,
)

/**
 * DVCMin. vx is 1..1: one immunization event per certificate.
 * [STT-PAY-001, STT-PAY-003, STT-PAY-005]
 */
data class DvcMin(
    val n: String,
    val dob: String,
    val s: String,
    /** ISO 3166-1 alpha-3. [STT-PAY-007] */
    val nt: String,
    val id: String? = null,
    val dt: String? = null,
    val gn: String? = null,
    val vx: DvcMinVaccineDetails,
    val v: String,
    /**
     * Elements this version does not know, kept as CBOR map entries
     * (key, encoded value). [STT-PAY-008]
     */
    val extensions: List<Pair<String, CborBytes>>,
)

/**
 * Step 2: the DVCMin payload (trust-phw DVCMin). [STT-PAY-010]
 *
 * Implemented by the library.
 */
interface Dvc {
    /**
     * Cardinality, invariant (must-have-issuer-or-clinician-name) and nt
     * checks. [STT-PAY-004, STT-PAY-007]
     */
    fun validate(p: DvcMin): List<ValidationIssue>

    fun toCbor(p: DvcMin): CborBytes

    fun fromCbor(cbor: CborBytes, limits: DecodeLimits): HcertResult<DvcMin, List<ValidationIssue>>
}

issuer/Issuer.kt

source
// GENERATED by tools/wit2kotlin/wit2kotlin.py from api/wit/pipeline.wit (interface issuer). Do not edit.
package who.smart.trust.issuer

import who.smart.trust.HcertResult
import who.smart.trust.payloads.Payload
import who.smart.trust.subclaims.Registry
import who.smart.trust.types.CoseAlgorithm
import who.smart.trust.types.Kid
import who.smart.trust.types.NumericDate
import who.smart.trust.types.ValidationIssue

data class Validity(
    val iat: NumericDate,
    /** Mandatory: exp is the only revocation mechanism. [STT-CWT-003] */
    val exp: NumericDate,
)

data class IssueRequest(
    /**
     * One or more payloads, each under its own subclaim in the hcert
     * claim, in subclaim-key order on the wire. [STT-SEL-004]
     */
    val payloads: List<Payload>,
    /** CWT iss, alpha-2. [STT-CWT-005] */
    val iss: String? = null,
    val validity: Validity,
    /** DSC whose key will sign; kid is derived from it. [STT-KID-001] */
    val dscDer: ByteArray,
    val alg: CoseAlgorithm,
    /** Refuse negative subclaims unless true. [STT-SEL-003] */
    val allowDevelopmentSubclaims: Boolean,
)

sealed interface IssueError {
    data class PayloadInvalid(val value: List<ValidationIssue>) : IssueError

    /** The request carries no payload. */
    data object NoPayload : IssueError

    /** Two payloads map to the same subclaim. [STT-SEL-004] */
    data class DuplicateSubclaim(val value: Int) : IssueError

    data class DevelopmentSubclaimNotAllowed(val value: Int) : IssueError

    data class UnknownSubclaim(val value: Int) : IssueError

    /** iat before or exp after the DSC validity. [STT-CWT-004] */
    data object ValidityOutsideDsc : IssueError

    data class BadCertificate(val value: String) : IssueError

    data class BadSignatureFormat(val value: String) : IssueError
}

data class IssuedHcert(
    /** QR-ready: QR Alphanumeric characters only. [STT-QR-001] */
    val hc1: String,
    /** Raw COSE_Sign1 for 8-bit-safe transports. [STT-GEN-009] */
    val cose: ByteArray,
)

interface PendingSignature : AutoCloseable {
    /** Bytes to sign (CBOR Sig_structure). [STT-COS-004] */
    fun toBeSigned(): ByteArray

    fun algorithm(): CoseAlgorithm

    fun kid(): Kid
}

/**
 * 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]
 *
 * Implemented by the library.
 */
interface Issuer {
    /** Phase 1: validate, build CWT, CBOR and protected header. */
    fun prepare(req: IssueRequest, reg: Registry): HcertResult<PendingSignature, IssueError>

    /**
     * Phase 2: attach the signature. ES256 accepts r||s or DER and always
     * emits r||s. [STT-COS-005]
     */
    fun finish(pending: PendingSignature, signature: ByteArray): HcertResult<IssuedHcert, IssueError>
}

payloads/Payloads.kt

source
// GENERATED by tools/wit2kotlin/wit2kotlin.py from api/wit/payload.wit (interface payloads). Do not edit.
package who.smart.trust.payloads

import who.smart.trust.dvc.DvcMin
import who.smart.trust.types.CborBytes
import who.smart.trust.vhl.VhlPayload

data class RawPayload(
    val subclaim: Int,
    val cbor: CborBytes,
)

sealed interface Payload {
    data class DvcMin(val value: who.smart.trust.dvc.DvcMin) : Payload

    data class Vhl(val value: VhlPayload) : Payload

    data class Raw(val value: RawPayload) : Payload
}

subclaims/Subclaims.kt

source
// GENERATED by tools/wit2kotlin/wit2kotlin.py from api/wit/payload.wit (interface subclaims). Do not edit.
package who.smart.trust.subclaims

import who.smart.trust.types.Subclaim

/** Payload kinds that have a typed codec. Others travel as raw CBOR. */
enum class PayloadKind {
    DVC_MIN,
    VHL,
}

interface Registry : AutoCloseable {
    fun lookup(key: Int): Subclaim?

    fun register(s: Subclaim)

    fun all(): List<Subclaim>

    /** Which subclaim number carries the given payload kind. */
    fun subclaimFor(kind: PayloadKind): Int?

    fun setSubclaimFor(kind: PayloadKind, key: Int)
}

/**
 * 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]
 *
 * Implemented by the library.
 */
interface Subclaims {
    /** Starts with the IG defaults: 1, 3, 4, 5, -6, -7; VHL is 5. [STT-SEL-001] */
    fun newRegistry(): Registry
}

trust/Trust.kt

source
// GENERATED by tools/wit2kotlin/wit2kotlin.py from api/wit/trust.wit (interface trust). Do not edit.
package who.smart.trust.trust

import who.smart.trust.HcertResult
import who.smart.trust.types.Kid
import who.smart.trust.types.NumericDate

sealed interface TrustError {
    data class Parse(val value: String) : TrustError

    /**
     * A DID document names a trust domain the loaded GDHCN Trust Domain
     * code system does not hold. [STT-SEL-006]
     */
    data class UnknownTrustDomain(val value: String) : TrustError

    /**
     * A DID document's proof does not verify, or the document is not
     * one the proof profile models. [STT-TRU-006]
     */
    data class ProofInvalid(val value: String) : TrustError
}

data class TrustedDsc(
    val der: ByteArray,
    val kid: Kid,
    val country: String? = null,
    val notBefore: NumericDate,
    val notAfter: NumericDate,
    /**
     * Extended Key Usage OIDs; empty means valid for any type.
     * [STT-VER-004]
     */
    val extendedKeyUsages: List<String>,
    /**
     * Trust domain and participant, when the DSC came from a DID
     * trustlist document.
     */
    val domain: String? = null,
    val participant: String? = null,
)

interface TrustStore : AutoCloseable {
    /** Each add-* returns the number of DSCs added. [STT-TRU-005] */
    fun addPem(pem: String): HcertResult<Int, TrustError>

    fun addDer(der: ByteArray): HcertResult<Int, TrustError>

    /**
     * A GDHCN DID trustlist v2 document, embedded or reference
     * variant. [STT-TRU-002]
     */
    fun addDidDocument(json: String): HcertResult<Int, TrustError>

    /**
     * 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]
     */
    fun addVerifiedDidDocument(json: String, signer: String): HcertResult<Int, TrustError>

    /**
     * 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]
     */
    fun loadTrustDomains(codesystemJson: String): HcertResult<Int, TrustError>

    /** The loaded trust domain codes; empty when none are loaded. */
    fun trustDomains(): List<String>

    /** Every DSC with this kid (kids are not unique). [STT-KID-002] */
    fun candidates(k: Kid): List<TrustedDsc>

    fun size(): Int

    fun clear()
}

/**
 * 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]
 *
 * Implemented by the library.
 */
interface Trust {
    /**
     * 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]
     */
    fun verifyDidProof(document: String, signer: String): HcertResult<Unit, TrustError>

    /** Creates a new [TrustStore]. */
    fun newTrustStore(): TrustStore
}

types/Types.kt

source
// GENERATED by tools/wit2kotlin/wit2kotlin.py from api/wit/types.wit (interface types). Do not edit.
package who.smart.trust.types

/**
 * COSE algorithm. es256 = -7 (primary), ps256 = -37 (secondary).
 * Both MUST be implemented. [STT-COS-002]
 */
enum class CoseAlgorithm {
    ES256,
    PS256,
}

/**
 * 8-byte key identifier: first 8 bytes of SHA-256 over the DER DSC.
 * [STT-KID-001]
 */
typealias Kid = ByteArray

/** CWT NumericDate: integer seconds since the Unix epoch. [STT-CWT-002] */
typealias NumericDate = Long

/** Encoded CBOR data item. */
typealias CborBytes = ByteArray

/**
 * An hcert subclaim number and what it carries. Negative numbers are
 * the development range. [STT-SEL-001, STT-SEL-003]
 */
data class Subclaim(
    val key: Int,
    val title: String,
    /** Canonical URL of the FHIR logical model, when there is one. */
    val logicalModel: String? = null,
)

/**
 * Bounds applied while decoding untrusted input. [STT-CBR-003,
 * STT-ZIP-002]
 */
data class DecodeLimits(
    val maxInputChars: Int,
    val maxInflatedBytes: Int,
    val maxDepth: Int,
    val maxItems: Int,
    val maxStringBytes: Int,
)

enum class Severity {
    BLOCKING,
    WARNING,
    INFORMATION,
}

/** One payload validation finding. `path` names the element. */
data class ValidationIssue(
    val path: String,
    val code: String,
    val message: String,
    val severity: Severity,
)

/** Verification steps, in the order they run. [STT-VER-001] */
enum class Step {
    PREFIX,
    BASE45,
    INFLATE,
    COSE,
    HEADER,
    TRUST_LOOKUP,
    SIGNATURE,
    KEY_USAGE,
    CWT,
    TIME,
    HCERT,
    PAYLOAD,
}

/** Why verification stopped. [STT-VER-008] */
enum class FailureReason {
    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. */
data class Diagnostic(
    val step: Step,
    val severity: Severity,
    val message: String,
)

verifier/Verifier.kt

source
// GENERATED by tools/wit2kotlin/wit2kotlin.py from api/wit/pipeline.wit (interface verifier). Do not edit.
package who.smart.trust.verifier

import who.smart.trust.HcertResult
import who.smart.trust.payloads.Payload
import who.smart.trust.subclaims.Registry
import who.smart.trust.trust.TrustStore
import who.smart.trust.trust.TrustedDsc
import who.smart.trust.types.CborBytes
import who.smart.trust.types.DecodeLimits
import who.smart.trust.types.Diagnostic
import who.smart.trust.types.FailureReason
import who.smart.trust.types.Kid
import who.smart.trust.types.NumericDate
import who.smart.trust.types.Step

data class VerifierOptions(
    val limits: DecodeLimits,
    val clockSkewSeconds: Int,
    /**
     * Refuse an hcert claim with more entries than this
     * (multiple-subclaims): a deployment's strict profile. None: any
     * number. [STT-SEL-004]
     */
    val maxPayloads: Int? = null,
)

/** One entry of the hcert claim. [STT-SEL-004] */
data class HcertEntry(
    val subclaim: Int,
    /** Raw CBOR for a subclaim the registry does not know. [STT-SEL-002] */
    val 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]
     */
    val keyUsageAuthorised: Boolean,
)

data class VerifiedHcert(
    val iss: String? = null,
    val iat: NumericDate,
    val exp: NumericDate,
    /**
     * 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]
     */
    val entries: List<HcertEntry>,
    /** The DSC that verified the signature. */
    val dsc: TrustedDsc,
    /**
     * Unknown CWT claims, as (encoded key, encoded value).
     * [STT-CWT-006]
     */
    val otherClaims: List<Pair<CborBytes, CborBytes>>,
    val diagnostics: List<Diagnostic>,
)

data class VerificationFailure(
    val step: Step,
    val reason: FailureReason,
    val detail: String,
    val diagnostics: List<Diagnostic>,
)

/**
 * Payload content is only reachable through `verified`.
 * [STT-VER-002, STT-VER-008]
 */
sealed interface VerificationResult {
    data class Verified(val value: VerifiedHcert) : VerificationResult

    data class Failed(val value: VerificationFailure) : VerificationResult
}

/**
 * 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.
 */
data class UnverifiedEntry(
    val subclaim: Int,
    /** The payload, when it decoded and met its model. */
    val payload: Payload? = null,
)

data class UnverifiedHcert(
    val kid: Kid? = null,
    val iss: String? = null,
    val iat: NumericDate? = null,
    val exp: NumericDate? = null,
    /** Every entry of the hcert claim, in subclaim-key order. */
    val entries: List<UnverifiedEntry>,
    val diagnostics: List<Diagnostic>,
)

/**
 * 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].
 *
 * Implemented by the library.
 */
interface Verifier {
    /** `now` is passed in: no ambient clock. [STT-VER-005] */
    fun verify(hc1: String, store: TrustStore, reg: Registry, now: NumericDate, options: VerifierOptions): VerificationResult

    /** Same, for raw COSE_Sign1 bytes. [STT-GEN-009] */
    fun verifyCose(cose: ByteArray, store: TrustStore, reg: Registry, now: NumericDate, options: VerifierOptions): VerificationResult

    /** [STT-VER-011] */
    fun readUnverified(hc1: String, reg: Registry, limits: DecodeLimits): HcertResult<UnverifiedHcert, VerificationFailure>
}

vhl/Vhl.kt

source
// GENERATED by tools/wit2kotlin/wit2kotlin.py from api/wit/vhl.wit (interface vhl). Do not edit.
package who.smart.trust.vhl

import who.smart.trust.HcertResult
import who.smart.trust.types.NumericDate
import who.smart.trust.types.ValidationIssue

/**
 * 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]
 */
data class VhlPayload(
    /**
     * Manifest URL: a FHIR List search with _id, code, status and
     * patient.identifier.
     */
    val url: String,
    /** 32 random bytes, 43 base64url characters. */
    val key: String,
    /** Expiry, epoch seconds. */
    val exp: Long? = null,
    /** L and/or P, in alphabetical order. */
    val flag: String? = null,
    /** At most 80 characters. */
    val label: String? = null,
    /** Protocol version; 1 when absent. */
    val v: Int? = null,
    /** extension.fhirBaseUrl, for the OAuth with SSRAA option. */
    val fhirBaseUrl: String? = null,
)

sealed interface VhlError {
    data class Invalid(val value: List<ValidationIssue>) : VhlError

    /** Not of the form vhlink:/<base64url JSON>. */
    data class NotAVhlink(val value: String) : VhlError

    data class Manifest(val value: String) : VhlError

    /** Wrong key, a bad tag, or a malformed JWE. */
    data class DecryptFailed(val value: String) : VhlError

    /** A JWE alg or enc other than dir / A256GCM. [STT-VHL-007] */
    data class Unsupported(val value: String) : VhlError
}

/** The ITI-YY5 Retrieve Manifest request for the host to POST. */
data class ManifestRequest(
    /** <base>/List/_search */
    val url: String,
    /** application/x-www-form-urlencoded */
    val contentType: String,
    val body: String,
)

/** One document listed by the manifest. */
data class ManifestDocument(
    /**
     * DocumentReference.content.attachment.url, when the DocumentReference
     * was included; the host GETs it (ITI-68).
     */
    val url: String? = null,
    /** List.entry.item.reference, when it was not; the host reads it first. */
    val reference: String? = null,
    /** The decrypted content type (attachment.contentType). */
    val contentType: String? = null,
    val title: String? = null,
)

data class Decrypted(
    /** The JWE cty header, when present. */
    val contentType: String? = null,
    val content: ByteArray,
)

/** What the receiver signs for the HTTP Message Signatures option. */
data class SignatureRequest(
    val method: String,
    val authority: String,
    val path: String,
    val contentType: String? = null,
    val body: ByteArray? = null,
    val keyid: String,
    /** e.g. ecdsa-p256-sha256 */
    val alg: String,
    val created: NumericDate,
)

data class SignatureBase(
    /** RFC 9530 Content-Digest header value, when there is a body. */
    val contentDigest: String? = null,
    /** Signature-Input header value. */
    val signatureInput: String,
    /** The RFC 9421 signature base: the bytes the host signs. */
    val base: ByteArray,
)

/**
 * 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]
 *
 * Implemented by the library.
 */
interface Vhl {
    /**
     * Every rule of the payload format; with `now`, an expired link too.
     * [STT-VHL-001, STT-VHL-009]
     */
    fun validate(p: VhlPayload, now: NumericDate?): List<ValidationIssue>

    /** [STT-VHL-003] */
    fun toVhlink(p: VhlPayload): String

    fun fromVhlink(text: String): HcertResult<VhlPayload, VhlError>

    /** A new key from the platform's secure random generator. [STT-VHL-004] */
    fun newKey(): String

    /** [STT-VHL-005] */
    fun buildManifestRequest(p: VhlPayload, recipient: String, passcode: String?, embeddedLengthMax: Int?): HcertResult<ManifestRequest, VhlError>

    /** The ITI-YY5 searchset Bundle. [STT-VHL-006] */
    fun parseManifest(bundleJson: String): HcertResult<List<ManifestDocument>, VhlError>

    /** JWE Compact, dir / A256GCM, the payload key. [STT-VHL-007] */
    fun encrypt(content: ByteArray, contentType: String, key: String): HcertResult<String, VhlError>

    fun decrypt(jwe: String, key: String): HcertResult<Decrypted, VhlError>

    /** [STT-VHL-008] */
    fun buildSignatureBase(r: SignatureRequest): SignatureBase

    /**
     * The Signature header value. An ecdsa-p256-sha256 DER signature is
     * converted to r||s.
     */
    fun signatureHeader(signature: ByteArray, alg: String): HcertResult<String, VhlError>
}