[dependencies]p256 = { version = "=0.14.0", features = ["pem"] } use p256::SecretKey;use p256::pkcs8::{DecodePrivateKey, EncodePublicKey, LineEnding}; fn main() -> Result<(), Box<dyn std::error::Error>> { let pem = std::fs::read_to_string(std::env::args().nth(1).unwrap_or("key.pem".into()))?; // "BEGIN PRIVATE KEY" is PKCS#8; legacy "BEGIN EC PRIVATE KEY" is SEC 1 let secret = if pem.contains("BEGIN EC PRIVATE KEY") { SecretKey::from_sec1_pem(&pem)? } else { SecretKey::from_pkcs8_pem(&pem)? }; let public_pem = secret.public_key().to_public_key_pem(LineEnding::LF)?; // SPKI std::fs::write("pub.pem", &public_pem)?; print!("{public_pem}"); Ok(())} How it works
SecretKey::from_pkcs8_pemreadsBEGIN PRIVATE KEYandSecretKey::from_sec1_pemreadsBEGIN EC PRIVATE KEY; the PEM label decides which one applies.public_key()computes Q = d·G on P-256 — EC public keys are always derivable from the private scalar, whether or not the file stored the point.to_public_key_pem(traitEncodePublicKey) writes the SPKI with the curve OID, at 64 columns with the line ending you choose;to_public_key_dergives the same structure as bytes.- The run compares the output byte for byte with
openssl pkey -puboutand checks that the SEC 1 file yields the identical PEM.
Gotchas
- The key type is per curve:
p256::SecretKeyrejects a P-384 key with a decoding error. For keys of unknown curve, inspect the algorithm OID first (for example with thepkcs8crate'sPrivateKeyInfo) and dispatch top384,p521ork256. from_pkcs8_pemon a SEC 1 file (and vice versa) fails with a PEM label error rather than guessing — check the label, as above, instead of trying both blindly.- Encrypted keys (
BEGIN ENCRYPTED PRIVATE KEY) need theencryptionfeature ofpkcs8andfrom_pkcs8_encrypted_pem; the plain loaders refuse them. ringcannot export a public key as SPKI:public_key().as_ref()is the bare 65-byte point. Wrap it in SPKI with thespkicrate or use RustCrypto types for anything that must be read by other tools.