astrid-runtime/astrid · error
malformed Distro.sig (expected 64-byte hex)
Error message
malformed Distro.sig (expected 64-byte hex): {e} What it means
verify_lock parses the Distro.sig file contents as a 64-byte hex-encoded ed25519 signature before verifying it against the lock's signing digest. If Signature::from_hex rejects the string (wrong length, non-hex characters, whitespace issues beyond trim), this error reports the malformed signature. The signature never reaches cryptographic verification; it fails at parse time.
Solutions
- Re-generate the signature with the distro signing command so Distro.sig contains 128 lowercase hex characters (64 bytes).
- If your signer outputs base64, convert to hex before writing Distro.sig (e.g. `base64 -d sig.b64 | xxd -p -c 128`).
- Check the file length: `wc -c Distro.sig` should be 128 bytes plus optional trailing newline; re-transfer if truncated.
- Verify you are reading the signature file, not a key or manifest, into verify_lock.
Example fix
// before
let sig_hex = std::fs::read_to_string("Distro.sig")?; // base64 from external signer
verify_lock(&lock, &sig_hex, &pk)?; // malformed: not hex
// after
let raw = base64::decode(sig_hex.trim())?;
let sig_hex = hex::encode(&raw);
verify_lock(&lock, &sig_hex, &pk)?; Defensive patterns
Strategy: validation
Validate before calling
let sig = sig_hex.trim();
anyhow::ensure!(!sig.is_empty(), "Distro.sig is empty");
anyhow::ensure!(sig.len() == 128 && sig.bytes().all(|b| b.is_ascii_hexdigit()),
"Distro.sig must be 128 hex chars (64 bytes), got {} chars", sig.len()); Type guard
fn is_hex_sig(s: &str) -> bool {
let s = s.trim();
s.len() == 128 && s.bytes().all(|b| b.is_ascii_hexdigit())
} Try / catch
match verify_lock(&lock, &sig_hex, &pk) {
Err(e) if e.to_string().contains("malformed Distro.sig") => {
eprintln!("signature file is not 64-byte hex — re-sign or convert from base64");
}
other => other?,
} Prevention
- Always generate Distro.sig with the distro signing command (hex output).
- Convert base64 signatures from external tools to hex before writing the file.
- Check file size (128 bytes + optional newline) after any transfer of Distro.sig.
When it happens
Trigger: Calling verify_lock with a sig_hex string that is not exactly 128 hex characters: an empty Distro.sig, a base64-encoded signature pasted instead of hex, a truncated or multi-line signature file, or a file containing extra content after the hex (beyond leading/trailing whitespace, which is trimmed).
Common situations: Signing with an external tool that emits base64 signatures while distro tooling expects hex; a partially written or corrupted Distro.sig transferred between machines; manually editing the sig file and dropping characters.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- principal has an invalid genesis public key
- public key must be in 'ed25519
- capsule provenance changed after authority decision
- decode public key hex
- distro ' ' is pinned to but this artifact is signed by —…
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/c9730a26ab5e86cb.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-cli/src/commands/distro/sign.rs:101
/// Render a [`PublicKey`] as `ed25519:<base64>`.
pub(crate) fn pubkey_to_wire(pk: &PublicKey) -> String {
format!("ed25519:{}", pk.to_base64())
}
/// Verify a hex `Distro.sig` against a lock and a public key.
///
/// # Errors
///
/// Returns an error if the signature is malformed (not 64 hex bytes) or
/// does not verify against the lock's signing digest under `pubkey`.
pub(crate) fn verify_lock(
lock: &DistroLock,
sig_hex: &str,
pubkey: &PublicKey,
) -> anyhow::Result<()> {
let sig = Signature::from_hex(sig_hex.trim())
.map_err(|e| anyhow::anyhow!("malformed Distro.sig (expected 64-byte hex): {e}"))?;
let digest = lock_signing_digest(lock)?;
pubkey
.verify(&digest, &sig)
.map_err(|_| anyhow::anyhow!("distro signature verification failed"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::distro::lock::{DistroLock, DistroLockMeta, LockedCapsule};
fn sample_lock() -> DistroLock {
DistroLock {
schema_version: 1,
distro: DistroLockMeta {
id: "test".into(),
version: "0.1.0".into(),
resolved_at: "2026-01-01T00:00:00Z".into(),View on GitHub (pinned to affd8760f4)