otavalidator

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 7, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

README

ota-artifact-validator

Field Value
Revision 1
Created 2026-06-07
Status scaffold
Part of Helix OTA
Language go
License Apache-2.0

Purpose

OTA artifact validation pipeline: structure, SHA-256 vs hash file, signature, version monotonicity, target compatibility, metadata extraction.

Boundary (decoupling)

OS-aware via plugins; no transport, no DB. Pure validate(input)->verdict. Reused by the server's upload path and CI.

This is a reusable, independently versioned building brick (HelixConstitution §11.4.28 submodules-as-equal-codebase). It is consumed by Helix OTA and is designed to be reusable by other projects. It must ship in-depth documentation, user guides, and full test coverage (§1 four-layer) before leaving scaffold status.

Status

Scaffold. Implementation tracked in the Helix OTA spec corpus (docs/research/main_specs/). See the master design and the submodule reuse map.

Mirrors

Documentation

Overview

Package otavalidator implements the server-side OTA artifact upload validation pipeline specified in docs/research/main_specs/1.0.0-mvp/server/artifact_validation.md (the S1..S6 ordered decision table) and the signing/verification rules in security/signing_verification.md.

The package is a pure library: it operates on bytes / io.Reader / interface ports only. It performs no disk, network, HTTP, or database access — the caller (the control-plane upload handler, or CI) supplies all inputs and implements the lookup ports. This keeps the validator decoupled and independently testable (HelixConstitution §11.4.28).

Each stage is a composable validator returning a typed Verdict carrying a stable RejectCode. The top-level Validate function runs the ordered, fail-fast pipeline and returns the first rejecting verdict with its code.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CompareDotted

func CompareDotted(a, b string) (int, error)

CompareDotted is the default VersionComparator: it compares dotted-numeric version identifiers (e.g. "1.4.0" vs "1.10.2") component-by-component as integers, so 1.10.0 > 1.9.0. An optional leading "v" is tolerated. Versions with differing component counts are compared as if the shorter were zero-padded (1.2 == 1.2.0). It returns an error if a component is not a non-negative integer.

Types

type Input

type Input struct {
	// Artifact streams the exact bytes that were signed and hashed (the S2/S3
	// scope). Validate reads it once, in full.
	Artifact io.Reader

	// HashFile is the raw content of the mandatory external hash file (S2).
	HashFile string

	// PublicKey is the trusted build-pipeline ed25519 public key (S3).
	PublicKey ed25519.PublicKey

	// Signature is the detached ed25519 signature over the SHA-256 digest (S3).
	Signature []byte

	// CurrentVersion is the latest published version for the same target,
	// supplied by the caller's prior-version lookup (S4). Empty means no prior
	// release exists.
	CurrentVersion string

	// Meta is the declared artifact metadata (version, os_type, board, sizes,
	// hashes) used by S4/S5/S6.
	Meta otaprotocol.ArtifactMeta

	// VersionComparator orders versions for S4. If nil, CompareDotted is used.
	VersionComparator VersionComparator

	// TargetPolicy decides S5 known/supported. Required for S5.
	TargetPolicy TargetPolicy
}

Input carries everything the S2..S6 pipeline needs. It is pure data / ports — no transport, disk, or DB. The S1 structure stage (ZIP_STORED parsing) is performed by the upload handler before calling Validate; this library starts at S2.

type RejectCode

type RejectCode string

RejectCode is a stable, operator-actionable reason a stage rejected an artifact. Codes mirror the decision table in artifact_validation.md §5 and leak no key material.

const (
	// S2 — SHA-256 vs hash file.
	RejectHashFileMissing   RejectCode = "S2_HASH_FILE_MISSING"
	RejectHashFileMalformed RejectCode = "S2_HASH_FILE_MALFORMED"
	RejectHashMismatch      RejectCode = "S2_HASH_MISMATCH"

	// S3 — signature.
	RejectSignatureMissing       RejectCode = "S3_SIGNATURE_MISSING"
	RejectSignatureKeyInvalid    RejectCode = "S3_SIGNATURE_KEY_INVALID"
	RejectSignatureInvalid       RejectCode = "S3_SIGNATURE_INVALID"
	RejectSignatureScopeMismatch RejectCode = "S3_SIGNATURE_SCOPE_MISMATCH"

	// S4 — version monotonicity.
	RejectVersionUnparseable RejectCode = "S4_VERSION_UNPARSEABLE"
	RejectNotMonotonic       RejectCode = "S4_NOT_MONOTONIC"
	RejectDuplicateVersion   RejectCode = "S4_DUPLICATE_VERSION"

	// S5 — target compatibility.
	RejectTargetUndeclared  RejectCode = "S5_TARGET_UNDECLARED"
	RejectTargetUnknown     RejectCode = "S5_TARGET_UNKNOWN"
	RejectTargetUnsupported RejectCode = "S5_TARGET_UNSUPPORTED"

	// S6 — metadata sanity.
	RejectMetadataIncomplete   RejectCode = "S6_METADATA_INCOMPLETE"
	RejectMetadataInconsistent RejectCode = "S6_METADATA_INCONSISTENT"
)

type Result

type Result struct {
	Final          Verdict
	Verdicts       []Verdict
	ComputedSHA256 string
}

Result is the outcome of a full pipeline run. Final is the decisive verdict (the first rejection, or the passing S6 verdict). Verdicts records every stage that ran, in order, for the audit trail. ComputedSHA256 is the lowercase-hex digest computed in S2 (empty if S2 could not hash the bytes).

func Validate

func Validate(in Input) Result

Validate runs the ordered, fail-fast S2..S6 pipeline. It returns at the first rejecting stage with that stage's verdict and code; later stages do not run. On full success the Final verdict is the passing S6 verdict.

func (Result) Accepted

func (r Result) Accepted() bool

Accepted reports whether the artifact passed the full pipeline.

type Stage

type Stage string

Stage identifies an ordered pipeline stage (S1..S6) from the decision table. S1 (structure / ZIP_STORED parsing) is out of scope for this pure-bytes library — it is handled by the upload handler's archive reader — so the stages implemented here are S2..S6.

const (
	// StageHash is S2 — SHA-256 vs hash file.
	StageHash Stage = "S2"
	// StageSignature is S3 — detached signature verification.
	StageSignature Stage = "S3"
	// StageVersion is S4 — version monotonicity.
	StageVersion Stage = "S4"
	// StageTarget is S5 — target compatibility (os_type / board).
	StageTarget Stage = "S5"
	// StageMetadata is S6 — metadata sanity.
	StageMetadata Stage = "S6"
)

type StaticTargetPolicy

type StaticTargetPolicy struct {
	// contains filtered or unexported fields
}

StaticTargetPolicy is an in-memory TargetPolicy backed by an explicit set of known targets and an explicit set of supported targets. It is a convenience for callers (and tests) that do not need a database-backed policy; the pipeline accepts any TargetPolicy implementation.

A target is Supported only if it is also present in the known set.

func NewStaticTargetPolicy

func NewStaticTargetPolicy(known, supported []TargetKey) *StaticTargetPolicy

NewStaticTargetPolicy builds a policy. supported targets are implicitly added to the known set so a supported target is always known.

func (*StaticTargetPolicy) Known

func (p *StaticTargetPolicy) Known(os otaprotocol.OSType, board string) bool

Known reports whether the target is recognized.

func (*StaticTargetPolicy) Supported

func (p *StaticTargetPolicy) Supported(os otaprotocol.OSType, board string) bool

Supported reports whether the target is accepted for the current phase.

type TargetKey

type TargetKey struct {
	OSType otaprotocol.OSType
	Board  string
}

TargetKey is an exported {os_type, board} pair for constructing a StaticTargetPolicy.

type TargetPolicy

type TargetPolicy interface {
	Known(os otaprotocol.OSType, board string) bool
	Supported(os otaprotocol.OSType, board string) bool
}

TargetPolicy decides S5 target compatibility for a declared {os_type, board}. Implementations are supplied by the caller (e.g. backed by the known-targets table) so the validator carries no database. Known reports whether the target is a recognized fleet target; Supported reports whether it is accepted for the current phase (Android Phase-1 for MVP).

type Verdict

type Verdict struct {
	Passed  bool
	Stage   Stage
	Code    RejectCode
	Message string
}

Verdict is the typed outcome of a single stage (or of the whole pipeline). A passing verdict has Passed == true and an empty Code. A rejecting verdict has Passed == false, the failing Stage, its RejectCode, and an operator-actionable Message.

func ValidateHash

func ValidateHash(r io.Reader, expectedHashFile string) (Verdict, string)

ValidateHash implements S2: it streams r through SHA-256 and compares the computed digest against the expected hash-file value (plain byte-for-byte digest comparison — the digest is public, so timing-safety buys nothing). expectedHashFile is the raw content of the mandatory hash file; it may carry a trailing filename (the common "<hex> name" coreutils format), which is tolerated. The returned digest is the lowercase-hex SHA-256 over r so the caller can persist it for S6 / the release record.

func ValidateMetadata

func ValidateMetadata(meta otaprotocol.ArtifactMeta, computedDigest string) Verdict

ValidateMetadata implements S6: required release fields must be present and well-formed (delegated to the ota-protocol ArtifactMeta contract), and the metadata must be internally consistent with the digest computed in S2. computedDigest is the lowercase-hex SHA-256 from S2; pass "" to skip the cross-check (e.g. when running S6 in isolation).

func ValidateSignature

func ValidateSignature(digestHex string, pubKey ed25519.PublicKey, sig []byte) Verdict

ValidateSignature implements S3: it verifies the detached ed25519 signature over the SHA-256 digest of the artifact under the trusted public key. digestHex is the lowercase-hex SHA-256 produced by S2 (this binds the signed bytes to the same artifact that passed S2 — the scope-match requirement). pubKey is the trusted build-pipeline public key; sig is the raw detached signature bytes.

ed25519 is the MVP DEFAULT scheme (signing_verification.md §3). The signature is over the digest, not the raw blob, keeping verification streaming-friendly for large payloads.

func ValidateTarget

func ValidateTarget(os otaprotocol.OSType, board string, policy TargetPolicy) Verdict

ValidateTarget implements S5: the artifact must declare a target and that target must be both known to the fleet and supported for the current phase.

func ValidateVersion

func ValidateVersion(declared, currentVersion string, cmp VersionComparator) Verdict

ValidateVersion implements S4: the declared version must be strictly greater than the latest published version for the same target. currentVersion is the latest published version supplied by the caller (the prior-version lookup); an empty currentVersion means no prior release exists, which passes. cmp orders the versions; if nil, CompareDotted is used.

func (Verdict) IsReject

func (v Verdict) IsReject() bool

IsReject reports whether the verdict denotes a rejection.

func (Verdict) String

func (v Verdict) String() string

String renders the verdict for logs (no key material).

type VersionComparator

type VersionComparator interface {
	Compare(a, b string) (int, error)
}

VersionComparator orders two version identifiers. It returns a negative number if a < b, zero if a == b, and a positive number if a > b, plus an error if either version is unparseable. The pipeline depends only on the sign of the result, so callers may plug in semver, build-number, or any monotonic scheme. A default dotted-numeric comparator (CompareDotted) is provided.

type VersionComparatorFunc

type VersionComparatorFunc func(a, b string) (int, error)

VersionComparatorFunc adapts a function to VersionComparator.

func (VersionComparatorFunc) Compare

func (f VersionComparatorFunc) Compare(a, b string) (int, error)

Compare calls f.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL