Sign in

    moon-tuf-preflight

    Offline structural preflight for TUF metadata envelopes

    tuf
    software-update
    metadata
    supply-chain
    Download zip
    Version
    0.2.0
    License
    MIT
    Last updated
    22 days ago
    Downloads
    9

    #moon-tuf-preflight

    A pure MoonBit diagnostic library and CLI for TUF metadata envelopes.

    It answers one question before anything reaches a real TUF verifier: is this metadata structurally usable for the role it claims? It inspects JSON shape, _type versus the requested role, version, expires, signature entry shape, root key references and thresholds, timestamp/snapshot meta versions, target paths, lengths and sha256/sha512 digest formats, and it cross-checks the versions that a timestamp/snapshot/targets bundle references against the ones it carries.

    Security boundary. These functions never verify a cryptographic signature, never compare a digest against downloaded bytes, never build a trusted root chain and never prevent rollback. A report with no findings means "this document is well formed", not "this update is safe". Findings must not be used as the authorization to install an update.

    Release history is in CHANGELOG.md.

    #Use as a library

    // Structural check of one envelope.
    let findings : Array[String] = @moon-tuf-preflight.preflight(targets_json, "targets")

    // Same check plus an expiry decision against a caller-supplied UTC clock.
    let dated : Array[String] =
    @moon-tuf-preflight.preflight_at(targets_json, "targets", "2026-09-24T00:00:00Z")

    // timestamp / snapshot / targets must agree on the versions they reference.
    let bundle : Array[String] = @moon-tuf-preflight.preflight_bundle(
    timestamp_json, snapshot_json, targets_json, "2026-09-24T00:00:00Z",
    )

    For severity, counts and a typed result, use the Report API:

    let report = @moon-tuf-preflight.preflight_at_report(targets_json, "targets", now)
    if not(report.ok()) {
    for finding in report.findings() {
    match finding.severity() {
    Error => println("error " + finding.code() + " at " + finding.location())
    Warning => println("warn " + finding.code() + " at " + finding.location())
    }
    }
    }

    #Findings

    Findings are stable strings of the form severity:code[:location], where severity is error or warning. A location is the key id, meta file name, target path or document name the finding belongs to; it is omitted when the finding applies to the whole envelope. finding_codes() returns every code the library can emit, sorted by character codes, so a consumer can size a dashboard, diff two library versions or reject a code it does not know.

    Note that MoonBit's own < for strings is shortlex order (length first), so it is not usable for sorting codes; compare_ordinal and is_sorted_ordinal provide the ordinal order that finding_codes() guarantees.

    Envelope and shared fields

    ExampleMeaning
    error:invalid-jsonthe input is not JSON
    error:envelope-not-objectthe top-level JSON value is not an object
    error:missing-signed-objectthe envelope has no object-valued signed
    error:unsupported-role:<role>the requested role is not root, timestamp, snapshot or targets
    error:role-mismatch:<role>signed._type is not the requested role
    error:invalid-versionversion is missing, fractional, zero or out of range
    error:invalid-expiresexpires is not an exact YYYY-MM-DDTHH:MM:SSZ UTC time
    warning:missing-spec-versionthe document declares no spec_version (pre-1.0 metadata)
    warning:malformed-spec-version:v1.0spec_version is not a dotted numeric version
    error:invalid-current-timethe now argument of preflight_at* is not a valid UTC time
    error:expired:2030-06-01T12:30:00Zexpires <= now, reported by preflight_at*

    Signatures (entry shape only, never verified)

    ExampleMeaning
    error:missing-signatures-arraysignatures is absent or not an array
    error:empty-signaturesthe signatures array is empty
    error:invalid-signature-entryan entry is not an object
    error:invalid-signature-keyidan entry has no non-empty keyid
    error:invalid-signature-value:k1an entry has no non-empty sig
    error:duplicate-signature-keyid:k1the same keyid signs twice

    root

    ExampleMeaning
    error:missing-keyskeys is absent or not an object
    error:missing-rolesroles is absent or not an object
    error:empty-keyidkeys contains an empty key id
    error:invalid-key:k1a key entry is not an object
    error:invalid-keytype:k1a key has no non-empty keytype
    error:invalid-key-scheme:k1a key has no non-empty scheme
    error:invalid-keyval:k1keyval is absent or not an object
    error:invalid-public-key:k1keyval.public is absent or empty
    error:missing-role:rootone of the four top-level roles is absent
    error:invalid-threshold:rootthreshold is not an integer of at least 1
    error:missing-keyids:rootkeyids is absent, not an array, or empty
    error:invalid-keyid:roota keyids element is not a string
    error:duplicate-keyid:root:k1a role lists the same key id twice
    error:unknown-keyid:root:k1a role references a key that is not in keys
    error:unreachable-threshold:root:threshold=2,usable=1fewer usable keys than the threshold

    timestamp and snapshot

    ExampleMeaning
    error:missing-meta:timestampmeta is absent or not an object
    error:empty-meta:timestampmeta is an empty object
    error:missing-meta:snapshot.jsonthe required reference (snapshot.json / targets.json) is absent
    error:invalid-meta-entry:targets.jsona meta entry is not an object
    error:invalid-meta-version:targets.jsona meta entry has no valid integer version
    error:invalid-meta-length:targets.jsona meta entry's optional length is not a non-negative integer
    error:invalid-meta-hashes:targets.jsona meta entry's optional hashes is not a non-empty object
    error:invalid-meta-hash:targets.json:sha256a meta digest is not hex or has the wrong length
    warning:unsupported-meta-hash-algorithm:targets.json:md5a meta digest algorithm is not sha256, sha3-256, sha512 or sha3-512

    targets

    ExampleMeaning
    error:missing-targetstargets is absent or not an object
    error:invalid-target-entry:app.bina target entry is not an object
    error:unsafe-target-path:../appabsolute, .., ., empty or backslash/colon path
    error:invalid-target-length:app.binlength is not a non-negative integer
    error:missing-target-hashes:app.binhashes is absent, not an object, or empty
    error:invalid-target-hash:app.bin:sha256digest is not hex or not the expected length
    warning:unsupported-target-hash-algorithm:app.bin:md5digest algorithm is not sha256, sha3-256, sha512 or sha3-512
    warning:non-object-target-custom:app.bincustom is present but neither an object nor null

    Bundle (reported by preflight_bundle*)

    Per-document findings keep the severity prefix and gain the document name as a code segment, so a finding from the timestamp document is rendered as error:timestamp:expired:.... Cross-document findings are not per-document and are rendered normally.

    ExampleMeaning
    error:timestamp:expired:2030-01-01T00:00:00Za timestamp finding, carrying its document name as a code segment
    error:snapshot:invalid-meta-version:targets.jsonthe same rule for the snapshot document
    error:snapshot-version-mismatch:snapshot.json:referenced=2,actual=1the timestamp references a different snapshot version
    error:targets-version-mismatch:targets.json:referenced=1,actual=3the snapshot references a different targets version
    error:missing-version-reference:timestamp->snapshot.jsonthe referring document has no usable meta version
    error:missing-document-version:targets.jsonthe referenced document has no usable version

    Inputs must use second-resolution UTC with a trailing Z. Digest checks cover hex characters and digest length only; no file is read. The current time is always a parameter, so CI, Wasm and offline runs reproduce identical output.

    #Use as a CLI

    moon run --target wasm-gc cmd/preflight -- --role targets "$(cat targets.json)" moon run --target wasm-gc cmd/preflight -- --role root --at 2026-09-24T00:00:00Z "$(cat root.json)"

    The CLI prints one finding per line, exits 0 when nothing is reported, 1 when at least one finding is reported and 2 on a usage error. --help lists the options.

    #Install and reproduce

    Install the MoonBit toolchain and confirm that moon version --all works, then run these commands from the project root:

    moon check --deny-warn --target wasm moon build --deny-warn --target wasm moon test --deny-warn --target wasm moon run --target wasm examples/demo

    Swap wasm for wasm-gc, js (needs Node.js) or native (needs a C compiler). CI runs the check, build and test steps for wasm, wasm-gc, js and native, runs the example, and verifies that moon fmt and moon info leave the tree unchanged.

    To use the published package instead of this checkout:

    moon add sun-zhao12/moon-tuf-preflight

    #Using it as a pipeline gate

    The two severities have different meanings, and a gate should treat them differently:

    • Error means the document is structurally unusable for the role: a verifier would reject it or, worse, accept an envelope the caller misread. Fail the build.
    • Warning means the document parses but carries something this preflight cannot confirm, such as an unfamiliar digest algorithm or a missing spec_version. Report it; do not fail by default.

    Report::has_errors() answers the gate question, while Report::ok() is true for warnings-only reports. A minimal gate:

    findings=$(moon run --target wasm-gc cmd/preflight -- --role targets "$(cat targets.json)" || true) if printf '%s' "$findings" | grep -q '^error:'; then printf '%s\n' "$findings" exit 1 fi

    Because the current time is an argument, a repository schedule can re-run the expiry check on a cron without any hidden clock read:

    moon run --target wasm-gc cmd/preflight -- --role timestamp --at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$(cat timestamp.json)"

    #What it does not do

    • No cryptographic signature verification, no digest comparison against real bytes.
    • No trusted root chain, no rollback or freeze protection, no update authorization.
    • No support for TUF delegations or succinct roles yet.
    • No network, filesystem or clock access: everything is text in, text out.

    #License and sources

    The implementation follows the public TUF specification for field names, role semantics and threshold rules. That specification is published under the Community Specification License 1.0 (its license text under CC-BY-4.0); this project only references its semantics, so it is licensed under MIT. No third-party source, test vector or data file was copied, and all example signatures and digests are placeholders that prove nothing about trust.

    Authorization

    pub struct Authorization {
    role : String
    keyids : Array[String]
    threshold : Int
    satisfied_count : Int
    authorized_keyids : Array[String]
    outcome : AuthorizationOutcome
    }

    A structural authorization verdict for one document against one role.

    Authorization::authorized_keyids

    fn Authorization::authorized_keyids(self : Authorization) -> Array[String]

    The key ids the role authorizes, in root document order.

    Authorization::is_satisfied

    fn Authorization::is_satisfied(self : Authorization) -> Bool

    Whether the document carries enough authorized signatures to be considered by a verifier. Never a statement that any signature is valid.

    Authorization::outcome

    The verdict.

    Authorization::role

    fn Authorization::role(self : Authorization) -> String

    The role that was checked.

    Authorization::satisfied_count

    fn Authorization::satisfied_count(self : Authorization) -> Int

    How many distinct authorized keys signed the document.

    Authorization::threshold

    fn Authorization::threshold(self : Authorization) -> Int

    The declared threshold, or 0 when the role entry was unusable.

    AuthorizationOutcome

    pub enum AuthorizationOutcome {
    Satisfied
    Unsatisfied
    Undecidable
    }

    The outcome of a probability-free authorization check: enough authorized signatures, not enough, or a problem that prevented the check entirely.

    AuthorizationOutcome::equal

    Whether two verdicts are the same.

    AuthorizationOutcome::to_string

    fn AuthorizationOutcome::to_string(self : AuthorizationOutcome) -> String

    A short name for the verdict, used in tests and log lines.

    DateTime

    pub struct DateTime {
    epoch_seconds : Int64
    form : DateTimeForm
    offset_minutes : Int
    }

    A parsed date-time plus the shape it was written in.

    DateTime::epoch_seconds

    fn DateTime::epoch_seconds(self : DateTime) -> Int64

    The instant as seconds since the Unix epoch.

    DateTime::equal

    fn DateTime::equal(self : DateTime, other : DateTime) -> Bool

    Whether two parsed instants are identical.

    DateTime::form

    fn DateTime::form(self : DateTime) -> DateTimeForm

    The shape of the source string.

    DateTime::is_canonical

    fn DateTime::is_canonical(self : DateTime) -> Bool

    Whether the string was written in the exact form the specification asks for.

    DateTime::offset_minutes

    fn DateTime::offset_minutes(self : DateTime) -> Int

    The numeric UTC offset in minutes that was applied during parsing.

    DateTimeForm

    pub enum DateTimeForm {
    Exact
    Fractional
    Offset
    }

    The relaxed ISO 8601 / RFC 3339 subset this parser accepts.

    DateTimeForm::equal

    fn DateTimeForm::equal(self : DateTimeForm, other : DateTimeForm) -> Bool

    Whether two forms describe the same shape.

    DateTimeForm::to_string

    fn DateTimeForm::to_string(self : DateTimeForm) -> String

    A short name for the form, used in diagnostics and tests.

    Delegation

    pub struct Delegation {
    name : String
    keyids : Array[String]
    threshold : Int
    paths : Array[String]
    path_hash_prefixes : Array[String]
    terminating : Bool
    well_formed : Bool
    }

    One entry of a delegations.roles array.

    Delegation::is_well_formed

    fn Delegation::is_well_formed(self : Delegation) -> Bool

    Whether the entry had all the fields a delegation needs.

    Delegation::keyids

    fn Delegation::keyids(self : Delegation) -> Array[String]

    The key ids the delegation authorizes.

    Delegation::name

    fn Delegation::name(self : Delegation) -> String

    The delegated role's name.

    Delegation::path_hash_prefixes

    fn Delegation::path_hash_prefixes(self : Delegation) -> Array[String]

    The declared path_hash_prefixes.

    Delegation::paths

    fn Delegation::paths(self : Delegation) -> Array[String]

    The declared paths globs.

    Delegation::terminating

    fn Delegation::terminating(self : Delegation) -> Bool

    Whether the delegation terminates the search.

    Delegation::threshold

    fn Delegation::threshold(self : Delegation) -> Int

    The number of authorized signatures the delegated metadata needs.

    Finding

    pub struct Finding {
    severity : Severity
    code : String
    location : String
    }

    One diagnostic produced by a structural preflight check.

    code is a stable, machine-readable identifier such as invalid-target-hash; location names the offending field owner (a key id, a meta file name or a target path) and is empty when the finding applies to the envelope as a whole.

    Finding::code

    fn Finding::code(self : Finding) -> String

    The stable machine-readable code, such as invalid-target-hash.

    Finding::location

    fn Finding::location(self : Finding) -> String

    The key id, meta file name, target path or document the finding belongs to, or the empty string when it applies to the whole envelope.

    Finding::severity

    fn Finding::severity(self : Finding) -> Severity

    Whether the finding blocks handing the document to a TUF verifier.

    Finding::to_string

    fn Finding::to_string(self : Finding) -> String

    The stable severity:code[:location] rendering of a finding.

    Report

    pub struct Report {
    findings : Array[Finding]
    role : String
    version : Int?
    }

    The complete preflight result for one metadata document.

    Call [Report::findings] for the human-readable diagnostics and [Report::ok] or [Report::has_errors] for pipeline decisions. A report with no findings is a structural statement only: this library never verifies signatures, hashes or the trusted root chain.

    Report::error_count

    fn Report::error_count(self : Report) -> Int

    Number of Error findings.

    Report::findings

    fn Report::findings(self : Report) -> Array[Finding]

    All findings, in detection order.

    Report::has_errors

    fn Report::has_errors(self : Report) -> Bool

    Whether any finding has severity Error.

    Report::ok

    fn Report::ok(self : Report) -> Bool

    Whether the document may be handed to a TUF verifier at all.

    true means every structural field required by the requested role is present and well formed. It does not mean the update is trusted, unexpired or rollback-safe.

    Report::render

    fn Report::render(self : Report) -> String

    A newline-joined report suitable for printing in a terminal or a CI log.

    Report::role

    fn Report::role(self : Report) -> String

    The role the report was produced for, as passed to [preflight_report].

    Report::to_strings

    fn Report::to_strings(self : Report) -> Array[String]

    The findings rendered as severity:code[:location] strings.

    Report::version

    fn Report::version(self : Report) -> Int?

    The signed.version integer, or None when it is missing or not an integer in 1 ..= 2147483647.

    Report::warning_count

    fn Report::warning_count(self : Report) -> Int

    Number of Warning findings.

    Semver

    pub struct Semver {
    major : Int
    minor : Int
    patch : Int
    suffix : String
    }

    A semver version as the three numeric components the TUF specification requires for spec_version. A missing component defaults to zero, so "1" and "1.0" both parse and compare as 1.0.0; a pre-release or build suffix is recorded instead, because the specification calls for a plain semantic version.

    Semver::equal

    fn Semver::equal(self : Semver, other : Semver) -> Bool

    Whether two semantic versions are identical.

    Semver::is_plain

    fn Semver::is_plain(self : Semver) -> Bool

    Whether the suffix is empty, which is what spec_version should look like.

    Semver::major

    fn Semver::major(self : Semver) -> Int

    The major version number.

    Semver::minor

    fn Semver::minor(self : Semver) -> Int

    The minor version number.

    Semver::patch

    fn Semver::patch(self : Semver) -> Int

    The patch version number.

    Semver::suffix

    fn Semver::suffix(self : Semver) -> String

    The pre-release or build suffix, including its leading - or +, or "".

    Semver::to_string

    fn Semver::to_string(self : Semver) -> String

    The canonical major.minor.patch rendering.

    Severity

    pub enum Severity {
    Error
    Warning
    }

    Severity of a single preflight finding.

    Error marks a metadata envelope that is structurally invalid for the requested role. Warning marks a value that parses but is suspicious, such as a target path that escapes its directory prefix.

    Severity::to_string

    fn Severity::to_string(self : Severity) -> String

    any_path_matches

    fn any_path_matches(patterns : Array[String], path : String) -> Bool

    Whether any of patterns matches path.

    authorization_findings

    fn authorization_findings(authorization : Authorization, document : String, findings : Array[Finding]) -> Unit

    Append the findings for one authorization verdict to findings.

    document is the metadata being authorized. Its own role key ids are read so that, during a root rotation, the new root's own keys are not reported as unexpected signers: a client that already holds that root checks them.

    authorized_keyids

    fn authorized_keyids(root : String, role : String) -> Array[String]?

    The key ids of role from a root document, or None when the root is not usable or does not define the role.

    chain_signers

    fn chain_signers(chain : ArrayView[String]) -> Array[Array[String]]

    Which key ids signed each document of a root chain, in the order the documents were supplied.

    The chain is the sequence a client walks while rotating trust: the authoritative root first, then each newer root in turn.

    chain_versions

    fn chain_versions(chain : ArrayView[String]) -> Array[Int?]

    The version numbers of a root chain, with None for a document whose version field is missing or malformed.

    check_authorization

    fn check_authorization(root : String, document : String, role : String) -> Authorization

    Check whether document carries enough authorized signatures for role, as authorized by root.

    root is the root metadata whose roles[role] entry applies: the document's own root when checking a root at its own version, or the previous root when checking a rotation.

    check_delegation_tree

    fn check_delegation_tree(documents : Map[String, String], findings : Array[Finding]) -> Unit

    Append findings for a whole delegation tree.

    documents maps a role name to its metadata text, including the top-level targets role. The check reports a delegated role whose metadata is missing, a cycle, and a document that tries to delegate a role it does not own.

    check_delegations

    fn check_delegations(targets : String, findings : Array[Finding]) -> Unit

    Append findings for the delegation tree of one targets document.

    findings receives one finding per problem, with the delegated role name as the location so a caller can point at the exact entry.

    check_root_chain

    fn check_root_chain(chain : ArrayView[String], findings : Array[Finding]) -> Unit

    Append findings for a root rotation chain.

    chain[0] is the root the caller already trusts; every later element must be the next version and must be signed by keys authorized by its immediate predecessor's root role, because that is the only root a client would hold when it processes the update. Cryptographic verification is out of scope, so a chain that is structurally consistent here can still fail in a real verifier.

    check_snapshot_coverage

    fn check_snapshot_coverage(snapshot : String, targets_versions : Map[String, Int], findings : Array[Finding]) -> Unit

    Append findings that compare a snapshot document against the targets documents it claims to describe.

    snapshot is snapshot.json text and targets_versions maps a metadata path such as targets.json to the version the corresponding document actually carries. A snapshot that omits a document the caller supplied, or names one the caller did not supply, cannot be resolved by a client.

    compare_date_time

    fn compare_date_time(left : DateTime, right : DateTime) -> Int

    Compare two parsed instants.

    compare_ordinal

    fn compare_ordinal(left : StringView, right : StringView) -> Int

    Compare two strings by character codes, the ordering usually called lexicographic or "ordinal".

    This exists because MoonBit's own String comparison is shortlex: it compares lengths first, so "duplicate-signature-keyid" < "empty-keyid" is false even though d sorts before e. Sorting a list of codes therefore needs this helper, and consumers that want the documented order of [finding_codes] should use it too.

    contains_keyid

    fn contains_keyid(keyids : Array[String], keyid : String) -> Bool

    Whether keyids contains keyid.

    count_authorized

    fn count_authorized(signature_keyids : Array[String], authorized_keyids : Array[String]) -> Int

    How many distinct key ids in signature_keyids are also authorized by the role, which is the number a threshold compares against.

    days_from_civil

    fn days_from_civil(year : Int, month : Int, day : Int) -> Int64

    Days from 1970-01-01 to year-month-day, by the civil-from-days algorithm of Howard Hinnant. Valid for any proleptic Gregorian date.

    The era is computed with an explicit floor division because MoonBit's Int division truncates toward zero, which would place dates before year 1 in the wrong era.

    days_in_month

    fn days_in_month(year : Int, month : Int) -> Int

    The number of days in month of year, honouring the Gregorian leap rule.

    delegation_edges

    fn delegation_edges(targets : String) -> Map[String, Array[String]]

    The delegation edges declared by a single targets document, mapping the document's own role name to the roles it delegates to.

    Each delegated role is expected to have its own metadata file; this function only records the edge, and [check_delegation_tree] walks the whole graph.

    distinct

    fn distinct(keyids : Array[String]) -> Array[String]

    The distinct key ids in keyids, in first-seen order.

    empty_findings

    fn empty_findings() -> Array[Finding]

    A fresh, empty findings buffer.

    The check helpers append to a caller-owned array, so every entry point needs one; naming the constructor keeps the public API free of an inline mutable literal and gives tests a single spelling to use.

    finding_codes

    fn finding_codes() -> Array[String]

    Every diagnostic code this library can emit, sorted with [compare_ordinal].

    Consumers that sort, filter or count findings often need to know the full set up front, for example to size a dashboard or to reject a code that a newer version of the library introduced. Codes that carry a location are listed without one; the bundle codes appear under their bare code.

    is_compatible_spec_version

    fn is_compatible_spec_version(candidate : Semver, expected : Semver) -> Bool

    Whether candidate is compatible with the expected specification version under the matching rule the TUF specification leaves to adopters: the major version must be equal, and the candidate must not be newer than expected.

    This is the rule this project documents for preflight_authorized; a caller that wants exact matching can compare to_string() instead.

    is_date_time

    fn is_date_time(value : String) -> Bool

    Whether value parses as a date-time of any accepted shape.

    is_hex_prefix

    fn is_hex_prefix(prefix : String, digest : String) -> Bool

    Whether prefix is a prefix of path under the specification's path_hash_prefixes rule.

    The client compares the prefix against the hexadecimal digest of the target path, so a preflight can only check that the prefix looks like a hex string and that the two strings relate as a prefix; it cannot compute the digest without hashing, which is deliberately out of scope.

    is_leap_year

    fn is_leap_year(year : Int) -> Bool

    Whether year is a Gregorian leap year.

    is_sorted_ordinal

    fn is_sorted_ordinal(values : ArrayView[String]) -> Bool

    Whether values is strictly increasing under [compare_ordinal], which is the order [finding_codes] promises.

    Exposed so a consumer can assert the same property over its own copy of the list, and so the library's own check builds do not treat the helper as dead code (the test suite is compiled separately from the library).

    parse_date_time

    fn parse_date_time(value : String) -> DateTime?

    Parse a TUF metadata date-time.

    Accepts the exact YYYY-MM-DDTHH:MM:SSZ form as well as the two deviations that appear in real metadata: a fractional second part, and a numeric UTC offset (+HH:MM / -HH:MM, with +00:00 and -00:00 allowed). Returns None when the string is not a date-time of any accepted shape, which lets the caller report one clear "not a date-time" finding instead of three unrelated component errors.

    parse_delegations

    fn parse_delegations(targets : String) -> Array[Delegation]?

    Read the delegations.roles array of a targets document.

    Returns None when the document has no delegations object, which is valid: delegation is optional. An entry that cannot be read is still returned, with well_formed false, so the caller can report it by index.

    parse_semver

    fn parse_semver(value : String) -> Semver?

    Parse a semantic version.

    Returns None when the string is not made of dot-separated decimal numbers with an optional -/+ suffix, or when a numeric component is empty (as in 1..0) or has a leading zero (as in 01.0.0), both of which semver forbids.

    The parser is written with local loop state on purpose: MoonBit scopes loop bodies, so mutating an outer let mut (or a captured Ref) from the body and then leaving the loop can silently discard the assignment. Every value this function needs is therefore derived from the return of a helper.

    path_matches

    fn path_matches(pattern : String, path : String) -> Bool

    Whether path matches the delegation glob pattern.

    The specification's globs never cross a path separator, so * matches any run of characters inside one segment, ? matches one character, and a [abc] / [a-z] class matches one character from the set. A trailing pattern of * therefore does not match a nested path, which is the rule a client applies.

    preflight

    fn preflight(input : String, role : String) -> Array[String]

    [preflight_report] collapsed to severity:code[:location] strings.

    preflight_at

    fn preflight_at(input : String, role : String, now : String) -> Array[String]

    [preflight_at_report] collapsed to severity:code[:location] strings.

    preflight_at_report

    fn preflight_at_report(input : String, role : String, now : String) -> Report

    Preflight a single envelope and additionally compare expires with the caller-supplied current UTC time now.

    Passing now in keeps CI, offline mirrors and Wasm builds reproducible and free of hidden clock reads. The check is deliberately non-strict at the boundary: metadata is reported as expired once expires <= now, matching the TUF rule that an update ceases to be valid at expires.

    preflight_bundle

    fn preflight_bundle(timestamp : String, snapshot : String, targets : String, now : String) -> Array[String]

    [preflight_bundle_report] collapsed to severity:code[:location] strings.

    preflight_bundle_report

    fn preflight_bundle_report(timestamp : String, snapshot : String, targets : String, now : String) -> Report

    Preflight a timestamp / snapshot / targets triple and check that the versions the documents reference agree with the versions they carry.

    Findings from a single document gain that document's name as a code segment, so a timestamp that expired is rendered as error:timestamp:expired:2030-01-01T00:00:00Z. A missing or malformed version on either side of a reference is reported as missing-version-reference or missing-document-version instead of being treated as agreement. now is the caller-supplied current UTC time.

    preflight_delegations

    fn preflight_delegations(targets : String) -> Array[String]

    Findings for a delegation tree, as severity:code[:location] strings.

    preflight_report

    fn preflight_report(input : String, role : String) -> Report

    Structurally preflight one TUF metadata envelope.

    input is the raw JSON document and role the role it claims to be. Malformed JSON yields error:invalid-json. See [Report] for the meaning of the result, and [preflight_at] / [preflight_bundle] for expiry and cross-document checks.

    preflight_root_chain

    fn preflight_root_chain(chain : ArrayView[String]) -> Array[String]

    Findings for a root rotation chain, as severity:code[:location] strings.

    preflight_snapshot_coverage

    fn preflight_snapshot_coverage(snapshot : String, targets_versions : Map[String, Int]) -> Array[String]

    Findings for a snapshot coverage check, as strings.

    reachable_roles

    fn reachable_roles(root_role : String, edges : Map[String, Array[String]]) -> Array[String]

    The role names reachable from root_role in a delegation tree.

    edges maps a role name to the names it delegates to. The walk is breadth-first and remembers what it has visited, so a cycle in the graph cannot make it loop forever.

    role_threshold

    fn role_threshold(root : String, role : String) -> Int?

    A root role's threshold, or None when it is missing or not a positive integer.

    seconds_between

    fn seconds_between(from : DateTime, to : DateTime) -> Int64

    Seconds from from to to, which is negative when to is earlier.

    signature_keyids

    fn signature_keyids(input : String) -> Array[String]

    The key ids listed in an envelope's signatures array, in document order.

    An entry counts only when it carries both a non-empty keyid and a non-empty sig; entries that are not objects or that miss either field are skipped, because [preflight_report] already reports those as shape findings and this function is used to decide how many usable signatures a document has.

    signature_values

    fn signature_values(input : String) -> Array[String]

    The sig values listed in an envelope, in document order, skipping entries that carry no usable value.

    Exposed so a caller can tell "two signatures" from "one signature repeated", and so a test can prove the preflight never treats a signature as valid.

    unexpected_signers

    fn unexpected_signers(signature_keyids : Array[String], authorized_keyids : Array[String], own_keyids : Array[String]) -> Array[String]

    The signature key ids that are neither authorized by the previous root nor part of the document's own role.

    During a root rotation the new root is expected to carry signatures from both the previous root's keys and its own new keys: a client checks the former, and a second client that already holds the new root checks the latter. Reporting the new keys as "unexpected" would therefore be noise, so they are excluded.

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io