aaif-goose/goose · error

Failed to parse PEM key: {}

Error message

Failed to parse PEM key: {}

What it means

On the native-tls build, convert_key_to_pkcs8_pem re-encodes legacy key formats (PKCS#1 RSA, SEC1 EC) into PKCS#8. First it runs pem::parse on the key text; if the file is not a valid PEM document at all — binary DER, encrypted/garbled content, missing armor — parsing fails with 'Failed to parse PEM key: {err}'. This is a key-format problem, not a file-read problem.

Source

Thrown at crates/goose-providers/src/api_client.rs:165

///   Generated by default with `openssl genrsa`. Very common in older setups, tutorials,
///   and CA-issued key files.
/// - **SEC1** (`-----BEGIN EC PRIVATE KEY-----`): Legacy EC-specific format.
///   Generated by default with `openssl ecparam -genkey`. Common with EC certificates.
/// - **PKCS#8** (`-----BEGIN PRIVATE KEY-----`): Generic, algorithm-agnostic wrapper.
///   This is the only format native-tls accepts.
///
/// Without this conversion, users with legacy-format keys (Kubernetes secrets, corporate
/// PKI, etc.) would get a cryptic "Failed to create identity" error and need to manually
/// run `openssl pkey -in key.pem -out key-pkcs8.pem`.
///
/// Note: the rustls code path (`Identity::from_pem`) accepts all formats natively,
/// so this conversion is only needed for native-tls.
#[cfg(feature = "native-tls")]
fn convert_key_to_pkcs8_pem(key_pem_str: &str) -> Result<String> {
    use pkcs8::der::{Decode, Encode};

    let parsed =
        pem::parse(key_pem_str).map_err(|e| anyhow::anyhow!("Failed to parse PEM key: {}", e))?;

    match parsed.tag() {
        "PRIVATE KEY" => Ok(key_pem_str.to_string()),
        "RSA PRIVATE KEY" => {
            let info = pkcs8::PrivateKeyInfo::new(pkcs1::ALGORITHM_ID, parsed.contents());
            let der_bytes = info
                .to_der()
                .map_err(|e| anyhow::anyhow!("Failed to encode PKCS#8: {}", e))?;
            Ok(pem::encode(&pem::Pem::new("PRIVATE KEY", der_bytes)))
        }
        "EC PRIVATE KEY" => {
            let ec_key = sec1::EcPrivateKey::from_der(parsed.contents())
                .map_err(|e| anyhow::anyhow!("Failed to parse EC key: {}", e))?;
            let curve_oid = ec_key
                .parameters
                .and_then(|p| p.named_curve())
                .ok_or_else(|| {
                    anyhow::anyhow!(

View on GitHub (pinned to 3810898a74)

Solutions

  1. Check the first line of the key file: it must read -----BEGIN ... PRIVATE KEY-----
  2. Convert DER to PEM: openssl pkey -inform der -in key.der -out key.pem
  3. Extract a key from PKCS#12 instead: openssl pkcs12 -in bundle.pfx -nocerts -nodes -out key.pem
  4. Remove passphrase protection: openssl pkey -in key.enc -passin pass:... -out key.pem

Example fix

# before
client_identity:
  key_path: /etc/goose/tls/client-key.der   # binary DER

# after (shell)
$ openssl pkey -inform der -in /etc/goose/tls/client-key.der -out /etc/goose/tls/client-key.pem
# key_path: /etc/goose/tls/client-key.pem  (text PEM with armor)
Defensive patterns

Strategy: validation

Validate before calling

let key_text = std::fs::read_to_string(&key_path)?;
anyhow::ensure!(
    key_text.contains("-----BEGIN") && key_text.contains("PRIVATE KEY-----"),
    "key file is not PEM armor; convert DER: openssl pkey -inform der -in {key_path} -out key.pem"
);

Type guard

fn looks_like_pem_key(text: &str) -> bool {
    text.contains("-----BEGIN") && text.contains("PRIVATE KEY-----")
}

Prevention

When it happens

Trigger: client_identity key_path contains DER-encoded key bytes (no -----BEGIN----- armor), a PKCS#12 (.p12/.pfx) file, an encrypted PEM, or a corrupted download. Only compiled with the native-tls feature; fires at client build time after the key file was read as text.

Common situations: Kubernetes secrets or corporate PKI handing out DER keys; users pointing key_path at a .pfx bundle; Windows-exported keys in PKCS#12; truncated file transfers.

Understand the failure class

Related errors


AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16). Data as JSON: /api/errors/0fd17b686bab905f. Report an issue: GitHub.