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

  1. Re-generate the signature with the distro signing command so Distro.sig contains 128 lowercase hex characters (64 bytes).
  2. If your signer outputs base64, convert to hex before writing Distro.sig (e.g. `base64 -d sig.b64 | xxd -p -c 128`).
  3. Check the file length: `wc -c Distro.sig` should be 128 bytes plus optional trailing newline; re-transfer if truncated.
  4. 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

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

Related errors


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)