From 990374dce1bc9d2cf2340e722336b47284bb5dd3 Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Thu, 16 Apr 2026 23:28:14 +0900 Subject: [PATCH 1/5] Implement SecretKey::from_pem to decode variety of keys --- elliptic-curve/src/secret_key.rs | 84 ++++++++++++++++++++++++++++++++ elliptic-curve/tests/pkcs8.rs | 23 +++++++-- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/elliptic-curve/src/secret_key.rs b/elliptic-curve/src/secret_key.rs index 989e219af..e4bf9d41c 100644 --- a/elliptic-curve/src/secret_key.rs +++ b/elliptic-curve/src/secret_key.rs @@ -293,6 +293,90 @@ where } } +#[cfg(feature = "pem")] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PemParseError { + ///Indicates invalid PEM string + Pem(pem::Error), + ///Indicates invalid pkcs8 EC key + Pkcs8(::pkcs8::Error), + ///Indicates invalid Sec1 EC key + Sec1(::sec1::Error), + ///Unable to recognize document label + UnknownLabel, +} + +#[cfg(feature = "pem")] +impl From for PemParseError { + #[inline(always)] + fn from(error: pem::Error) -> Self { + Self::Pem(error) + } +} + +#[cfg(feature = "pem")] +impl From<::pkcs8::Error> for PemParseError { + #[inline(always)] + fn from(error: ::pkcs8::Error) -> Self { + Self::Pkcs8(error) + } +} + +#[cfg(feature = "pem")] +impl From<::sec1::Error> for PemParseError { + #[inline(always)] + fn from(error: ::sec1::Error) -> Self { + Self::Sec1(error) + } +} + +#[cfg(feature = "pem")] +impl fmt::Display for PemParseError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Pem(error) => fmt.write_fmt(format_args!("Failed to parse PEM: {error}")), + Self::UnknownLabel => fmt.write_str("Unrecognized key label"), + Self::Pkcs8(error) => fmt.write_fmt(format_args!("Faoled to parse Pkcs8 key: {error}")), + Self::Sec1(error) => fmt.write_fmt(format_args!("Faoled to parse SEC1 key: {error}")), + } + } +} + +#[cfg(all(feature = "std", feature = "pem"))] +impl core::error::Error for PemParseError {} + +#[cfg(feature = "pem")] +impl SecretKey +where + C: AssociatedOid + Curve + ValidatePublicKey, + FieldBytesSize: ModulusSize, +{ + /// Parse [`SecretKey`] from PEM-encoded private key. + /// + /// Supported formats: + /// - `SEC1` - requires feature `sec1` + /// - `PKCS #8` - requires feature `pkcs8` + /// + /// # Errors + /// - If `pem` is not valid PEM encoded private key + /// - If label within `pem` is not known valid label + /// - If label is valid, but unable to decode DER content of the PEM file + #[cfg(feature = "pem")] + pub fn from_pem(pem: &str) -> ::core::result::Result { + let (label, der_bytes) = pem::decode_vec(pem.as_bytes()).map_err(PemParseError::Pem)?; + + if label == sec1::EcPrivateKey::PEM_LABEL { + return ::sec1::DecodeEcPrivateKey::from_sec1_der(&der_bytes) + .map_err(PemParseError::Sec1); + } else if ::pkcs8::PrivateKeyInfoRef::validate_pem_label(label).is_ok() { + return ::pkcs8::DecodePrivateKey::from_pkcs8_der(&der_bytes) + .map_err(PemParseError::Pkcs8); + } + + Err(PemParseError::UnknownLabel) + } +} + impl ConstantTimeEq for SecretKey where C: Curve, diff --git a/elliptic-curve/tests/pkcs8.rs b/elliptic-curve/tests/pkcs8.rs index 7320de2b6..5291e481c 100644 --- a/elliptic-curve/tests/pkcs8.rs +++ b/elliptic-curve/tests/pkcs8.rs @@ -23,17 +23,34 @@ const EXAMPLE_SCALAR: [u8; 32] = hex!("AABBCCDDEEFF0000000000000000000000000000000000000000000000000001"); /// Example PKCS#8 private key -fn example_private_key() -> der::SecretDocument { +fn example_private_key_der() -> der::SecretDocument { SecretKey::from_slice(&EXAMPLE_SCALAR) .unwrap() .to_pkcs8_der() .unwrap() } +#[cfg(feature = "pem")] +/// Example PKCS#8 private key +fn example_private_key_pem() -> impl AsRef { + SecretKey::from_slice(&EXAMPLE_SCALAR) + .unwrap() + .to_pkcs8_pem(Default::default()) + .unwrap() +} + #[test] fn decode_pkcs8_private_key_from_der() { - dbg!(example_private_key().as_bytes()); - let secret_key = SecretKey::from_pkcs8_der(example_private_key().as_bytes()).unwrap(); + dbg!(example_private_key_der().as_bytes()); + let secret_key = SecretKey::from_pkcs8_der(example_private_key_der().as_bytes()).unwrap(); + assert_eq!(secret_key.to_bytes().as_slice(), &EXAMPLE_SCALAR); +} + +#[cfg(feature = "pem")] +#[test] +fn decode_pkcs8_private_key_from_pem() { + dbg!(example_private_key_pem().as_ref()); + let secret_key = SecretKey::from_pem(example_private_key_pem().as_ref()).unwrap(); assert_eq!(secret_key.to_bytes().as_slice(), &EXAMPLE_SCALAR); } From b220c0f09c2c72c5927dc129e3d172fa43bbb04c Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Thu, 16 Apr 2026 23:39:42 +0900 Subject: [PATCH 2/5] Switch to use SecretDocument to decode PEM --- elliptic-curve/src/secret_key.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/elliptic-curve/src/secret_key.rs b/elliptic-curve/src/secret_key.rs index e4bf9d41c..3da412aa2 100644 --- a/elliptic-curve/src/secret_key.rs +++ b/elliptic-curve/src/secret_key.rs @@ -297,7 +297,7 @@ where #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum PemParseError { ///Indicates invalid PEM string - Pem(pem::Error), + Pem(der::Error), ///Indicates invalid pkcs8 EC key Pkcs8(::pkcs8::Error), ///Indicates invalid Sec1 EC key @@ -307,9 +307,9 @@ pub enum PemParseError { } #[cfg(feature = "pem")] -impl From for PemParseError { +impl From for PemParseError { #[inline(always)] - fn from(error: pem::Error) -> Self { + fn from(error: der::Error) -> Self { Self::Pem(error) } } @@ -363,13 +363,13 @@ where /// - If label is valid, but unable to decode DER content of the PEM file #[cfg(feature = "pem")] pub fn from_pem(pem: &str) -> ::core::result::Result { - let (label, der_bytes) = pem::decode_vec(pem.as_bytes()).map_err(PemParseError::Pem)?; + let (label, document) = der::SecretDocument::from_pem(pem).map_err(PemParseError::Pem)?; - if label == sec1::EcPrivateKey::PEM_LABEL { - return ::sec1::DecodeEcPrivateKey::from_sec1_der(&der_bytes) + if ::sec1::EcPrivateKey::validate_pem_label(label).is_ok() { + return ::sec1::DecodeEcPrivateKey::from_sec1_der(document.as_bytes()) .map_err(PemParseError::Sec1); } else if ::pkcs8::PrivateKeyInfoRef::validate_pem_label(label).is_ok() { - return ::pkcs8::DecodePrivateKey::from_pkcs8_der(&der_bytes) + return ::pkcs8::DecodePrivateKey::from_pkcs8_der(document.as_bytes()) .map_err(PemParseError::Pkcs8); } From d5524be32c61f35ba67dafb102fd873140656ffd Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Thu, 16 Apr 2026 23:41:21 +0900 Subject: [PATCH 3/5] Reorder PEM decoding to give prefence pkcs8 --- elliptic-curve/src/secret_key.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/elliptic-curve/src/secret_key.rs b/elliptic-curve/src/secret_key.rs index 3da412aa2..0dde6a6e1 100644 --- a/elliptic-curve/src/secret_key.rs +++ b/elliptic-curve/src/secret_key.rs @@ -365,12 +365,12 @@ where pub fn from_pem(pem: &str) -> ::core::result::Result { let (label, document) = der::SecretDocument::from_pem(pem).map_err(PemParseError::Pem)?; - if ::sec1::EcPrivateKey::validate_pem_label(label).is_ok() { - return ::sec1::DecodeEcPrivateKey::from_sec1_der(document.as_bytes()) - .map_err(PemParseError::Sec1); - } else if ::pkcs8::PrivateKeyInfoRef::validate_pem_label(label).is_ok() { + if ::pkcs8::PrivateKeyInfoRef::validate_pem_label(label).is_ok() { return ::pkcs8::DecodePrivateKey::from_pkcs8_der(document.as_bytes()) .map_err(PemParseError::Pkcs8); + } else if ::sec1::EcPrivateKey::validate_pem_label(label).is_ok() { + return ::sec1::DecodeEcPrivateKey::from_sec1_der(document.as_bytes()) + .map_err(PemParseError::Sec1); } Err(PemParseError::UnknownLabel) From f9e0b78c41e36c945531aea63d4dd9084c8076f2 Mon Sep 17 00:00:00 2001 From: DoumanAsh Date: Thu, 16 Apr 2026 23:56:16 +0900 Subject: [PATCH 4/5] Validate lael twice to avoid decoding PEM until path is selected --- elliptic-curve/src/secret_key.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/elliptic-curve/src/secret_key.rs b/elliptic-curve/src/secret_key.rs index 0dde6a6e1..893b2b2f7 100644 --- a/elliptic-curve/src/secret_key.rs +++ b/elliptic-curve/src/secret_key.rs @@ -297,7 +297,7 @@ where #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum PemParseError { ///Indicates invalid PEM string - Pem(der::Error), + Pem(pem_rfc7468::Error), ///Indicates invalid pkcs8 EC key Pkcs8(::pkcs8::Error), ///Indicates invalid Sec1 EC key @@ -307,9 +307,9 @@ pub enum PemParseError { } #[cfg(feature = "pem")] -impl From for PemParseError { +impl From for PemParseError { #[inline(always)] - fn from(error: der::Error) -> Self { + fn from(error: pem_rfc7468::Error) -> Self { Self::Pem(error) } } @@ -363,14 +363,12 @@ where /// - If label is valid, but unable to decode DER content of the PEM file #[cfg(feature = "pem")] pub fn from_pem(pem: &str) -> ::core::result::Result { - let (label, document) = der::SecretDocument::from_pem(pem).map_err(PemParseError::Pem)?; + let label = pem_rfc7468::decode_label(pem.as_bytes()).map_err(PemParseError::Pem)?; if ::pkcs8::PrivateKeyInfoRef::validate_pem_label(label).is_ok() { - return ::pkcs8::DecodePrivateKey::from_pkcs8_der(document.as_bytes()) - .map_err(PemParseError::Pkcs8); + return ::pkcs8::DecodePrivateKey::from_pkcs8_pem(pem).map_err(PemParseError::Pkcs8); } else if ::sec1::EcPrivateKey::validate_pem_label(label).is_ok() { - return ::sec1::DecodeEcPrivateKey::from_sec1_der(document.as_bytes()) - .map_err(PemParseError::Sec1); + return ::sec1::DecodeEcPrivateKey::from_sec1_pem(pem).map_err(PemParseError::Sec1); } Err(PemParseError::UnknownLabel) From e5c6dccb660cd611195fb4c0165dcb26518b79c6 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Thu, 16 Apr 2026 09:34:31 -0600 Subject: [PATCH 5/5] Apply suggestion from @tarcieri --- elliptic-curve/src/secret_key.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elliptic-curve/src/secret_key.rs b/elliptic-curve/src/secret_key.rs index 893b2b2f7..830b8d3bd 100644 --- a/elliptic-curve/src/secret_key.rs +++ b/elliptic-curve/src/secret_key.rs @@ -342,7 +342,7 @@ impl fmt::Display for PemParseError { } } -#[cfg(all(feature = "std", feature = "pem"))] +#[cfg(feature = "pem")] impl core::error::Error for PemParseError {} #[cfg(feature = "pem")]