astrid-runtime/astrid · error

decode public key hex

Error message

decode public key hex: {e}

What it means

pubkey_hex_to_wire converts a 64-char hex Ed25519 public key into the `ed25519:<base64>` wire form using astrid_crypto::PublicKey::from_hex. If the hex string cannot be decoded into a 32-byte public key, the decode error is rethrown as "decode public key hex: {e}".

Solutions

  1. Ensure the input is the raw 64-hex-char public key; if you have the wire form, extract the base64 part and decode to hex instead
  2. Trim the string and strip prefixes/quotes before calling (the code already trims, but embedded whitespace breaks parsing)
  3. Regenerate the keypair and re-export the key if the stored value is corrupt

Example fix

// before
let wire = pubkey_hex_to_wire("ed25519:AbCd...")?; // already wire form
// after
let wire = pubkey_hex_to_wire("a1b2...64-hex-chars")?;
Defensive patterns

Strategy: validation

Validate before calling

fn is_hex_pubkey(s: &str) -> bool {
    let s = s.trim();
    s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit())
}

Type guard

fn convertible_to_wire(s: &str) -> bool {
    let s = s.trim();
    s.len() == 64 && s.chars().all(|c| c.is_ascii_hexdigit())
}

Try / catch

match pubkey_hex_to_wire(pub_hex) {
    Ok(wire) => wire,
    Err(e) if e.to_string().starts_with("decode public key hex") => {
        eprintln!("Input must be 64 hex chars, not the ed25519:<base64> wire form");
        return Err(e);
    }
}

Prevention

When it happens

Trigger: Called with a string that is not exactly 64 valid hex characters: wrong length, invalid hex digits, embedded whitespace inside the string, empty input, or a value already in base64/wire form.

Common situations: Pasting a `ed25519:<base64>` key where hex is expected (double conversion); truncated keys from terminal copy/paste; keys read from config with quotes or prefixes left in.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

Thrown at crates/astrid-cli/src/commands/keypair.rs:616

    SysRng
        .try_fill_bytes(&mut bytes)
        .expect("OS CSPRNG unavailable while generating default keypair name");
    format!("key-{}", hex::encode(bytes))
}

fn fingerprint_pubkey(hex_pub: &str) -> Result<String> {
    PublicKeyFingerprint::from_ed25519_hex(hex_pub)
        .map(PublicKeyFingerprint::into_inner)
        .map_err(|e| anyhow::anyhow!("fingerprint Ed25519 public key: {e}"))
}

/// Convert a 64-char hex ed25519 public key into the `ed25519:<base64>`
/// wire form that `[distro.signing].pubkey`, `astrid distro seal`, and
/// the distro trust store consume. Reuses `astrid-crypto`'s encoder so
/// the base64 variant matches the verifier byte-for-byte.
fn pubkey_hex_to_wire(pub_hex: &str) -> Result<String> {
    let pk = astrid_crypto::PublicKey::from_hex(pub_hex.trim())
        .map_err(|e| anyhow::anyhow!("decode public key hex: {e}"))?;
    Ok(format!("ed25519:{}", pk.to_base64()))
}

/// Encode a 32-byte ed25519 public key in the `OpenSSH` wire format
/// (`ssh-ed25519 <base64>` — RFC 8709 §4). Lets operators paste the
/// same key into `authorized_keys` if they want to reuse it for SSH.
/// The body is a length-prefixed type tag followed by the key.
fn encode_openssh_ed25519(pubkey: &[u8]) -> String {
    use base64::Engine;
    let mut blob = Vec::with_capacity(4 + 11 + 4 + 32);
    let typ = b"ssh-ed25519";
    blob.extend_from_slice(&u32::try_from(typ.len()).unwrap_or(0).to_be_bytes());
    blob.extend_from_slice(typ);
    blob.extend_from_slice(&u32::try_from(pubkey.len()).unwrap_or(0).to_be_bytes());
    blob.extend_from_slice(pubkey);
    format!(
        "ssh-ed25519 {}",
        base64::engine::general_purpose::STANDARD.encode(&blob)

View on GitHub (pinned to affd8760f4)