Offline structural preflight for TUF metadata envelopes
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.
// 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",
)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())
}
}
}| Example | Meaning |
|---|---|
| error:invalid-json | the input is not JSON |
| error:envelope-not-object | the top-level JSON value is not an object |
| error:missing-signed-object | the 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-version | version is missing, fractional, zero or out of range |
| error:invalid-expires | expires is not an exact YYYY-MM-DDTHH:MM:SSZ UTC time |
| warning:missing-spec-version | the document declares no spec_version (pre-1.0 metadata) |
| warning:malformed-spec-version:v1.0 | spec_version is not a dotted numeric version |
| error:invalid-current-time | the now argument of preflight_at* is not a valid UTC time |
| error:expired:2030-06-01T12:30:00Z | expires <= now, reported by preflight_at* |
| Example | Meaning |
|---|---|
| error:missing-signatures-array | signatures is absent or not an array |
| error:empty-signatures | the signatures array is empty |
| error:invalid-signature-entry | an entry is not an object |
| error:invalid-signature-keyid | an entry has no non-empty keyid |
| error:invalid-signature-value:k1 | an entry has no non-empty sig |
| error:duplicate-signature-keyid:k1 | the same keyid signs twice |
| Example | Meaning |
|---|---|
| error:missing-keys | keys is absent or not an object |
| error:missing-roles | roles is absent or not an object |
| error:empty-keyid | keys contains an empty key id |
| error:invalid-key:k1 | a key entry is not an object |
| error:invalid-keytype:k1 | a key has no non-empty keytype |
| error:invalid-key-scheme:k1 | a key has no non-empty scheme |
| error:invalid-keyval:k1 | keyval is absent or not an object |
| error:invalid-public-key:k1 | keyval.public is absent or empty |
| error:missing-role:root | one of the four top-level roles is absent |
| error:invalid-threshold:root | threshold is not an integer of at least 1 |
| error:missing-keyids:root | keyids is absent, not an array, or empty |
| error:invalid-keyid:root | a keyids element is not a string |
| error:duplicate-keyid:root:k1 | a role lists the same key id twice |
| error:unknown-keyid:root:k1 | a role references a key that is not in keys |
| error:unreachable-threshold:root:threshold=2,usable=1 | fewer usable keys than the threshold |
| Example | Meaning |
|---|---|
| error:missing-meta:timestamp | meta is absent or not an object |
| error:empty-meta:timestamp | meta is an empty object |
| error:missing-meta:snapshot.json | the required reference (snapshot.json / targets.json) is absent |
| error:invalid-meta-entry:targets.json | a meta entry is not an object |
| error:invalid-meta-version:targets.json | a meta entry has no valid integer version |
| error:invalid-meta-length:targets.json | a meta entry's optional length is not a non-negative integer |
| error:invalid-meta-hashes:targets.json | a meta entry's optional hashes is not a non-empty object |
| error:invalid-meta-hash:targets.json:sha256 | a meta digest is not hex or has the wrong length |
| warning:unsupported-meta-hash-algorithm:targets.json:md5 | a meta digest algorithm is not sha256, sha3-256, sha512 or sha3-512 |
| Example | Meaning |
|---|---|
| error:missing-targets | targets is absent or not an object |
| error:invalid-target-entry:app.bin | a target entry is not an object |
| error:unsafe-target-path:../app | absolute, .., ., empty or backslash/colon path |
| error:invalid-target-length:app.bin | length is not a non-negative integer |
| error:missing-target-hashes:app.bin | hashes is absent, not an object, or empty |
| error:invalid-target-hash:app.bin:sha256 | digest is not hex or not the expected length |
| warning:unsupported-target-hash-algorithm:app.bin:md5 | digest algorithm is not sha256, sha3-256, sha512 or sha3-512 |
| warning:non-object-target-custom:app.bin | custom is present but neither an object nor null |
| Example | Meaning |
|---|---|
| error:timestamp:expired:2030-01-01T00:00:00Z | a timestamp finding, carrying its document name as a code segment |
| error:snapshot:invalid-meta-version:targets.json | the same rule for the snapshot document |
| error:snapshot-version-mismatch:snapshot.json:referenced=2,actual=1 | the timestamp references a different snapshot version |
| error:targets-version-mismatch:targets.json:referenced=1,actual=3 | the snapshot references a different targets version |
| error:missing-version-reference:timestamp->snapshot.json | the referring document has no usable meta version |
| error:missing-document-version:targets.json | the referenced document has no usable version |
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)"moon check --deny-warn --target wasm
moon build --deny-warn --target wasm
moon test --deny-warn --target wasm
moon run --target wasm examples/demomoon add sun-zhao12/moon-tuf-preflightfindings=$(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
fimoon run --target wasm-gc cmd/preflight -- --role timestamp --at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$(cat timestamp.json)"pub struct Authorization {
role : String
keyids : Array[String]
threshold : Int
satisfied_count : Int
authorized_keyids : Array[String]
outcome : AuthorizationOutcome
}pub enum AuthorizationOutcome {
Satisfied
Unsatisfied
Undecidable
}pub enum DateTimeForm {
Exact
Fractional
Offset
}pub struct Semver {
major : Int
minor : Int
patch : Int
suffix : String
}pub enum Severity {
Error
Warning
}fn authorization_findings(authorization : Authorization, document : String, findings : Array[Finding]) -> Unitfn compare_ordinal(left : StringView, right : StringView) -> Intfn days_from_civil(year : Int, month : Int, day : Int) -> Int64fn days_in_month(year : Int, month : Int) -> Intfn is_date_time(value : String) -> Boolfn is_hex_prefix(prefix : String, digest : String) -> Boolfn path_matches(pattern : String, path : String) -> Boolfn preflight_bundle(timestamp : String, snapshot : String, targets : String, now : String) -> Array[String]fn preflight_bundle_report(timestamp : String, snapshot : String, targets : String, now : String) -> Reportfn role_threshold(root : String, role : String) -> Int?Install
Download zipOffline structural preflight for TUF metadata envelopes