aaif-goose/goose · error

Unsupported key format '{}'. Expected PKCS#8, PKCS#1, or SEC

Error message

Unsupported key format '{}'. Expected PKCS#8, PKCS#1, or SEC1. Convert with: openssl pkey -in key.pem -out key-pkcs8.pem

What it means

convert_key_to_pkcs8_pem recognizes exactly three PEM tags: 'PRIVATE KEY' (PKCS#8), 'RSA PRIVATE KEY' (PKCS#1), and 'EC PRIVATE KEY' (SEC1). Any other leading tag hits this catch-all arm. Typical offenders: 'ENCRYPTED PRIVATE KEY' (passphrase-protected PKCS#8), 'OPENSSH PRIVATE KEY' (ed25519 keys from ssh-keygen), 'DSA PRIVATE KEY', or a certificate body passed as the key.

Source

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

                .parameters
                .and_then(|p| p.named_curve())
                .ok_or_else(|| {
                    anyhow::anyhow!(
                        "EC key missing curve parameters. Convert to PKCS#8: \
                         openssl pkey -in key.pem -out key-pkcs8.pem"
                    )
                })?;
            let algorithm = pkcs8::AlgorithmIdentifierRef {
                oid: sec1::ALGORITHM_OID,
                parameters: Some((&curve_oid).into()),
            };
            let info = pkcs8::PrivateKeyInfo::new(algorithm, 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)))
        }
        tag => Err(anyhow::anyhow!(
            "Unsupported key format '{}'. Expected PKCS#8, PKCS#1, or SEC1. \
             Convert with: openssl pkey -in key.pem -out key-pkcs8.pem",
            tag
        )),
    }
}

#[async_trait]
pub trait AuthProvider: Send + Sync {
    async fn get_auth_header(&self) -> Result<(String, String)>;

    async fn refresh_credentials(&self) -> Result<()> {
        anyhow::bail!("credential refresh not supported")
    }
}

pub struct ApiResponse {
    pub status: StatusCode,

View on GitHub (pinned to 3810898a74)

Solutions

  1. Convert whatever you have to unencrypted PKCS#8: openssl pkey -in key.pem -out key-pkcs8.pem (use -passin for encrypted input)
  2. For encrypted PKCS#8 specifically: openssl pkcs8 -topk8 -nocrypt -in key.pem -out key-pkcs8.pem
  3. For OpenSSH ed25519 keys: openssl pkey -in id_ed25519 -out key-pkcs8.pem (recent OpenSSL reads OpenSSH format)
  4. Double-check key_path really points at the private key ('openssl pkey -in <file> -noout' must succeed)

Example fix

# before
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAA...
# key_path = id_ed25519 -> Unsupported key format 'OPENSSH PRIVATE KEY'

# after
openssl pkey -in id_ed25519 -out key-pkcs8.pem
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEG...
-----END PRIVATE KEY-----
Defensive patterns

Strategy: validation

Validate before calling

const SUPPORTED: [&str; 3] = ["PRIVATE KEY", "RSA PRIVATE KEY", "EC PRIVATE KEY"];
fn pem_tag_ok(path: &str) -> anyhow::Result<()> {
    let first = std::fs::read_to_string(path)?
        .lines().find(|l| l.starts_with("-----BEGIN"))
        .unwrap_or_default().to_string();
    let tag = first.trim_start_matches("-----BEGIN").trim_end_matches("-----").trim();
    anyhow::ensure!(SUPPORTED.contains(&tag),
        "unsupported PEM tag '{tag}'; convert: openssl pkey -in {path} -out {path}.pkcs8");
    Ok(())
}

Try / catch

match build_identity(cert, key) {
    Err(e) if e.to_string().contains("Unsupported key format") =>
        eprintln!("convert the key first: openssl pkey -in {key} -out {key}.pkcs8 (or -passin for encrypted keys)"),
    r => r?,
}

Prevention

When it happens

Trigger: Configuring TlsConfig::with_client_cert_and_key (native-tls build) with a key file that is passphrase-encrypted, in OpenSSH format, DSA, or actually the certificate/public half instead of the private key.

Common situations: Teams generate ed25519 keys with ssh-keygen by habit and hand them to mTLS config; security teams deliver passphrase-protected PKCS#8; someone pastes cert.pem into the key_path field. Note the rustls path (Identity::from_pem) is equally strict about encryption, so the fix is the same regardless of backend.

Related errors


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