astrid-runtime/astrid · error

invalid ed25519 public key

Error message

invalid ed25519 public key: {e}

What it means

The second stage of parse_pubkey: after the ed25519: prefix is stripped, the remaining base64 is handed to PublicKey::from_base64. If the crypto layer cannot decode it into a valid ed25519 public key (bad base64 characters, wrong length, invalid point), the error is wrapped as "invalid ed25519 public key: {e}".

Solutions

  1. Re-copy the public key value carefully — verify it base64-decodes to exactly 32 bytes.
  2. Regenerate the wire string via pubkey_to_wire(pk) from a key you hold rather than manual copy/paste.
  3. Ensure you are using the public key, not the secret key or a signature, in this field.
  4. Strip whitespace/newlines from the value before passing it (trim in scripts).

Example fix

// before
parse_pubkey(&conf.publisher_key.trim_end_matches('"'))?; // value corrupted by editor
// after
let b64: String = conf.publisher_key.trim().chars().filter(|c| !c.is_whitespace()).collect();
parse_pubkey(&format!("ed25519:{b64}"))?;
Defensive patterns

Strategy: validation

Validate before calling

use base64::Engine;
let b64 = wire.strip_prefix("ed25519:").context("missing ed25519: prefix")?;
let raw = base64::engine::general_purpose::STANDARD.decode(b64.trim())?;
anyhow::ensure!(raw.len() == 32, "public key must decode to 32 bytes, got {}", raw.len());

Type guard

fn valid_pubkey_wire(s: &str) -> bool {
    s.strip_prefix("ed25519:")
        .and_then(|b| base64::engine::general_purpose::STANDARD.decode(b.trim()).ok())
        .map_or(false, |raw| raw.len() == 32)
}

Try / catch

match parse_pubkey(wire) {
    Ok(pk) => pk,
    Err(e) if e.to_string().contains("invalid ed25519 public key") => {
        eprintln!("key payload is not valid base64 / not 32 bytes; re-copy the key");
        return Err(e);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling parse_pubkey with a well-prefixed "ed25519:<b64>" string whose payload is not valid base64 or does not decode to a 32-byte valid ed25519 public key — truncated keys, whitespace/typo corruption, or a signature/secret pasted where a public key belongs. Also hit by the pubkey_wire_roundtrips test.

Common situations: Copy-paste truncation of a long key string; smart quotes or line breaks introduced by editing a config in a rich-text editor; using a key from a different curve/tool that base64-decodes to the wrong length.

Related errors


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

Appendix: source

Thrown at crates/astrid-cli/src/commands/distro/sign.rs:81

    let mut hasher = blake3::Hasher::new();
    hasher.update(SIG_DOMAIN_TAG);
    hasher.update(&bytes);
    Ok(*hasher.finalize().as_bytes())
}

/// Sign a lock with `keypair`, returning the hex `Distro.sig` contents.
pub(crate) fn sign_lock(lock: &DistroLock, keypair: &KeyPair) -> anyhow::Result<String> {
    let digest = lock_signing_digest(lock)?;
    let sig = keypair.sign(&digest);
    Ok(sig.to_hex())
}

/// Parse the `ed25519:<base64>` wire form into a [`PublicKey`].
pub(crate) fn parse_pubkey(wire: &str) -> anyhow::Result<PublicKey> {
    let b64 = wire.strip_prefix("ed25519:").ok_or_else(|| {
        anyhow::anyhow!("public key must be in 'ed25519:<base64>' form, got {wire:?}")
    })?;
    PublicKey::from_base64(b64).map_err(|e| anyhow::anyhow!("invalid ed25519 public key: {e}"))
}

/// 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<()> {

View on GitHub (pinned to affd8760f4)