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 ¶
- func CompareDotted(a, b string) (int, error)
- type Input
- type RejectCode
- type Result
- type Stage
- type StaticTargetPolicy
- type TargetKey
- type TargetPolicy
- type Verdict
- func ValidateHash(r io.Reader, expectedHashFile string) (Verdict, string)
- func ValidateMetadata(meta otaprotocol.ArtifactMeta, computedDigest string) Verdict
- func ValidateSignature(digestHex string, pubKey ed25519.PublicKey, sig []byte) Verdict
- func ValidateTarget(os otaprotocol.OSType, board string, policy TargetPolicy) Verdict
- func ValidateVersion(declared, currentVersion string, cmp VersionComparator) Verdict
- type VersionComparator
- type VersionComparatorFunc
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CompareDotted ¶
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 ¶
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).
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 ¶
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 ¶
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.
type VersionComparator ¶
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.