astrid-runtime/astrid · error

invalid ed25519 secret key

Error message

invalid ed25519 secret key: {e}

What it means

load_signing_key reads a signing key file, requires exactly 32 raw bytes, and then hands them to astrid_crypto::KeyPair::from_secret_key. If the cryptographic layer still rejects the bytes (not a valid ed25519 scalar), the underlying crypto error is wrapped into "invalid ed25519 secret key: {e}". The file exists and is the right length; its content just isn't a usable ed25519 secret key.

Solutions

  1. Regenerate the signing key with the tooling that emits 32 raw ed25519 secret-key bytes and point the seal command at the new file.
  2. If your key is stored hex- or base64-encoded, decode it to 32 raw bytes before writing it to the keyfile.
  3. Verify file contents: `wc -c` should report exactly 32 bytes and the bytes should be a valid ed25519 scalar (not the public key).
  4. Restore the keyfile from a known-good backup if it was corrupted in transit.

Example fix

// before: base64 text file fed to seal
 SigningKeyPath=/home/me/distro.key   // contains 'dGhpcyBpcyBiYXNlNjQuLi4='
// after
$ base64 -d distro.key.b64 > distro.key && wc -c distro.key  # must be 32
SigningKeyPath=/home/me/distro.key
Defensive patterns

Strategy: validation

Validate before calling

let bytes = std::fs::read(path)?;
anyhow::ensure!(bytes.len() == 32, "signing key {} must be 32 raw bytes, got {}", path.display(), bytes.len());
anyhow::ensure!(bytes.iter().any(|&b| b != 0), "signing key must not be all zeros");

Try / catch

match load_signing_key(&path) {
    Ok(kp) => kp,
    Err(e) if e.to_string().starts_with("invalid ed25519 secret key") => {
        eprintln!("keyfile content is not a raw ed25519 secret; regenerate or decode it first");
        return Err(e);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Running `seal` (run_seal) with a key path whose 32 bytes are rejected by ed25519 parsing — e.g. a base64/hex-encoded key stored raw-decoded incorrectly, an all-zero or out-of-range scalar, or a truncated/corrupted keyfile that coincidentally is 32 bytes. Also hit intentionally by the test load_signing_key_rejects_wrong_length.

Common situations: Generating the key with a different tool that stores it hex/base64 encoded while seal expects raw bytes; copying the wrong file (e.g. a public-key file or unrelated 32-byte blob) as the signing key; file corruption after a failed transfer.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/4a612d98f3d51aa2. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-cli/src/commands/distro/seal.rs:205

    }
    bail!(
        "seal requires a local Distro.toml path or its directory; {distro:?} is not a file or directory"
    )
}

/// Load a 32-byte raw ed25519 secret key from `path`. Never logged.
fn load_signing_key(path: &Path) -> anyhow::Result<astrid_crypto::KeyPair> {
    let bytes = std::fs::read(path)
        .with_context(|| format!("failed to read signing key {}", path.display()))?;
    if bytes.len() != 32 {
        bail!(
            "signing key {} must be exactly 32 raw bytes (got {})",
            path.display(),
            bytes.len()
        );
    }
    astrid_crypto::KeyPair::from_secret_key(&bytes)
        .map_err(|e| anyhow::anyhow!("invalid ed25519 secret key: {e}"))
}

#[cfg(test)]
mod tests {
    use super::*;

    struct CurrentDirGuard(PathBuf);

    impl CurrentDirGuard {
        fn set(path: &Path) -> Self {
            let original = std::env::current_dir().unwrap();
            std::env::set_current_dir(path).unwrap();
            Self(original)
        }
    }

    fn run_bare_cwd_child(child_name: &str, marker: &str) {
        let result_dir = tempfile::tempdir().unwrap();

View on GitHub (pinned to affd8760f4)