diff --git a/.github/workflows/pkcs8.yml b/.github/workflows/pkcs8.yml index 9ba172ce5..ad8666c17 100644 --- a/.github/workflows/pkcs8.yml +++ b/.github/workflows/pkcs8.yml @@ -42,7 +42,7 @@ jobs: target: ${{ matrix.target }} override: true - uses: RustCrypto/actions/cargo-hack-install@master - - run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features std,rand + - run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features getrandom,std,rand minimal-versions: uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master diff --git a/Cargo.lock b/Cargo.lock index 11e196222..95eddfd89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -389,6 +389,7 @@ dependencies = [ "pem-rfc7468", "proptest", "time", + "zeroize", ] [[package]] @@ -677,6 +678,7 @@ dependencies = [ "der", "hex-literal", "pkcs8", + "tempfile", "zeroize", ] @@ -716,7 +718,7 @@ dependencies = [ "rand_core 0.6.3", "spki", "subtle", - "zeroize", + "tempfile", ] [[package]] @@ -1022,6 +1024,7 @@ dependencies = [ "pkcs8", "serde", "subtle", + "tempfile", "zeroize", ] @@ -1147,6 +1150,7 @@ dependencies = [ "der", "hex-literal", "sha2 0.10.2", + "tempfile", ] [[package]] diff --git a/der/Cargo.toml b/der/Cargo.toml index 09d40fa55..61224c0e1 100644 --- a/der/Cargo.toml +++ b/der/Cargo.toml @@ -21,6 +21,7 @@ der_derive = { version = "=0.6.0-pre.3", optional = true, path = "derive" } flagset = { version = "0.4.3", optional = true } pem-rfc7468 = { version = "0.5", optional = true, path = "../pem-rfc7468" } time = { version = "0.3.4", optional = true, default-features = false } +zeroize = { version = "1.5", optional = true, default-features = false, features = ["alloc"] } [dev-dependencies] hex-literal = "0.3.3" @@ -30,7 +31,7 @@ proptest = "1" alloc = [] derive = ["der_derive"] oid = ["const-oid"] -pem = ["alloc", "pem-rfc7468/alloc"] +pem = ["alloc", "pem-rfc7468/alloc", "zeroize"] std = ["alloc"] [package.metadata.docs.rs] diff --git a/der/src/decode.rs b/der/src/decode.rs index b42ac5486..74f1b7e3e 100644 --- a/der/src/decode.rs +++ b/der/src/decode.rs @@ -2,6 +2,12 @@ use crate::{Decoder, FixedTag, Header, Result}; +#[cfg(feature = "pem")] +use { + crate::pem::{self, PemLabel}, + zeroize::Zeroize, +}; + #[cfg(doc)] use crate::{Length, Tag}; @@ -50,6 +56,31 @@ pub trait DecodeOwned: for<'a> Decode<'a> {} impl DecodeOwned for T where T: for<'a> Decode<'a> {} +/// PEM decoding trait. +/// +/// This trait is automatically impl'd for any type which impls both +/// [`DecodeOwned`] and [`PemLabel`]. +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +pub trait DecodePem: DecodeOwned + PemLabel { + /// Try to decode this type from PEM. + fn from_pem(pem: &str) -> Result; +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl DecodePem for T { + fn from_pem(pem: &str) -> Result { + // TODO(tarcieri): support for decoding directly from PEM (instead of two-pass) + let (label, mut der_bytes) = pem::decode_vec(pem.as_bytes())?; + Self::validate_pem_label(label)?; + + let result = T::from_der(&der_bytes); + der_bytes.zeroize(); + result + } +} + /// Decode the value part of a Tag-Length-Value encoded field, sans the [`Tag`] /// and [`Length`]. pub trait DecodeValue<'a>: Sized { diff --git a/der/src/document.rs b/der/src/document.rs index cf6ef4d7e..5cf91a94b 100644 --- a/der/src/document.rs +++ b/der/src/document.rs @@ -1,7 +1,8 @@ //! ASN.1 DER-encoded documents stored on the heap. -use crate::{Decode, Encode, Error, Result}; -use alloc::{boxed::Box, vec::Vec}; +use crate::{Decode, Decoder, Encode, Encoder, Error, FixedTag, Length, Result, Tag}; +use alloc::vec::Vec; +use core::fmt::{self, Debug}; #[cfg(feature = "pem")] use {crate::pem, alloc::string::String}; @@ -9,128 +10,337 @@ use {crate::pem, alloc::string::String}; #[cfg(feature = "std")] use std::{fs, path::Path}; +#[cfg(all(feature = "pem", feature = "std"))] +use alloc::borrow::ToOwned; + +#[cfg(feature = "zeroize")] +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + /// ASN.1 DER-encoded document. /// -/// This trait is intended to impl on types which contain an ASN.1 DER-encoded -/// document which is guaranteed to encode as the associated `Message` type. +/// This type wraps an encoded ASN.1 DER message. The document checked to +/// ensure it contains a valid DER-encoded `SEQUENCE`. /// /// It implements common functionality related to encoding/decoding such /// documents, such as PEM encapsulation as well as reading/writing documents /// from/to the filesystem. +/// +/// The [`SecretDocument`] provides a wrapper for this type with additional +/// hardening applied. #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] -pub trait Document<'a>: AsRef<[u8]> + Sized + TryFrom, Error = Error> { - /// ASN.1 message type this document decodes to. - type Message: Decode<'a> + Encode + Sized; +#[derive(Clone, Eq, PartialEq)] +pub struct Document { + /// ASN.1 DER encoded bytes. + der_bytes: Vec, - /// Does this type contain potentially sensitive data? - /// - /// This enables hardened file permissions when persisting data to disk. - const SENSITIVE: bool; + /// Length of this document. + length: Length, +} - /// Borrow the inner serialized bytes of this document. - fn as_der(&self) -> &[u8] { - self.as_ref() +impl Document { + /// Get the ASN.1 DER-encoded bytes of this document. + pub fn as_bytes(&self) -> &[u8] { + self.der_bytes.as_slice() } - /// Return an allocated ASN.1 DER serialization as a boxed slice. - fn to_der(&self) -> Box<[u8]> { - self.as_ref().to_vec().into_boxed_slice() + /// Convert to a [`SecretDocument`]. + #[cfg(feature = "zeroize")] + #[cfg_attr(docsrs, doc(cfg(feature = "zeroize")))] + pub fn into_secret(self) -> SecretDocument { + SecretDocument(self) } - /// Decode this document as ASN.1 DER. - fn decode(&'a self) -> Self::Message { - Self::Message::from_der(self.as_ref()).expect("ASN.1 DER document malformed") + /// Convert to an ASN.1 DER-encoded byte vector. + pub fn into_vec(self) -> Vec { + self.der_bytes } - /// Create a new document from the provided ASN.1 DER bytes. - fn from_der(bytes: &[u8]) -> Result { - bytes.to_vec().try_into() + /// Return an ASN.1 DER-encoded byte vector. + pub fn to_vec(&self) -> Vec { + self.der_bytes.clone() } - /// Encode the provided type as ASN.1 DER. - fn from_msg(msg: &Self::Message) -> Result { + /// Get the length of the encoded ASN.1 DER in bytes. + pub fn len(&self) -> Length { + self.length + } + + /// Try to decode the inner ASN.1 DER message contained in this + /// [`Document`] as the given type. + pub fn decode_msg<'a, T: Decode<'a>>(&'a self) -> Result { + T::from_der(self.as_bytes()) + } + + /// Encode the provided type as ASN.1 DER, storing the resulting encoded DER + /// as a [`Document`]. + pub fn encode_msg(msg: &T) -> Result { msg.to_vec()?.try_into() } /// Decode ASN.1 DER document from PEM. + /// + /// Returns the PEM label and decoded [`Document`] on success. #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - fn from_pem(s: &str) -> Result - where - Self: pem::PemLabel, - { - let (label, der_bytes) = pem::decode_vec(s.as_bytes())?; - - if label != Self::PEM_LABEL { - return Err(pem::Error::Label.into()); - } - - der_bytes.try_into() + pub fn from_pem(pem: &str) -> Result<(&str, Self)> { + let (label, der_bytes) = pem::decode_vec(pem.as_bytes())?; + Ok((label, der_bytes.try_into()?)) } - /// Encode ASN.1 DER document as a PEM string. + /// Encode ASN.1 DER document as a PEM string with encapsulation boundaries + /// containing the provided PEM type `label` (e.g. `CERTIFICATE`). #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - fn to_pem(&self, line_ending: pem::LineEnding) -> Result - where - Self: pem::PemLabel, - { - Ok(pem::encode_string( - Self::PEM_LABEL, - line_ending, - self.as_ref(), - )?) + pub fn to_pem(&self, label: &'static str, line_ending: pem::LineEnding) -> Result { + Ok(pem::encode_string(label, line_ending, self.as_bytes())?) } /// Read ASN.1 DER document from a file. #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn read_der_file(path: impl AsRef) -> Result { + pub fn read_der_file(path: impl AsRef) -> Result { fs::read(path)?.try_into() } + /// Write ASN.1 DER document to a file. + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + pub fn write_der_file(&self, path: impl AsRef) -> Result<()> { + Ok(fs::write(path, self.as_bytes())?) + } + /// Read PEM-encoded ASN.1 DER document from a file. #[cfg(all(feature = "pem", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(all(feature = "pem", feature = "std"))))] - fn read_pem_file(path: impl AsRef) -> Result - where - Self: pem::PemLabel, - { - Self::from_pem(&fs::read_to_string(path)?) + pub fn read_pem_file(path: impl AsRef) -> Result<(String, Self)> { + Self::from_pem(&fs::read_to_string(path)?).map(|(label, doc)| (label.to_owned(), doc)) + } + + /// Write PEM-encoded ASN.1 DER document to a file. + #[cfg(all(feature = "pem", feature = "std"))] + #[cfg_attr(docsrs, doc(cfg(all(feature = "pem", feature = "std"))))] + pub fn write_pem_file( + &self, + path: impl AsRef, + label: &'static str, + line_ending: pem::LineEnding, + ) -> Result<()> { + let pem = self.to_pem(label, line_ending)?; + Ok(fs::write(path, pem.as_bytes())?) + } +} + +impl AsRef<[u8]> for Document { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl Debug for Document { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Document(")?; + + for byte in self.as_bytes() { + write!(f, "{:02X}", byte)?; + } + + f.write_str(")") + } +} + +impl Decode<'_> for Document { + fn decode(decoder: &mut Decoder<'_>) -> Result { + let header = decoder.peek_header()?; + let length = (header.encoded_len()? + header.length)?; + let bytes = decoder.bytes(length)?; + Ok(Self { + der_bytes: bytes.into(), + length, + }) + } +} + +impl Encode for Document { + fn encoded_len(&self) -> Result { + Ok(self.len()) + } + + fn encode(&self, encoder: &mut Encoder<'_>) -> Result<()> { + encoder.bytes(self.as_bytes()) + } +} + +impl FixedTag for Document { + const TAG: Tag = Tag::Sequence; +} + +impl TryFrom<&[u8]> for Document { + type Error = Error; + + fn try_from(der_bytes: &[u8]) -> Result { + Self::from_der(der_bytes) + } +} + +impl TryFrom> for Document { + type Error = Error; + + fn try_from(der_bytes: Vec) -> Result { + let mut decoder = Decoder::new(&der_bytes)?; + decode_sequence(&mut decoder)?; + decoder.finish(())?; + + let length = der_bytes.len().try_into()?; + Ok(Self { der_bytes, length }) + } +} + +/// Secret [`Document`] type. +/// +/// Useful for formats which represent potentially secret data, such as +/// cryptographic keys. +/// +/// This type provides additional hardening such as ensuring that the contents +/// are zeroized-on-drop, and also using more restrictive file permissions when +/// writing files to disk. +#[cfg(feature = "zeroize")] +#[cfg_attr(docsrs, doc(cfg(all(feature = "alloc", feature = "zeroize"))))] +#[derive(Clone)] +pub struct SecretDocument(Document); + +#[cfg(feature = "zeroize")] +impl SecretDocument { + /// Borrow the inner serialized bytes of this document. + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + /// Return an allocated ASN.1 DER serialization as a byte vector. + pub fn to_bytes(&self) -> Zeroizing> { + Zeroizing::new(self.0.to_vec()) + } + + /// Get the length of the encoded ASN.1 DER in bytes. + pub fn len(&self) -> Length { + self.0.len() + } + + /// Try to decode the inner ASN.1 DER message as the given type. + pub fn decode_msg<'a, T: Decode<'a>>(&'a self) -> Result { + self.0.decode_msg() + } + + /// Encode the provided type as ASN.1 DER. + pub fn encode_msg(msg: &T) -> Result { + Document::encode_msg(msg).map(Self) + } + + /// Decode ASN.1 DER document from PEM. + #[cfg(feature = "pem")] + #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] + pub fn from_pem(pem: &str) -> Result<(&str, Self)> { + Document::from_pem(pem).map(|(label, doc)| (label, Self(doc))) + } + + /// Encode ASN.1 DER document as a PEM string. + #[cfg(feature = "pem")] + #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] + pub fn to_pem( + &self, + label: &'static str, + line_ending: pem::LineEnding, + ) -> Result> { + self.0.to_pem(label, line_ending).map(Zeroizing::new) + } + + /// Read ASN.1 DER document from a file. + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + pub fn read_der_file(path: impl AsRef) -> Result { + Document::read_der_file(path).map(Self) } /// Write ASN.1 DER document to a file. #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn write_der_file(&self, path: impl AsRef) -> Result<()> { - write_file(path, self.as_ref(), Self::SENSITIVE) + pub fn write_der_file(&self, path: impl AsRef) -> Result<()> { + write_secret_file(path, self.as_bytes()) + } + + /// Read PEM-encoded ASN.1 DER document from a file. + #[cfg(all(feature = "pem", feature = "std"))] + #[cfg_attr(docsrs, doc(cfg(all(feature = "pem", feature = "std"))))] + pub fn read_pem_file(path: impl AsRef) -> Result<(String, Self)> { + Document::read_pem_file(path).map(|(label, doc)| (label, Self(doc))) } /// Write PEM-encoded ASN.1 DER document to a file. #[cfg(all(feature = "pem", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(all(feature = "pem", feature = "std"))))] - fn write_pem_file(&self, path: impl AsRef, line_ending: pem::LineEnding) -> Result<()> - where - Self: pem::PemLabel, - { - write_file(path, self.to_pem(line_ending)?.as_bytes(), Self::SENSITIVE) + pub fn write_pem_file( + &self, + path: impl AsRef, + label: &'static str, + line_ending: pem::LineEnding, + ) -> Result<()> { + write_secret_file(path, self.to_pem(label, line_ending)?.as_bytes()) + } +} +#[cfg(feature = "zeroize")] +impl Debug for SecretDocument { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("SecretDocument").finish_non_exhaustive() } } -/// Write a file to the filesystem, potentially using hardened permissions -/// if the file contains secret data. -#[cfg(feature = "std")] -fn write_file(path: impl AsRef, data: &[u8], sensitive: bool) -> Result<()> { - if sensitive { - write_secret_file(path, data) - } else { - Ok(fs::write(path, data)?) +#[cfg(feature = "zeroize")] +impl Drop for SecretDocument { + fn drop(&mut self) { + self.0.der_bytes.zeroize(); + } +} + +#[cfg(feature = "zeroize")] +impl From for SecretDocument { + fn from(doc: Document) -> SecretDocument { + SecretDocument(doc) } } +#[cfg(feature = "zeroize")] +impl TryFrom<&[u8]> for SecretDocument { + type Error = Error; + + fn try_from(der_bytes: &[u8]) -> Result { + Document::try_from(der_bytes).map(Self) + } +} + +#[cfg(feature = "zeroize")] +impl TryFrom> for SecretDocument { + type Error = Error; + + fn try_from(der_bytes: Vec) -> Result { + Document::try_from(der_bytes).map(Self) + } +} + +#[cfg(feature = "zeroize")] +impl ZeroizeOnDrop for SecretDocument {} + +/// Attempt to decode a ASN.1 `SEQUENCE` from the given decoder, returning the +/// entire sequence including the header. +fn decode_sequence<'a>(decoder: &mut Decoder<'a>) -> Result<&'a [u8]> { + let header = decoder.peek_header()?; + header.tag.assert_eq(Tag::Sequence)?; + + let len = (header.encoded_len()? + header.length)?; + decoder.bytes(len) +} + /// Write a file containing secret data to the filesystem, restricting the /// file permissions so it's only readable by the owner -#[cfg(all(unix, feature = "std"))] +#[cfg(all(unix, feature = "std", feature = "zeroize"))] fn write_secret_file(path: impl AsRef, data: &[u8]) -> Result<()> { use std::{io::Write, os::unix::fs::OpenOptionsExt}; @@ -151,7 +361,7 @@ fn write_secret_file(path: impl AsRef, data: &[u8]) -> Result<()> { /// Write a file containing secret data to the filesystem // TODO(tarcieri): permissions hardening on Windows -#[cfg(all(not(unix), feature = "std"))] +#[cfg(all(not(unix), feature = "std", feature = "zeroize"))] fn write_secret_file(path: impl AsRef, data: &[u8]) -> Result<()> { fs::write(path, data)?; Ok(()) diff --git a/der/src/encode.rs b/der/src/encode.rs index 7c37c28f6..7b4532b0b 100644 --- a/der/src/encode.rs +++ b/der/src/encode.rs @@ -5,6 +5,13 @@ use crate::{Encoder, Header, Length, Result, Tagged}; #[cfg(feature = "alloc")] use {crate::ErrorKind, alloc::vec::Vec, core::iter}; +#[cfg(feature = "pem")] +use { + alloc::string::String, + pem_rfc7468::{self as pem, LineEnding, PemLabel}, + zeroize::Zeroizing, +}; + #[cfg(doc)] use crate::Tag; @@ -74,6 +81,27 @@ where } } +/// PEM encoding trait. +/// +/// This trait is automatically impl'd for any type which impls both +/// [`Encode`] and [`PemLabel`]. +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +pub trait EncodePem: Encode + PemLabel { + /// Try to encode this type as PEM. + fn to_pem(&self, line_ending: LineEnding) -> Result; +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl EncodePem for T { + fn to_pem(&self, line_ending: LineEnding) -> Result { + // TODO(tarcieri): support for encoding directly from PEM (instead of two-pass) + let der = Zeroizing::new(self.to_vec()?); + Ok(pem::encode_string(Self::PEM_LABEL, line_ending, &der)?) + } +} + /// Encode the value part of a Tag-Length-Value encoded field, sans the [`Tag`] /// and [`Length`]. pub trait EncodeValue { diff --git a/der/src/length.rs b/der/src/length.rs index b5b91b75b..a27889e29 100644 --- a/der/src/length.rs +++ b/der/src/length.rs @@ -155,6 +155,12 @@ impl From for Length { } } +impl From for u32 { + fn from(length: Length) -> u32 { + length.0 + } +} + impl TryFrom for Length { type Error = Error; @@ -167,12 +173,6 @@ impl TryFrom for Length { } } -impl From for u32 { - fn from(length: Length) -> u32 { - length.0 - } -} - impl TryFrom for Length { type Error = Error; diff --git a/der/src/lib.rs b/der/src/lib.rs index d882c6537..e8dacd7c6 100644 --- a/der/src/lib.rs +++ b/der/src/lib.rs @@ -179,7 +179,7 @@ //! // //! // When the `alloc` feature of this crate is enabled, any type that impls //! // the `Encode` trait including all ASN.1 built-in types and any type -//! // which impls `Sequence` can be serialized by calling `Encode::to_vec()`. +//! // which impls `Sequence` can be serialized by calling `Encode::to_der()`. //! // //! // If you would prefer to avoid allocations, you can create a byte array //! // as backing storage instead, pass that to `der::Encoder::new`, and then @@ -369,7 +369,7 @@ pub use crate::{ }; #[cfg(feature = "alloc")] -pub use document::Document; +pub use crate::document::Document; #[cfg(feature = "bigint")] #[cfg_attr(docsrs, doc(cfg(feature = "bigint")))] @@ -381,10 +381,19 @@ pub use der_derive::{Choice, Enumerated, Newtype, Sequence, ValueOrd}; #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -pub use pem_rfc7468 as pem; +pub use { + crate::{decode::DecodePem, encode::EncodePem}, + pem_rfc7468 as pem, +}; #[cfg(feature = "time")] #[cfg_attr(docsrs, doc(cfg(feature = "time")))] pub use time; +#[cfg(feature = "zeroize")] +pub use zeroize; + +#[cfg(all(feature = "alloc", feature = "zeroize"))] +pub use crate::document::SecretDocument; + pub(crate) use crate::{arrayvec::ArrayVec, byte_slice::ByteSlice, str_slice::StrSlice}; diff --git a/der/src/tag/number.rs b/der/src/tag/number.rs index 1e101fae4..1930e9436 100644 --- a/der/src/tag/number.rs +++ b/der/src/tag/number.rs @@ -125,10 +125,12 @@ impl TagNumber { /// /// Panics if the tag number is greater than `30`. /// For a fallible conversion, use [`TryFrom`] instead. - #[allow(clippy::no_effect)] pub const fn new(byte: u8) -> Self { - // TODO(tarcieri): hax! use const panic when available - ["tag number out of range"][(byte > Self::MAX) as usize]; + #[allow(clippy::panic)] + if byte > Self::MAX { + panic!("tag number out of range"); + } + Self(byte) } diff --git a/pkcs1/Cargo.toml b/pkcs1/Cargo.toml index 5854cec3b..53114d8f3 100644 --- a/pkcs1/Cargo.toml +++ b/pkcs1/Cargo.toml @@ -23,6 +23,7 @@ zeroize = { version = "1", optional = true, default-features = false } [dev-dependencies] hex-literal = "0.3" +tempfile = "3" [features] alloc = ["der/alloc", "pkcs8/alloc", "zeroize/alloc"] diff --git a/pkcs1/src/error.rs b/pkcs1/src/error.rs index 06cb73aeb..135bcd7d9 100644 --- a/pkcs1/src/error.rs +++ b/pkcs1/src/error.rs @@ -2,6 +2,9 @@ use core::fmt; +#[cfg(feature = "pem")] +use der::pem; + /// Result type pub type Result = core::result::Result; @@ -46,6 +49,13 @@ impl From for Error { } } +#[cfg(feature = "pem")] +impl From for Error { + fn from(err: pem::Error) -> Error { + der::Error::from(err).into() + } +} + #[cfg(feature = "pkcs8")] impl From for pkcs8::Error { fn from(err: Error) -> pkcs8::Error { diff --git a/pkcs1/src/lib.rs b/pkcs1/src/lib.rs index f29cc315e..871f3871b 100644 --- a/pkcs1/src/lib.rs +++ b/pkcs1/src/lib.rs @@ -35,10 +35,7 @@ pub use self::{ #[cfg(feature = "alloc")] pub use crate::{ - private_key::{ - document::RsaPrivateKeyDocument, other_prime_info::OtherPrimeInfo, OtherPrimeInfos, - }, - public_key::document::RsaPublicKeyDocument, + private_key::{other_prime_info::OtherPrimeInfo, OtherPrimeInfos}, traits::{EncodeRsaPrivateKey, EncodeRsaPublicKey}, }; diff --git a/pkcs1/src/private_key.rs b/pkcs1/src/private_key.rs index 78b0b516d..5daad5ad8 100644 --- a/pkcs1/src/private_key.rs +++ b/pkcs1/src/private_key.rs @@ -1,7 +1,5 @@ //! PKCS#1 RSA Private Keys. -#[cfg(feature = "alloc")] -pub(crate) mod document; #[cfg(feature = "alloc")] pub(crate) mod other_prime_info; @@ -10,14 +8,10 @@ use core::fmt; use der::{asn1::UIntBytes, Decode, Decoder, Encode, Sequence, Tag}; #[cfg(feature = "alloc")] -use {self::other_prime_info::OtherPrimeInfo, crate::RsaPrivateKeyDocument, alloc::vec::Vec}; +use {self::other_prime_info::OtherPrimeInfo, alloc::vec::Vec, der::SecretDocument}; #[cfg(feature = "pem")] -use { - crate::{EncodeRsaPrivateKey, LineEnding}, - alloc::string::String, - zeroize::Zeroizing, -}; +use der::pem::PemLabel; /// PKCS#1 RSA Private Keys as defined in [RFC 8017 Appendix 1.2]. /// @@ -93,21 +87,6 @@ impl<'a> RsaPrivateKey<'a> { Version::TwoPrime } } - - /// Encode this [`RsaPrivateKey`] as ASN.1 DER. - #[cfg(feature = "alloc")] - #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] - pub fn to_der(&self) -> Result { - self.try_into() - } - - /// Encode this [`RsaPrivateKey`] as PEM-encoded ASN.1 DER using the given - /// [`LineEnding`]. - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - pub fn to_pem(&self, line_ending: LineEnding) -> Result> { - self.to_der()?.to_pkcs1_pem(line_ending) - } } impl<'a> Decode<'a> for RsaPrivateKey<'a> { @@ -178,7 +157,7 @@ impl<'a> TryFrom<&'a [u8]> for RsaPrivateKey<'a> { } } -impl<'a> fmt::Debug for RsaPrivateKey<'a> { +impl fmt::Debug for RsaPrivateKey<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("RsaPrivateKey") .field("version", &self.version()) @@ -188,6 +167,32 @@ impl<'a> fmt::Debug for RsaPrivateKey<'a> { } } +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl TryFrom> for SecretDocument { + type Error = Error; + + fn try_from(private_key: RsaPrivateKey<'_>) -> Result { + SecretDocument::try_from(&private_key) + } +} + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl TryFrom<&RsaPrivateKey<'_>> for SecretDocument { + type Error = Error; + + fn try_from(private_key: &RsaPrivateKey<'_>) -> Result { + Ok(Self::encode_msg(private_key)?) + } +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl PemLabel for RsaPrivateKey<'_> { + const PEM_LABEL: &'static str = "RSA PRIVATE KEY"; +} + /// Placeholder struct for `OtherPrimeInfos` in the no-`alloc` case. #[cfg(not(feature = "alloc"))] #[derive(Clone)] diff --git a/pkcs1/src/private_key/document.rs b/pkcs1/src/private_key/document.rs deleted file mode 100644 index e49c1afde..000000000 --- a/pkcs1/src/private_key/document.rs +++ /dev/null @@ -1,149 +0,0 @@ -//! PKCS#1 RSA private key document. - -use crate::{DecodeRsaPrivateKey, EncodeRsaPrivateKey, Error, Result, RsaPrivateKey}; -use alloc::vec::Vec; -use core::fmt; -use der::{Decode, Document, Encode}; -use zeroize::{Zeroize, Zeroizing}; - -#[cfg(feature = "pem")] -use { - crate::{pem, LineEnding}, - alloc::string::String, - core::str::FromStr, -}; - -#[cfg(feature = "std")] -use std::path::Path; - -/// PKCS#1 `RSA PRIVATE KEY` document. -/// -/// This type provides storage for [`RsaPrivateKey`] encoded as ASN.1 DER -/// with the invariant that the contained-document is "well-formed", i.e. it -/// will parse successfully according to this crate's parsing rules. -#[derive(Clone)] -#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] -pub struct RsaPrivateKeyDocument(Zeroizing>); - -impl<'a> Document<'a> for RsaPrivateKeyDocument { - type Message = RsaPrivateKey<'a>; - const SENSITIVE: bool = true; -} - -impl DecodeRsaPrivateKey for RsaPrivateKeyDocument { - fn from_pkcs1_der(bytes: &[u8]) -> Result { - Ok(Self::from_der(bytes)?) - } - - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - fn from_pkcs1_pem(s: &str) -> Result { - Ok(Self::from_pem(s)?) - } - - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn read_pkcs1_der_file(path: impl AsRef) -> Result { - Ok(Self::read_der_file(path)?) - } - - #[cfg(all(feature = "pem", feature = "std"))] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn read_pkcs1_pem_file(path: impl AsRef) -> Result { - Ok(Self::read_pem_file(path)?) - } -} - -impl EncodeRsaPrivateKey for RsaPrivateKeyDocument { - fn to_pkcs1_der(&self) -> Result { - Ok(self.clone()) - } - - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - fn to_pkcs1_pem(&self, line_ending: LineEnding) -> Result> { - Ok(Zeroizing::new(self.to_pem(line_ending)?)) - } - - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn write_pkcs1_der_file(&self, path: impl AsRef) -> Result<()> { - Ok(self.write_der_file(path)?) - } - - #[cfg(all(feature = "pem", feature = "std"))] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn write_pkcs1_pem_file(&self, path: impl AsRef, line_ending: LineEnding) -> Result<()> { - Ok(self.write_pem_file(path, line_ending)?) - } -} - -impl AsRef<[u8]> for RsaPrivateKeyDocument { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl TryFrom<&[u8]> for RsaPrivateKeyDocument { - type Error = Error; - - fn try_from(bytes: &[u8]) -> Result { - RsaPrivateKeyDocument::from_pkcs1_der(bytes) - } -} - -impl TryFrom> for RsaPrivateKeyDocument { - type Error = Error; - - fn try_from(private_key: RsaPrivateKey<'_>) -> Result { - RsaPrivateKeyDocument::try_from(&private_key) - } -} - -impl TryFrom<&RsaPrivateKey<'_>> for RsaPrivateKeyDocument { - type Error = Error; - - fn try_from(private_key: &RsaPrivateKey<'_>) -> Result { - Ok(private_key.to_vec()?.try_into()?) - } -} - -impl TryFrom> for RsaPrivateKeyDocument { - type Error = der::Error; - - fn try_from(mut bytes: Vec) -> der::Result { - // Ensure document is well-formed - if let Err(err) = RsaPrivateKey::from_der(bytes.as_slice()) { - bytes.zeroize(); - return Err(err); - } - - Ok(Self(Zeroizing::new(bytes))) - } -} - -impl fmt::Debug for RsaPrivateKeyDocument { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_tuple("RsaPrivateKeyDocument") - .field(&self.decode()) - .finish() - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl FromStr for RsaPrivateKeyDocument { - type Err = Error; - - fn from_str(s: &str) -> Result { - Self::from_pkcs1_pem(s) - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl pem::PemLabel for RsaPrivateKeyDocument { - const PEM_LABEL: &'static str = "RSA PRIVATE KEY"; -} diff --git a/pkcs1/src/public_key.rs b/pkcs1/src/public_key.rs index bfbf5ef4a..e1d28cc27 100644 --- a/pkcs1/src/public_key.rs +++ b/pkcs1/src/public_key.rs @@ -1,16 +1,13 @@ //! PKCS#1 RSA Public Keys. -#[cfg(feature = "alloc")] -pub(crate) mod document; - use crate::{Error, Result}; use der::{asn1::UIntBytes, Decode, Decoder, Encode, Sequence}; #[cfg(feature = "alloc")] -use crate::RsaPublicKeyDocument; +use der::Document; #[cfg(feature = "pem")] -use {crate::LineEnding, alloc::string::String, der::Document}; +use der::pem::PemLabel; /// PKCS#1 RSA Public Keys as defined in [RFC 8017 Appendix 1.1]. /// @@ -33,23 +30,6 @@ pub struct RsaPublicKey<'a> { pub public_exponent: UIntBytes<'a>, } -impl<'a> RsaPublicKey<'a> { - /// Encode this [`RsaPublicKey`] as ASN.1 DER. - #[cfg(feature = "alloc")] - #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] - pub fn to_der(self) -> Result { - self.try_into() - } - - /// Encode this [`RsaPublicKey`] as PEM-encoded ASN.1 DER with the given - /// [`LineEnding`]. - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - pub fn to_pem(self, line_ending: LineEnding) -> Result { - Ok(self.to_der()?.to_pem(line_ending)?) - } -} - impl<'a> Decode<'a> for RsaPublicKey<'a> { fn decode(decoder: &mut Decoder<'a>) -> der::Result { decoder.sequence(|decoder| { @@ -77,3 +57,29 @@ impl<'a> TryFrom<&'a [u8]> for RsaPublicKey<'a> { Ok(Self::from_der(bytes)?) } } + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl TryFrom> for Document { + type Error = Error; + + fn try_from(spki: RsaPublicKey<'_>) -> Result { + Self::try_from(&spki) + } +} + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl TryFrom<&RsaPublicKey<'_>> for Document { + type Error = Error; + + fn try_from(spki: &RsaPublicKey<'_>) -> Result { + Ok(Self::encode_msg(spki)?) + } +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl PemLabel for RsaPublicKey<'_> { + const PEM_LABEL: &'static str = "RSA PUBLIC KEY"; +} diff --git a/pkcs1/src/public_key/document.rs b/pkcs1/src/public_key/document.rs deleted file mode 100644 index be70c617e..000000000 --- a/pkcs1/src/public_key/document.rs +++ /dev/null @@ -1,143 +0,0 @@ -//! PKCS#1 RSA public key document. - -use crate::{DecodeRsaPublicKey, EncodeRsaPublicKey, Error, Result, RsaPublicKey}; -use alloc::vec::Vec; -use core::fmt; -use der::{Decode, Document, Encode}; - -#[cfg(feature = "pem")] -use { - crate::{pem, LineEnding}, - alloc::string::String, - core::str::FromStr, -}; - -#[cfg(feature = "std")] -use std::path::Path; - -/// PKCS#1 `RSA PUBLIC KEY` document. -/// -/// This type provides storage for [`RsaPublicKey`] encoded as ASN.1 -/// DER with the invariant that the contained-document is "well-formed", i.e. -/// it will parse successfully according to this crate's parsing rules. -#[derive(Clone)] -#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] -pub struct RsaPublicKeyDocument(Vec); - -impl<'a> Document<'a> for RsaPublicKeyDocument { - type Message = RsaPublicKey<'a>; - const SENSITIVE: bool = false; -} - -impl DecodeRsaPublicKey for RsaPublicKeyDocument { - fn from_pkcs1_der(bytes: &[u8]) -> Result { - Ok(Self::from_der(bytes)?) - } - - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - fn from_pkcs1_pem(s: &str) -> Result { - Ok(Self::from_pem(s)?) - } - - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn read_pkcs1_der_file(path: impl AsRef) -> Result { - Ok(Self::read_der_file(path)?) - } - - #[cfg(all(feature = "pem", feature = "std"))] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn read_pkcs1_pem_file(path: impl AsRef) -> Result { - Ok(Self::read_pem_file(path)?) - } -} - -impl EncodeRsaPublicKey for RsaPublicKeyDocument { - fn to_pkcs1_der(&self) -> Result { - Ok(self.clone()) - } - - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - fn to_pkcs1_pem(&self, line_ending: LineEnding) -> Result { - Ok(self.to_pem(line_ending)?) - } - - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn write_pkcs1_der_file(&self, path: impl AsRef) -> Result<()> { - Ok(self.write_der_file(path)?) - } - - #[cfg(all(feature = "pem", feature = "std"))] - #[cfg_attr(docsrs, doc(cfg(all(feature = "pem", feature = "std"))))] - fn write_pkcs1_pem_file(&self, path: impl AsRef, line_ending: LineEnding) -> Result<()> { - Ok(self.write_pem_file(path, line_ending)?) - } -} - -impl AsRef<[u8]> for RsaPublicKeyDocument { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl TryFrom<&[u8]> for RsaPublicKeyDocument { - type Error = Error; - - fn try_from(bytes: &[u8]) -> Result { - Ok(Self::from_der(bytes)?) - } -} - -impl TryFrom> for RsaPublicKeyDocument { - type Error = Error; - - fn try_from(public_key: RsaPublicKey<'_>) -> Result { - RsaPublicKeyDocument::try_from(&public_key) - } -} - -impl TryFrom<&RsaPublicKey<'_>> for RsaPublicKeyDocument { - type Error = Error; - - fn try_from(public_key: &RsaPublicKey<'_>) -> Result { - Ok(public_key.to_vec()?.try_into()?) - } -} - -impl TryFrom> for RsaPublicKeyDocument { - type Error = der::Error; - - fn try_from(bytes: Vec) -> der::Result { - // Ensure document is well-formed - RsaPublicKey::from_der(bytes.as_slice())?; - Ok(Self(bytes)) - } -} - -impl fmt::Debug for RsaPublicKeyDocument { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_tuple("RsaPublicKeyDocument") - .field(&self.decode()) - .finish() - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl FromStr for RsaPublicKeyDocument { - type Err = Error; - - fn from_str(s: &str) -> Result { - Self::from_pkcs1_pem(s) - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl pem::PemLabel for RsaPublicKeyDocument { - const PEM_LABEL: &'static str = "RSA PUBLIC KEY"; -} diff --git a/pkcs1/src/traits.rs b/pkcs1/src/traits.rs index 5bbcc3ae5..edf06c106 100644 --- a/pkcs1/src/traits.rs +++ b/pkcs1/src/traits.rs @@ -3,10 +3,17 @@ use crate::Result; #[cfg(feature = "alloc")] -use crate::{RsaPrivateKeyDocument, RsaPublicKeyDocument}; +use { + crate::{RsaPrivateKey, RsaPublicKey}, + der::SecretDocument, +}; #[cfg(feature = "pem")] -use {crate::LineEnding, alloc::string::String}; +use { + crate::LineEnding, + alloc::string::String, + der::{pem::PemLabel, zeroize::Zeroizing}, +}; #[cfg(feature = "pkcs8")] use crate::{ALGORITHM_ID, ALGORITHM_OID}; @@ -15,13 +22,7 @@ use crate::{ALGORITHM_ID, ALGORITHM_OID}; use std::path::Path; #[cfg(all(feature = "alloc", feature = "pkcs8"))] -use der::Document; - -#[cfg(feature = "pem")] -use zeroize::Zeroizing; - -#[cfg(doc)] -use crate::{RsaPrivateKey, RsaPublicKey}; +use der::{Decode, Document}; /// Parse an [`RsaPrivateKey`] from a PKCS#1-encoded document. pub trait DecodeRsaPrivateKey: Sized { @@ -39,7 +40,9 @@ pub trait DecodeRsaPrivateKey: Sized { #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] fn from_pkcs1_pem(s: &str) -> Result { - RsaPrivateKeyDocument::from_pkcs1_pem(s).and_then(|doc| Self::from_pkcs1_der(doc.as_der())) + let (label, doc) = SecretDocument::from_pem(s)?; + RsaPrivateKey::validate_pem_label(label)?; + Self::from_pkcs1_der(doc.as_bytes()) } /// Load PKCS#1 private key from an ASN.1 DER-encoded file on the local @@ -47,8 +50,7 @@ pub trait DecodeRsaPrivateKey: Sized { #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn read_pkcs1_der_file(path: impl AsRef) -> Result { - RsaPrivateKeyDocument::read_pkcs1_der_file(path) - .and_then(|doc| Self::from_pkcs1_der(doc.as_der())) + Self::from_pkcs1_der(SecretDocument::read_der_file(path)?.as_bytes()) } /// Load PKCS#1 private key from a PEM-encoded file on the local filesystem. @@ -56,8 +58,9 @@ pub trait DecodeRsaPrivateKey: Sized { #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn read_pkcs1_pem_file(path: impl AsRef) -> Result { - RsaPrivateKeyDocument::read_pkcs1_pem_file(path) - .and_then(|doc| Self::from_pkcs1_der(doc.as_der())) + let (label, doc) = SecretDocument::read_pem_file(path)?; + RsaPrivateKey::validate_pem_label(&label)?; + Self::from_pkcs1_der(doc.as_bytes()) } } @@ -77,7 +80,9 @@ pub trait DecodeRsaPublicKey: Sized { #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] fn from_pkcs1_pem(s: &str) -> Result { - RsaPublicKeyDocument::from_pkcs1_pem(s).and_then(|doc| Self::from_pkcs1_der(doc.as_der())) + let (label, doc) = Document::from_pem(s)?; + RsaPublicKey::validate_pem_label(label)?; + Self::from_pkcs1_der(doc.as_bytes()) } /// Load [`RsaPublicKey`] from an ASN.1 DER-encoded file on the local @@ -85,8 +90,8 @@ pub trait DecodeRsaPublicKey: Sized { #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn read_pkcs1_der_file(path: impl AsRef) -> Result { - RsaPublicKeyDocument::read_pkcs1_der_file(path) - .and_then(|doc| Self::from_pkcs1_der(doc.as_der())) + let doc = Document::read_der_file(path)?; + Self::from_pkcs1_der(doc.as_bytes()) } /// Load [`RsaPublicKey`] from a PEM-encoded file on the local filesystem. @@ -94,8 +99,9 @@ pub trait DecodeRsaPublicKey: Sized { #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn read_pkcs1_pem_file(path: impl AsRef) -> Result { - RsaPublicKeyDocument::read_pkcs1_pem_file(path) - .and_then(|doc| Self::from_pkcs1_der(doc.as_der())) + let (label, doc) = Document::read_pem_file(path)?; + RsaPublicKey::validate_pem_label(&label)?; + Self::from_pkcs1_der(doc.as_bytes()) } } @@ -103,28 +109,30 @@ pub trait DecodeRsaPublicKey: Sized { #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] pub trait EncodeRsaPrivateKey { - /// Serialize a [`RsaPrivateKeyDocument`] containing a PKCS#1-encoded private key. - fn to_pkcs1_der(&self) -> Result; + /// Serialize a [`SecretDocument`] containing a PKCS#1-encoded private key. + fn to_pkcs1_der(&self) -> Result; /// Serialize this private key as PEM-encoded PKCS#1 with the given [`LineEnding`]. #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] fn to_pkcs1_pem(&self, line_ending: LineEnding) -> Result> { - self.to_pkcs1_der()?.to_pkcs1_pem(line_ending) + let doc = self.to_pkcs1_der()?; + Ok(doc.to_pem(RsaPrivateKey::PEM_LABEL, line_ending)?) } /// Write ASN.1 DER-encoded PKCS#1 private key to the given path. #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn write_pkcs1_der_file(&self, path: impl AsRef) -> Result<()> { - self.to_pkcs1_der()?.write_pkcs1_der_file(path) + Ok(self.to_pkcs1_der()?.write_der_file(path)?) } /// Write ASN.1 DER-encoded PKCS#1 private key to the given path. #[cfg(all(feature = "pem", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(all(feature = "pem", feature = "std"))))] fn write_pkcs1_pem_file(&self, path: impl AsRef, line_ending: LineEnding) -> Result<()> { - self.to_pkcs1_der()?.write_pkcs1_pem_file(path, line_ending) + let doc = self.to_pkcs1_der()?; + Ok(doc.write_pem_file(path, RsaPrivateKey::PEM_LABEL, line_ending)?) } } @@ -132,28 +140,30 @@ pub trait EncodeRsaPrivateKey { #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] pub trait EncodeRsaPublicKey { - /// Serialize a [`RsaPublicKeyDocument`] containing a PKCS#1-encoded public key. - fn to_pkcs1_der(&self) -> Result; + /// Serialize a [`Document`] containing a PKCS#1-encoded public key. + fn to_pkcs1_der(&self) -> Result; /// Serialize this public key as PEM-encoded PKCS#1 with the given line ending. #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] fn to_pkcs1_pem(&self, line_ending: LineEnding) -> Result { - self.to_pkcs1_der()?.to_pkcs1_pem(line_ending) + let doc = self.to_pkcs1_der()?; + Ok(doc.to_pem(RsaPublicKey::PEM_LABEL, line_ending)?) } /// Write ASN.1 DER-encoded public key to the given path. #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn write_pkcs1_der_file(&self, path: impl AsRef) -> Result<()> { - self.to_pkcs1_der()?.write_pkcs1_der_file(path) + Ok(self.to_pkcs1_der()?.write_der_file(path)?) } /// Write ASN.1 DER-encoded public key to the given path. #[cfg(all(feature = "pem", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(all(feature = "pem", feature = "std"))))] fn write_pkcs1_pem_file(&self, path: impl AsRef, line_ending: LineEnding) -> Result<()> { - self.to_pkcs1_der()?.write_pkcs1_pem_file(path, line_ending) + let doc = self.to_pkcs1_der()?; + Ok(doc.write_pem_file(path, RsaPublicKey::PEM_LABEL, line_ending)?) } } @@ -161,13 +171,8 @@ pub trait EncodeRsaPublicKey { #[cfg_attr(docsrs, doc(cfg(feature = "pkcs8")))] impl DecodeRsaPrivateKey for T { fn from_pkcs1_der(private_key: &[u8]) -> Result { - let algorithm = pkcs8::AlgorithmIdentifier { - oid: ALGORITHM_OID, - parameters: Some(der::asn1::Null.into()), - }; - Ok(Self::try_from(pkcs8::PrivateKeyInfo { - algorithm, + algorithm: ALGORITHM_ID, private_key, public_key: None, })?) @@ -188,19 +193,21 @@ impl DecodeRsaPublicKey for T { #[cfg(all(feature = "alloc", feature = "pkcs8"))] #[cfg_attr(docsrs, doc(all(feature = "alloc", feature = "pkcs8")))] impl EncodeRsaPrivateKey for T { - fn to_pkcs1_der(&self) -> Result { - let doc = self.to_pkcs8_der()?; - Ok(RsaPrivateKeyDocument::from_der(doc.decode().private_key)?) + fn to_pkcs1_der(&self) -> Result { + let pkcs8_doc = self.to_pkcs8_der()?; + let pkcs8_key = pkcs8::PrivateKeyInfo::from_der(pkcs8_doc.as_bytes())?; + pkcs8_key.algorithm.assert_algorithm_oid(ALGORITHM_OID)?; + RsaPrivateKey::from_der(pkcs8_key.private_key)?.try_into() } } #[cfg(all(feature = "alloc", feature = "pkcs8"))] #[cfg_attr(docsrs, doc(all(feature = "alloc", feature = "pkcs8")))] impl EncodeRsaPublicKey for T { - fn to_pkcs1_der(&self) -> Result { + fn to_pkcs1_der(&self) -> Result { let doc = self.to_public_key_der()?; - Ok(RsaPublicKeyDocument::from_der( - doc.decode().subject_public_key, - )?) + let spki = pkcs8::SubjectPublicKeyInfo::from_der(doc.as_bytes())?; + spki.algorithm.assert_algorithm_oid(ALGORITHM_OID)?; + RsaPublicKey::from_der(spki.subject_public_key)?.try_into() } } diff --git a/pkcs1/tests/private_key.rs b/pkcs1/tests/private_key.rs index 5ca1e9d90..f650534da 100644 --- a/pkcs1/tests/private_key.rs +++ b/pkcs1/tests/private_key.rs @@ -3,9 +3,6 @@ use hex_literal::hex; use pkcs1::{RsaPrivateKey, Version}; -#[cfg(feature = "pem")] -use pkcs1::{der::Document, RsaPrivateKeyDocument}; - /// RSA-2048 PKCS#1 private key encoded as ASN.1 DER. /// /// Note: this key is extracted from the corresponding `rsa2048-priv.der` @@ -18,14 +15,6 @@ const RSA_4096_DER_EXAMPLE: &[u8] = include_bytes!("examples/rsa4096-priv.der"); /// RSA-2048 PKCS#1 private key with 3 primes encoded as ASN.1 DER const RSA_2048_MULTI_PRIME_DER_EXAMPLE: &[u8] = include_bytes!("examples/rsa2048-priv-3prime.der"); -/// RSA-2048 PKCS#1 private key encoded as PEM -#[cfg(feature = "pem")] -const RSA_2048_PEM_EXAMPLE: &str = include_str!("examples/rsa2048-priv.pem"); - -/// RSA-4096 PKCS#1 private key encoded as PEM -#[cfg(feature = "pem")] -const RSA_4096_PEM_EXAMPLE: &str = include_str!("examples/rsa4096-priv.pem"); - #[test] fn decode_rsa2048_der() { let key = RsaPrivateKey::try_from(RSA_2048_DER_EXAMPLE).unwrap(); @@ -93,28 +82,6 @@ fn decode_rsa2048_multi_prime_der() { assert_eq!(other_prime_infos[0].coefficient.as_bytes(), hex!("39EA226CABFB317E41A5593B9168D1A0124993B45D9CD14A22BD1557CDCB43D28024AC26ED2C8530B53E9B93A878F428807C5282EBB811399F913017CDF2149013D80CDF73F609D6C692475EB7A123D0E93E6A60FC")); } -#[cfg(feature = "pem")] -#[test] -fn decode_rsa_2048_pem() { - let pkcs1_doc: RsaPrivateKeyDocument = RSA_2048_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(pkcs1_doc.as_ref(), RSA_2048_DER_EXAMPLE); - - // Ensure `RsaPrivateKeyDocument` parses successfully - let pk = RsaPrivateKey::try_from(RSA_2048_DER_EXAMPLE).unwrap(); - assert_eq!(pkcs1_doc.decode().modulus.as_bytes(), pk.modulus.as_bytes()); -} - -#[cfg(feature = "pem")] -#[test] -fn decode_rsa_4096_pem() { - let pkcs1_doc: RsaPrivateKeyDocument = RSA_4096_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(pkcs1_doc.as_ref(), RSA_4096_DER_EXAMPLE); - - // Ensure `RsaPrivateKeyDocument` parses successfully - let pk = RsaPrivateKey::try_from(RSA_4096_DER_EXAMPLE).unwrap(); - assert_eq!(pkcs1_doc.decode().modulus.as_bytes(), pk.modulus.as_bytes()); -} - #[test] fn private_key_to_public_key() { let private_key = RsaPrivateKey::try_from(RSA_2048_DER_EXAMPLE).unwrap(); diff --git a/pkcs1/tests/public_key.rs b/pkcs1/tests/public_key.rs index 6c329dd73..cde6e41ff 100644 --- a/pkcs1/tests/public_key.rs +++ b/pkcs1/tests/public_key.rs @@ -3,9 +3,6 @@ use hex_literal::hex; use pkcs1::RsaPublicKey; -#[cfg(feature = "pem")] -use pkcs1::{der::Document, RsaPublicKeyDocument}; - /// RSA-2048 PKCS#1 public key encoded as ASN.1 DER. /// /// Note: this key is extracted from the corresponding `rsa2048-priv.der` @@ -15,13 +12,13 @@ const RSA_2048_DER_EXAMPLE: &[u8] = include_bytes!("examples/rsa2048-pub.der"); /// RSA-4096 PKCS#1 public key encoded as ASN.1 DER const RSA_4096_DER_EXAMPLE: &[u8] = include_bytes!("examples/rsa4096-pub.der"); -/// RSA-2048 PKCS#1 public key encoded as PEM -#[cfg(feature = "pem")] -const RSA_2048_PEM_EXAMPLE: &str = include_str!("examples/rsa2048-pub.pem"); - -/// RSA-4096 PKCS#1 public key encoded as PEM -#[cfg(feature = "pem")] -const RSA_4096_PEM_EXAMPLE: &str = include_str!("examples/rsa4096-pub.pem"); +// /// RSA-2048 PKCS#1 public key encoded as PEM +// #[cfg(feature = "pem")] +// const RSA_2048_PEM_EXAMPLE: &str = include_str!("examples/rsa2048-pub.pem"); +// +// /// RSA-4096 PKCS#1 public key encoded as PEM +// #[cfg(feature = "pem")] +// const RSA_4096_PEM_EXAMPLE: &str = include_str!("examples/rsa4096-pub.pem"); #[test] fn decode_rsa2048_der() { @@ -43,24 +40,25 @@ fn decode_rsa4096_der() { assert_eq!(key.public_exponent.as_bytes(), hex!("010001")); } -#[test] -#[cfg(feature = "pem")] -fn decode_rsa_2048_pem() { - let pkcs1_doc: RsaPublicKeyDocument = RSA_2048_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(pkcs1_doc.as_ref(), RSA_2048_DER_EXAMPLE); - - // Ensure `RsaPublicKeyDocument` parses successfully - let pk = RsaPublicKey::try_from(RSA_2048_DER_EXAMPLE).unwrap(); - assert_eq!(pkcs1_doc.decode().modulus.as_bytes(), pk.modulus.as_bytes()); -} - -#[test] -#[cfg(feature = "pem")] -fn decode_rsa_4096_pem() { - let pkcs1_doc: RsaPublicKeyDocument = RSA_4096_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(pkcs1_doc.as_ref(), RSA_4096_DER_EXAMPLE); - - // Ensure `RsaPublicKeyDocument` parses successfully - let pk = RsaPublicKey::try_from(RSA_4096_DER_EXAMPLE).unwrap(); - assert_eq!(pkcs1_doc.decode().modulus.as_bytes(), pk.modulus.as_bytes()); -} +// TODO(tarcieri): test trait-based PEM decoding +// #[test] +// #[cfg(feature = "pem")] +// fn decode_rsa_2048_pem() { +// let pkcs1_doc: Document = RSA_2048_PEM_EXAMPLE.parse().unwrap(); +// assert_eq!(pkcs1_doc.as_ref(), RSA_2048_DER_EXAMPLE); +// +// // Ensure `Document` parses successfully +// let pk = RsaPublicKey::try_from(RSA_2048_DER_EXAMPLE).unwrap(); +// assert_eq!(pkcs1_doc.decode().modulus.as_bytes(), pk.modulus.as_bytes()); +// } +// +// #[test] +// #[cfg(feature = "pem")] +// fn decode_rsa_4096_pem() { +// let pkcs1_doc: Document = RSA_4096_PEM_EXAMPLE.parse().unwrap(); +// assert_eq!(pkcs1_doc.as_ref(), RSA_4096_DER_EXAMPLE); +// +// // Ensure `Document` parses successfully +// let pk = RsaPublicKey::try_from(RSA_4096_DER_EXAMPLE).unwrap(); +// assert_eq!(pkcs1_doc.decode().modulus.as_bytes(), pk.modulus.as_bytes()); +// } diff --git a/pkcs1/tests/traits.rs b/pkcs1/tests/traits.rs new file mode 100644 index 000000000..65bef2660 --- /dev/null +++ b/pkcs1/tests/traits.rs @@ -0,0 +1,100 @@ +//! Tests for PKCS#1 encoding/decoding traits. + +#![cfg(any(feature = "pem", feature = "std"))] + +use der::SecretDocument; +use pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey, Result}; + +#[cfg(feature = "pem")] +use pkcs1::der::pem::LineEnding; + +#[cfg(feature = "std")] +use tempfile::tempdir; + +#[cfg(all(feature = "pem", feature = "std"))] +use std::fs; + +/// PKCS#1 `RsaPrivateKey` encoded as ASN.1 DER +const RSA_2048_PRIV_DER_EXAMPLE: &[u8] = include_bytes!("examples/rsa2048-priv.der"); + +/// PKCS#1 `RsaPrivateKey` encoded as PEM +#[cfg(feature = "pem")] +const RSA_2048_PRIV_PEM_EXAMPLE: &str = include_str!("examples/rsa2048-priv.pem"); + +/// Mock RSA private key type for testing trait impls against. +pub struct MockPrivateKey(Vec); + +impl AsRef<[u8]> for MockPrivateKey { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl DecodeRsaPrivateKey for MockPrivateKey { + fn from_pkcs1_der(bytes: &[u8]) -> Result { + Ok(MockPrivateKey(bytes.to_vec())) + } +} + +impl EncodeRsaPrivateKey for MockPrivateKey { + fn to_pkcs1_der(&self) -> Result { + Ok(SecretDocument::try_from(self.as_ref())?) + } +} + +#[cfg(feature = "pem")] +#[test] +fn from_pkcs1_pem() { + let key = MockPrivateKey::from_pkcs1_pem(RSA_2048_PRIV_PEM_EXAMPLE).unwrap(); + assert_eq!(key.as_ref(), RSA_2048_PRIV_DER_EXAMPLE); +} + +#[cfg(feature = "std")] +#[test] +fn read_pkcs1_der_file() { + let key = MockPrivateKey::read_pkcs1_der_file("tests/examples/rsa2048-priv.der").unwrap(); + assert_eq!(key.as_ref(), RSA_2048_PRIV_DER_EXAMPLE); +} + +#[cfg(all(feature = "pem", feature = "std"))] +#[test] +fn read_pkcs1_pem_file() { + let key = MockPrivateKey::read_pkcs1_pem_file("tests/examples/rsa2048-priv.pem").unwrap(); + assert_eq!(key.as_ref(), RSA_2048_PRIV_DER_EXAMPLE); +} + +#[cfg(feature = "pem")] +#[test] +fn to_pkcs1_pem() { + let pem = MockPrivateKey(RSA_2048_PRIV_DER_EXAMPLE.to_vec()) + .to_pkcs1_pem(LineEnding::LF) + .unwrap(); + + assert_eq!(&*pem, RSA_2048_PRIV_PEM_EXAMPLE); +} + +#[cfg(feature = "std")] +#[test] +fn write_pkcs1_der_file() { + let dir = tempdir().unwrap(); + let path = dir.path().join("example.der"); + MockPrivateKey(RSA_2048_PRIV_DER_EXAMPLE.to_vec()) + .write_pkcs1_der_file(&path) + .unwrap(); + + let key = MockPrivateKey::read_pkcs1_der_file(&path).unwrap(); + assert_eq!(key.as_ref(), RSA_2048_PRIV_DER_EXAMPLE); +} + +#[cfg(all(feature = "pem", feature = "std"))] +#[test] +fn write_pkcs1_pem_file() { + let dir = tempdir().unwrap(); + let path = dir.path().join("example.pem"); + MockPrivateKey(RSA_2048_PRIV_DER_EXAMPLE.to_vec()) + .write_pkcs1_pem_file(&path, LineEnding::LF) + .unwrap(); + + let pem = fs::read_to_string(path).unwrap(); + assert_eq!(&pem, RSA_2048_PRIV_PEM_EXAMPLE); +} diff --git a/pkcs8/Cargo.toml b/pkcs8/Cargo.toml index b92480d72..3b4515251 100644 --- a/pkcs8/Cargo.toml +++ b/pkcs8/Cargo.toml @@ -23,18 +23,18 @@ spki = { version = "=0.6.0-pre.2", path = "../spki" } rand_core = { version = "0.6", optional = true, default-features = false } pkcs5 = { version = "=0.5.0-pre.1", optional = true, path = "../pkcs5" } subtle = { version = "2", optional = true, default-features = false } -zeroize = { version = "1", optional = true, default-features = false, features = ["alloc"] } [dev-dependencies] hex-literal = "0.3" +tempfile = "3" [features] -alloc = ["der/alloc", "spki/alloc", "zeroize"] +alloc = ["der/alloc", "der/zeroize", "spki/alloc"] 3des = ["encryption", "pkcs5/3des"] des-insecure = ["encryption", "pkcs5/des-insecure"] encryption = ["alloc", "pkcs5/alloc", "pkcs5/pbes2", "rand_core"] +getrandom = ["rand_core/getrandom"] pem = ["alloc", "der/pem", "spki/pem"] -rand = ["std", "rand_core/std"] sha1 = ["encryption", "pkcs5/sha1"] std = ["alloc", "der/std", "spki/std"] diff --git a/pkcs8/src/document.rs b/pkcs8/src/document.rs deleted file mode 100644 index ad4acaac1..000000000 --- a/pkcs8/src/document.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Serialized DER-encoded documents stored in heap-backed buffers. -// TODO(tarcieri): heapless support? - -#[cfg(feature = "pkcs5")] -pub(crate) mod encrypted_private_key; -pub(crate) mod private_key; diff --git a/pkcs8/src/document/encrypted_private_key.rs b/pkcs8/src/document/encrypted_private_key.rs deleted file mode 100644 index 0d81b5d6a..000000000 --- a/pkcs8/src/document/encrypted_private_key.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! PKCS#8 encrypted private key document. - -use crate::{EncryptedPrivateKeyInfo, Error, Result}; -use alloc::{borrow::ToOwned, vec::Vec}; -use core::fmt; -use der::{Decode, Document, Encode}; -use zeroize::{Zeroize, Zeroizing}; - -#[cfg(feature = "encryption")] -use crate::PrivateKeyDocument; - -#[cfg(feature = "pem")] -use {core::str::FromStr, der::pem}; - -/// Encrypted PKCS#8 private key document. -/// -/// This type provides heap-backed storage for [`EncryptedPrivateKeyInfo`] -/// encoded as ASN.1 DER with the invariant that the contained-document is -/// "well-formed", i.e. it will parse successfully according to this crate's -/// parsing rules. -#[derive(Clone, Eq, PartialEq)] -#[cfg_attr(docsrs, doc(cfg(all(feature = "alloc", feature = "pkcs5"))))] -pub struct EncryptedPrivateKeyDocument(Zeroizing>); - -impl<'a> Document<'a> for EncryptedPrivateKeyDocument { - type Message = EncryptedPrivateKeyInfo<'a>; - const SENSITIVE: bool = true; -} - -impl EncryptedPrivateKeyDocument { - /// Attempt to decrypt this encrypted private key using the provided - /// password to derive an encryption key. - #[cfg(feature = "encryption")] - #[cfg_attr(docsrs, doc(cfg(feature = "encryption")))] - pub fn decrypt(&self, password: impl AsRef<[u8]>) -> Result { - self.encrypted_private_key_info().decrypt(password) - } - - /// Parse the [`EncryptedPrivateKeyInfo`] contained in this [`EncryptedPrivateKeyDocument`]. - pub fn encrypted_private_key_info(&self) -> EncryptedPrivateKeyInfo<'_> { - EncryptedPrivateKeyInfo::try_from(self.0.as_ref()) - .expect("malformed EncryptedPrivateKeyDocument") - } -} - -impl AsRef<[u8]> for EncryptedPrivateKeyDocument { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl TryFrom> for EncryptedPrivateKeyDocument { - type Error = Error; - - fn try_from(key: EncryptedPrivateKeyInfo<'_>) -> Result { - EncryptedPrivateKeyDocument::try_from(&key) - } -} - -impl TryFrom<&EncryptedPrivateKeyInfo<'_>> for EncryptedPrivateKeyDocument { - type Error = Error; - - fn try_from(key: &EncryptedPrivateKeyInfo<'_>) -> Result { - Ok(key.to_vec()?.try_into()?) - } -} - -impl TryFrom<&[u8]> for EncryptedPrivateKeyDocument { - type Error = Error; - - fn try_from(bytes: &[u8]) -> Result { - // Ensure document is well-formed - EncryptedPrivateKeyInfo::try_from(bytes)?; - Ok(Self(Zeroizing::new(bytes.to_owned()))) - } -} - -impl TryFrom> for EncryptedPrivateKeyDocument { - type Error = der::Error; - - fn try_from(mut bytes: Vec) -> der::Result { - // Ensure document is well-formed - if let Err(err) = EncryptedPrivateKeyInfo::from_der(bytes.as_slice()) { - bytes.zeroize(); - return Err(err); - } - - Ok(Self(Zeroizing::new(bytes))) - } -} - -impl fmt::Debug for EncryptedPrivateKeyDocument { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_tuple("EncryptedPrivateKeyDocument") - .field(&self.encrypted_private_key_info()) - .finish() - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl FromStr for EncryptedPrivateKeyDocument { - type Err = Error; - - fn from_str(s: &str) -> Result { - Ok(Self::from_pem(s)?) - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl pem::PemLabel for EncryptedPrivateKeyDocument { - const PEM_LABEL: &'static str = "ENCRYPTED PRIVATE KEY"; -} diff --git a/pkcs8/src/document/private_key.rs b/pkcs8/src/document/private_key.rs deleted file mode 100644 index 01df9b925..000000000 --- a/pkcs8/src/document/private_key.rs +++ /dev/null @@ -1,202 +0,0 @@ -//! PKCS#8 private key document. - -use crate::{DecodePrivateKey, EncodePrivateKey, Error, PrivateKeyInfo, Result}; -use alloc::vec::Vec; -use core::fmt; -use der::{Decode, Document}; -use zeroize::{Zeroize, Zeroizing}; - -#[cfg(feature = "encryption")] -use { - crate::{EncryptedPrivateKeyDocument, EncryptedPrivateKeyInfo}, - pkcs5::pbes2, - rand_core::{CryptoRng, RngCore}, -}; - -#[cfg(feature = "pem")] -use { - alloc::string::String, - core::str::FromStr, - der::pem::{self, LineEnding}, -}; - -#[cfg(feature = "std")] -use std::path::Path; - -/// PKCS#8 private key document. -/// -/// This type provides storage for [`PrivateKeyInfo`] encoded as ASN.1 DER -/// with the invariant that the contained-document is "well-formed", i.e. it -/// will parse successfully according to this crate's parsing rules. -#[derive(Clone)] -#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] -pub struct PrivateKeyDocument(Zeroizing>); - -impl<'a> Document<'a> for PrivateKeyDocument { - type Message = PrivateKeyInfo<'a>; - const SENSITIVE: bool = true; -} - -impl PrivateKeyDocument { - /// Encrypt this private key using a symmetric encryption key derived - /// from the provided password. - /// - /// Uses the following algorithms for encryption: - /// - PBKDF: scrypt with default parameters: - /// - logâ‚‚(N): 15 - /// - r: 8 - /// - p: 1 - /// - Cipher: AES-256-CBC (best available option for PKCS#5 encryption) - #[cfg(feature = "encryption")] - #[cfg_attr(docsrs, doc(cfg(feature = "encryption")))] - pub fn encrypt( - &self, - mut rng: impl CryptoRng + RngCore, - password: impl AsRef<[u8]>, - ) -> Result { - let mut salt = [0u8; 16]; - rng.fill_bytes(&mut salt); - - let mut iv = [0u8; 16]; - rng.fill_bytes(&mut iv); - - let pbes2_params = pbes2::Parameters::scrypt_aes256cbc(Default::default(), &salt, &iv)?; - self.encrypt_with_params(pbes2_params, password) - } - - /// Encrypt this private key using a symmetric encryption key derived - /// from the provided password and [`pbes2::Parameters`]. - #[cfg(feature = "encryption")] - #[cfg_attr(docsrs, doc(cfg(feature = "encryption")))] - pub fn encrypt_with_params( - &self, - pbes2_params: pbes2::Parameters<'_>, - password: impl AsRef<[u8]>, - ) -> Result { - let encrypted_data = pbes2_params.encrypt(password, self.as_ref())?; - - EncryptedPrivateKeyInfo { - encryption_algorithm: pbes2_params.into(), - encrypted_data: &encrypted_data, - } - .try_into() - } -} - -impl DecodePrivateKey for PrivateKeyDocument { - fn from_pkcs8_der(bytes: &[u8]) -> Result { - Ok(Self::from_der(bytes)?) - } - - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - fn from_pkcs8_pem(s: &str) -> Result { - Ok(Self::from_pem(s)?) - } - - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn read_pkcs8_der_file(path: impl AsRef) -> Result { - Ok(Self::read_der_file(path)?) - } - - #[cfg(all(feature = "pem", feature = "std"))] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn read_pkcs8_pem_file(path: impl AsRef) -> Result { - Ok(Self::read_pem_file(path)?) - } -} - -impl EncodePrivateKey for PrivateKeyDocument { - fn to_pkcs8_der(&self) -> Result { - Ok(self.clone()) - } - - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - fn to_pkcs8_pem(&self, line_ending: LineEnding) -> Result> { - Ok(Zeroizing::new(self.to_pem(line_ending)?)) - } - - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn write_pkcs8_der_file(&self, path: impl AsRef) -> Result<()> { - Ok(self.write_der_file(path)?) - } - - #[cfg(all(feature = "pem", feature = "std"))] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn write_pkcs8_pem_file(&self, path: impl AsRef, line_ending: LineEnding) -> Result<()> { - Ok(self.write_pem_file(path, line_ending)?) - } -} - -impl AsRef<[u8]> for PrivateKeyDocument { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl TryFrom> for PrivateKeyDocument { - type Error = Error; - - fn try_from(private_key_info: PrivateKeyInfo<'_>) -> Result { - Self::try_from(&private_key_info) - } -} - -impl TryFrom<&PrivateKeyInfo<'_>> for PrivateKeyDocument { - type Error = Error; - - fn try_from(private_key_info: &PrivateKeyInfo<'_>) -> Result { - Ok(Self::from_msg(private_key_info)?) - } -} - -impl TryFrom<&[u8]> for PrivateKeyDocument { - type Error = Error; - - fn try_from(bytes: &[u8]) -> Result { - PrivateKeyDocument::from_pkcs8_der(bytes) - } -} - -impl TryFrom> for PrivateKeyDocument { - type Error = der::Error; - - fn try_from(mut bytes: Vec) -> der::Result { - // Ensure document is well-formed - if let Err(err) = PrivateKeyInfo::from_der(bytes.as_slice()) { - bytes.zeroize(); - return Err(err); - } - - Ok(Self(Zeroizing::new(bytes))) - } -} - -impl fmt::Debug for PrivateKeyDocument { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_tuple("PrivateKeyDocument") - .field(&self.decode()) - .finish() - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl FromStr for PrivateKeyDocument { - type Err = Error; - - fn from_str(s: &str) -> Result { - Self::from_pkcs8_pem(s) - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl pem::PemLabel for PrivateKeyDocument { - const PEM_LABEL: &'static str = "PRIVATE KEY"; -} diff --git a/pkcs8/src/encrypted_private_key_info.rs b/pkcs8/src/encrypted_private_key_info.rs index 2937c28a9..08babce09 100644 --- a/pkcs8/src/encrypted_private_key_info.rs +++ b/pkcs8/src/encrypted_private_key_info.rs @@ -6,10 +6,16 @@ use der::{asn1::OctetString, Decode, Decoder, Encode, Sequence}; use pkcs5::EncryptionScheme; #[cfg(feature = "alloc")] -use crate::{EncryptedPrivateKeyDocument, PrivateKeyDocument}; +use der::SecretDocument; + +#[cfg(feature = "encryption")] +use { + pkcs5::pbes2, + rand_core::{CryptoRng, RngCore}, +}; #[cfg(feature = "pem")] -use {crate::LineEnding, alloc::string::String, der::Document, zeroize::Zeroizing}; +use der::pem::PemLabel; /// PKCS#8 `EncryptedPrivateKeyInfo`. /// @@ -46,28 +52,48 @@ impl<'a> EncryptedPrivateKeyInfo<'a> { /// password to derive an encryption key. #[cfg(feature = "encryption")] #[cfg_attr(docsrs, doc(cfg(feature = "encryption")))] - pub fn decrypt(&self, password: impl AsRef<[u8]>) -> Result { + pub fn decrypt(&self, password: impl AsRef<[u8]>) -> Result { Ok(self .encryption_algorithm .decrypt(password, self.encrypted_data)? .try_into()?) } - /// Encode this [`EncryptedPrivateKeyInfo`] as ASN.1 DER. - #[cfg(feature = "alloc")] - #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] - pub fn to_der(&self) -> Result { - self.try_into() + /// Encrypt the given ASN.1 DER document using a symmetric encryption key + /// derived from the provided password. + #[cfg(feature = "encryption")] + #[cfg_attr(docsrs, doc(cfg(feature = "encryption")))] + pub(crate) fn encrypt( + mut rng: impl CryptoRng + RngCore, + password: impl AsRef<[u8]>, + doc: &[u8], + ) -> Result { + let mut salt = [0u8; 16]; + rng.fill_bytes(&mut salt); + + let mut iv = [0u8; 16]; + rng.fill_bytes(&mut iv); + + let pbes2_params = pbes2::Parameters::scrypt_aes256cbc(Default::default(), &salt, &iv)?; + EncryptedPrivateKeyInfo::encrypt_with(pbes2_params, password, doc) } - /// Encode this [`EncryptedPrivateKeyInfo`] as PEM-encoded ASN.1 DER with - /// the given [`LineEnding`]. - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - pub fn to_pem(&self, line_ending: LineEnding) -> Result> { - Ok(Zeroizing::new( - EncryptedPrivateKeyDocument::try_from(self)?.to_pem(line_ending)?, - )) + /// Encrypt this private key using a symmetric encryption key derived + /// from the provided password and [`pbes2::Parameters`]. + #[cfg(feature = "encryption")] + #[cfg_attr(docsrs, doc(cfg(feature = "encryption")))] + pub(crate) fn encrypt_with( + pbes2_params: pbes2::Parameters<'a>, + password: impl AsRef<[u8]>, + doc: &[u8], + ) -> Result { + let encrypted_data = pbes2_params.encrypt(password, doc)?; + + EncryptedPrivateKeyInfo { + encryption_algorithm: pbes2_params.into(), + encrypted_data: &encrypted_data, + } + .try_into() } } @@ -109,3 +135,29 @@ impl<'a> fmt::Debug for EncryptedPrivateKeyInfo<'a> { .finish_non_exhaustive() } } + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(all(feature = "alloc", feature = "pkcs5"))))] +impl TryFrom> for SecretDocument { + type Error = Error; + + fn try_from(encrypted_private_key: EncryptedPrivateKeyInfo<'_>) -> Result { + SecretDocument::try_from(&encrypted_private_key) + } +} + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(all(feature = "alloc", feature = "pkcs5"))))] +impl TryFrom<&EncryptedPrivateKeyInfo<'_>> for SecretDocument { + type Error = Error; + + fn try_from(encrypted_private_key: &EncryptedPrivateKeyInfo<'_>) -> Result { + Ok(Self::encode_msg(encrypted_private_key)?) + } +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl PemLabel for EncryptedPrivateKeyInfo<'_> { + const PEM_LABEL: &'static str = "ENCRYPTED PRIVATE KEY"; +} diff --git a/pkcs8/src/error.rs b/pkcs8/src/error.rs index 11498f43a..bc4c2eafe 100644 --- a/pkcs8/src/error.rs +++ b/pkcs8/src/error.rs @@ -2,6 +2,9 @@ use core::fmt; +#[cfg(feature = "pem")] +use der::pem; + /// Result type pub type Result = core::result::Result; @@ -59,6 +62,13 @@ impl From for Error { } } +#[cfg(feature = "pem")] +impl From for Error { + fn from(err: pem::Error) -> Error { + der::Error::from(err).into() + } +} + #[cfg(feature = "pkcs5")] impl From for Error { fn from(err: pkcs5::Error) -> Error { diff --git a/pkcs8/src/lib.rs b/pkcs8/src/lib.rs index c1f7cee26..01ecbb6d0 100644 --- a/pkcs8/src/lib.rs +++ b/pkcs8/src/lib.rs @@ -22,21 +22,12 @@ //! - [`SubjectPublicKeyInfo`]: algorithm identifier and data representing a public key //! (re-exported from the [`spki`] crate) //! -//! When the `alloc` feature is enabled, the following additional types are -//! available which provide more convenient decoding/encoding support: -//! -//! - [`EncryptedPrivateKeyDocument`]: (with `pkcs5` feature) heap-backed encrypted key. -//! - [`PrivateKeyDocument`]: heap-backed storage for serialized [`PrivateKeyInfo`]. -//! - [`PublicKeyDocument`]: heap-backed storage for serialized [`SubjectPublicKeyInfo`]. -//! //! When the `pem` feature is enabled, it also supports decoding/encoding //! documents from "PEM encoding" format as defined in RFC 7468. //! //! ## Encrypted Private Key Support //! [`EncryptedPrivateKeyInfo`] supports decoding/encoding encrypted PKCS#8 -//! private keys and is gated under the `pkcs5` feature. The corresponding -//! [`EncryptedPrivateKeyDocument`] type provides heap-backed storage -//! (`alloc` feature required). +//! private keys and is gated under the `pkcs5` feature. //! //! When the `encryption` feature of this crate is enabled, it provides //! [`EncryptedPrivateKeyInfo::decrypt`] and [`PrivateKeyInfo::encrypt`] @@ -73,7 +64,7 @@ //! [PKCS#5v2 Password Based Encryption Scheme 2 (RFC 8018)]: https://tools.ietf.org/html/rfc8018#section-6.2 //! [scrypt]: https://en.wikipedia.org/wiki/Scrypt -#[cfg(feature = "alloc")] +#[cfg(feature = "pem")] extern crate alloc; #[cfg(feature = "std")] extern crate std; @@ -83,9 +74,6 @@ mod private_key_info; mod traits; mod version; -#[cfg(feature = "alloc")] -mod document; - #[cfg(feature = "pkcs5")] pub(crate) mod encrypted_private_key_info; @@ -100,8 +88,9 @@ pub use spki::{self, AlgorithmIdentifier, DecodePublicKey, SubjectPublicKeyInfo} #[cfg(feature = "alloc")] pub use { - crate::{document::private_key::PrivateKeyDocument, traits::EncodePrivateKey}, - spki::{EncodePublicKey, PublicKeyDocument}, + crate::traits::EncodePrivateKey, + der::{Document, SecretDocument}, + spki::EncodePublicKey, }; #[cfg(feature = "pem")] @@ -109,10 +98,7 @@ pub use { pub use der::pem::LineEnding; #[cfg(feature = "pkcs5")] -pub use {crate::encrypted_private_key_info::EncryptedPrivateKeyInfo, pkcs5}; +pub use {encrypted_private_key_info::EncryptedPrivateKeyInfo, pkcs5}; #[cfg(feature = "rand_core")] pub use rand_core; - -#[cfg(all(feature = "alloc", feature = "pkcs5"))] -pub use crate::document::encrypted_private_key::EncryptedPrivateKeyDocument; diff --git a/pkcs8/src/private_key_info.rs b/pkcs8/src/private_key_info.rs index fd9eef9e7..458c722e6 100644 --- a/pkcs8/src/private_key_info.rs +++ b/pkcs8/src/private_key_info.rs @@ -8,26 +8,24 @@ use der::{ }; #[cfg(feature = "alloc")] -use crate::PrivateKeyDocument; +use der::SecretDocument; #[cfg(feature = "encryption")] use { - crate::EncryptedPrivateKeyDocument, + crate::EncryptedPrivateKeyInfo, + der::zeroize::Zeroizing, + pkcs5::pbes2, rand_core::{CryptoRng, RngCore}, }; #[cfg(feature = "pem")] -use { - crate::{EncodePrivateKey, LineEnding}, - alloc::string::String, - zeroize::Zeroizing, -}; +use der::pem::PemLabel; #[cfg(feature = "subtle")] use subtle::{Choice, ConstantTimeEq}; /// Context-specific tag number for the public key. -const PUBLIC_KEY_TAG: TagNumber = TagNumber::new(1); +const PUBLIC_KEY_TAG: TagNumber = TagNumber::N1; /// PKCS#8 `PrivateKeyInfo`. /// @@ -129,30 +127,34 @@ impl<'a> PrivateKeyInfo<'a> { /// Encrypt this private key using a symmetric encryption key derived /// from the provided password. /// - /// See [`PrivateKeyDocument::encrypt`] for more information. + /// Uses the following algorithms for encryption: + /// - PBKDF: scrypt with default parameters: + /// - logâ‚‚(N): 15 + /// - r: 8 + /// - p: 1 + /// - Cipher: AES-256-CBC (best available option for PKCS#5 encryption) #[cfg(feature = "encryption")] #[cfg_attr(docsrs, doc(cfg(feature = "encryption")))] pub fn encrypt( &self, rng: impl CryptoRng + RngCore, password: impl AsRef<[u8]>, - ) -> Result { - self.to_der()?.encrypt(rng, password) - } - - /// Encode this [`PrivateKeyInfo`] as ASN.1 DER. - #[cfg(feature = "alloc")] - #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] - pub fn to_der(&self) -> Result { - self.try_into() + ) -> Result { + let der = Zeroizing::new(self.to_vec()?); + EncryptedPrivateKeyInfo::encrypt(rng, password, der.as_ref()) } - /// Encode this [`PrivateKeyInfo`] as PEM-encoded ASN.1 DER with the given - /// [`LineEnding`]. - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - pub fn to_pem(&self, line_ending: LineEnding) -> Result> { - self.to_der()?.to_pkcs8_pem(line_ending) + /// Encrypt this private key using a symmetric encryption key derived + /// from the provided password and [`pbes2::Parameters`]. + #[cfg(feature = "encryption")] + #[cfg_attr(docsrs, doc(cfg(feature = "encryption")))] + pub fn encrypt_with_params( + &self, + pbes2_params: pbes2::Parameters<'_>, + password: impl AsRef<[u8]>, + ) -> Result { + let der = Zeroizing::new(self.to_vec()?); + EncryptedPrivateKeyInfo::encrypt_with(pbes2_params, password, der.as_ref()) } } @@ -233,6 +235,32 @@ impl<'a> fmt::Debug for PrivateKeyInfo<'a> { } } +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl TryFrom> for SecretDocument { + type Error = Error; + + fn try_from(private_key: PrivateKeyInfo<'_>) -> Result { + SecretDocument::try_from(&private_key) + } +} + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl TryFrom<&PrivateKeyInfo<'_>> for SecretDocument { + type Error = Error; + + fn try_from(private_key: &PrivateKeyInfo<'_>) -> Result { + Ok(Self::encode_msg(private_key)?) + } +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl PemLabel for PrivateKeyInfo<'_> { + const PEM_LABEL: &'static str = "PRIVATE KEY"; +} + #[cfg(feature = "subtle")] #[cfg_attr(docsrs, doc(cfg(feature = "subtle")))] impl<'a> ConstantTimeEq for PrivateKeyInfo<'a> { @@ -245,6 +273,10 @@ impl<'a> ConstantTimeEq for PrivateKeyInfo<'a> { } } +#[cfg(feature = "subtle")] +#[cfg_attr(docsrs, doc(cfg(feature = "subtle")))] +impl<'a> Eq for PrivateKeyInfo<'a> {} + #[cfg(feature = "subtle")] #[cfg_attr(docsrs, doc(cfg(feature = "subtle")))] impl<'a> PartialEq for PrivateKeyInfo<'a> { @@ -252,7 +284,3 @@ impl<'a> PartialEq for PrivateKeyInfo<'a> { self.ct_eq(other).into() } } - -#[cfg(feature = "subtle")] -#[cfg_attr(docsrs, doc(cfg(feature = "subtle")))] -impl<'a> Eq for PrivateKeyInfo<'a> {} diff --git a/pkcs8/src/traits.rs b/pkcs8/src/traits.rs index 4ed2f72df..dd86b90ef 100644 --- a/pkcs8/src/traits.rs +++ b/pkcs8/src/traits.rs @@ -3,18 +3,22 @@ use crate::{Error, PrivateKeyInfo, Result}; #[cfg(feature = "alloc")] -use {crate::PrivateKeyDocument, der::Document}; +use der::SecretDocument; #[cfg(feature = "encryption")] use { - crate::{EncryptedPrivateKeyDocument, EncryptedPrivateKeyInfo}, + crate::EncryptedPrivateKeyInfo, rand_core::{CryptoRng, RngCore}, }; +#[cfg(feature = "pem")] +use {crate::LineEnding, alloc::string::String, der::zeroize::Zeroizing}; + +#[cfg(feature = "pem")] +use der::pem::PemLabel; + #[cfg(feature = "std")] use std::path::Path; -#[cfg(feature = "pem")] -use {crate::LineEnding, alloc::string::String, zeroize::Zeroizing}; /// Parse a private key object from a PKCS#8 encoded document. pub trait DecodePrivateKey: for<'a> TryFrom, Error = Error> + Sized { @@ -29,16 +33,8 @@ pub trait DecodePrivateKey: for<'a> TryFrom, Error = Error> + #[cfg(feature = "encryption")] #[cfg_attr(docsrs, doc(cfg(feature = "encryption")))] fn from_pkcs8_encrypted_der(bytes: &[u8], password: impl AsRef<[u8]>) -> Result { - EncryptedPrivateKeyInfo::try_from(bytes)? - .decrypt(password) - .and_then(|doc| Self::from_pkcs8_doc(&doc)) - } - - /// Deserialize PKCS#8 private key from a [`PrivateKeyDocument`]. - #[cfg(feature = "alloc")] - #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] - fn from_pkcs8_doc(doc: &PrivateKeyDocument) -> Result { - Self::try_from(doc.decode()) + let doc = EncryptedPrivateKeyInfo::try_from(bytes)?.decrypt(password)?; + Self::from_pkcs8_der(doc.as_bytes()) } /// Deserialize PKCS#8-encoded private key from PEM. @@ -51,7 +47,9 @@ pub trait DecodePrivateKey: for<'a> TryFrom, Error = Error> + #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] fn from_pkcs8_pem(s: &str) -> Result { - PrivateKeyDocument::from_pkcs8_pem(s).and_then(|doc| Self::from_pkcs8_doc(&doc)) + let (label, doc) = SecretDocument::from_pem(s)?; + PrivateKeyInfo::validate_pem_label(label)?; + Self::from_pkcs8_der(doc.as_bytes()) } /// Deserialize encrypted PKCS#8-encoded private key from PEM and attempt @@ -65,9 +63,9 @@ pub trait DecodePrivateKey: for<'a> TryFrom, Error = Error> + #[cfg(all(feature = "encryption", feature = "pem"))] #[cfg_attr(docsrs, doc(cfg(all(feature = "encryption", feature = "pem"))))] fn from_pkcs8_encrypted_pem(s: &str, password: impl AsRef<[u8]>) -> Result { - EncryptedPrivateKeyDocument::from_pem(s)? - .decrypt(password) - .and_then(|doc| Self::from_pkcs8_doc(&doc)) + let (label, doc) = SecretDocument::from_pem(s)?; + EncryptedPrivateKeyInfo::validate_pem_label(label)?; + Self::from_pkcs8_encrypted_der(doc.as_bytes(), password) } /// Load PKCS#8 private key from an ASN.1 DER-encoded file on the local @@ -75,7 +73,7 @@ pub trait DecodePrivateKey: for<'a> TryFrom, Error = Error> + #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn read_pkcs8_der_file(path: impl AsRef) -> Result { - PrivateKeyDocument::read_pkcs8_der_file(path).and_then(|doc| Self::from_pkcs8_doc(&doc)) + Self::from_pkcs8_der(SecretDocument::read_der_file(path)?.as_bytes()) } /// Load PKCS#8 private key from a PEM-encoded file on the local filesystem. @@ -83,7 +81,9 @@ pub trait DecodePrivateKey: for<'a> TryFrom, Error = Error> + #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn read_pkcs8_pem_file(path: impl AsRef) -> Result { - PrivateKeyDocument::read_pkcs8_pem_file(path).and_then(|doc| Self::from_pkcs8_doc(&doc)) + let (label, doc) = SecretDocument::read_pem_file(path)?; + PrivateKeyInfo::validate_pem_label(&label)?; + Self::from_pkcs8_der(doc.as_bytes()) } } @@ -91,10 +91,10 @@ pub trait DecodePrivateKey: for<'a> TryFrom, Error = Error> + #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] pub trait EncodePrivateKey { - /// Serialize a [`PrivateKeyDocument`] containing a PKCS#8-encoded private key. - fn to_pkcs8_der(&self) -> Result; + /// Serialize a [`SecretDocument`] containing a PKCS#8-encoded private key. + fn to_pkcs8_der(&self) -> Result; - /// Create an [`EncryptedPrivateKeyDocument`] containing the ciphertext of + /// Create an [`SecretDocument`] containing the ciphertext of /// a PKCS#8 encoded private key encrypted under the given `password`. #[cfg(feature = "encryption")] #[cfg_attr(docsrs, doc(cfg(feature = "encryption")))] @@ -102,15 +102,16 @@ pub trait EncodePrivateKey { &self, rng: impl CryptoRng + RngCore, password: impl AsRef<[u8]>, - ) -> Result { - self.to_pkcs8_der()?.encrypt(rng, password) + ) -> Result { + EncryptedPrivateKeyInfo::encrypt(rng, password, self.to_pkcs8_der()?.as_bytes()) } /// Serialize this private key as PEM-encoded PKCS#8 with the given [`LineEnding`]. #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] fn to_pkcs8_pem(&self, line_ending: LineEnding) -> Result> { - self.to_pkcs8_der()?.to_pkcs8_pem(line_ending) + let doc = self.to_pkcs8_der()?; + Ok(doc.to_pem(PrivateKeyInfo::PEM_LABEL, line_ending)?) } /// Serialize this private key as an encrypted PEM-encoded PKCS#8 private @@ -123,23 +124,22 @@ pub trait EncodePrivateKey { password: impl AsRef<[u8]>, line_ending: LineEnding, ) -> Result> { - Ok(Zeroizing::new( - self.to_pkcs8_encrypted_der(rng, password)? - .to_pem(line_ending)?, - )) + let doc = self.to_pkcs8_encrypted_der(rng, password)?; + Ok(doc.to_pem(EncryptedPrivateKeyInfo::PEM_LABEL, line_ending)?) } /// Write ASN.1 DER-encoded PKCS#8 private key to the given path #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn write_pkcs8_der_file(&self, path: impl AsRef) -> Result<()> { - self.to_pkcs8_der()?.write_pkcs8_der_file(path) + Ok(self.to_pkcs8_der()?.write_der_file(path)?) } /// Write ASN.1 DER-encoded PKCS#8 private key to the given path #[cfg(all(feature = "pem", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(all(feature = "pem", feature = "std"))))] fn write_pkcs8_pem_file(&self, path: impl AsRef, line_ending: LineEnding) -> Result<()> { - self.to_pkcs8_der()?.write_pkcs8_pem_file(path, line_ending) + let doc = self.to_pkcs8_der()?; + Ok(doc.write_pem_file(path, PrivateKeyInfo::PEM_LABEL, line_ending)?) } } diff --git a/pkcs8/tests/encrypted_private_key.rs b/pkcs8/tests/encrypted_private_key.rs index 98e1fa0f3..2bd72aef9 100644 --- a/pkcs8/tests/encrypted_private_key.rs +++ b/pkcs8/tests/encrypted_private_key.rs @@ -3,16 +3,13 @@ #![cfg(feature = "pkcs5")] use hex_literal::hex; -use pkcs8::{pkcs5::pbes2, EncryptedPrivateKeyInfo}; +use pkcs8::{pkcs5::pbes2, EncryptedPrivateKeyInfo, PrivateKeyInfo}; -#[cfg(feature = "encryption")] -use pkcs8::PrivateKeyDocument; - -#[cfg(any(feature = "std", feature = "pem"))] -use pkcs8::EncryptedPrivateKeyDocument; +#[cfg(feature = "alloc")] +use der::Encode; -#[cfg(feature = "std")] -use der::Document; +#[cfg(feature = "pem")] +use der::EncodePem; /// Ed25519 PKCS#8 private key plaintext encoded as ASN.1 DER #[cfg(feature = "encryption")] @@ -144,28 +141,13 @@ fn decode_ed25519_encpriv_aes256_pbkdf2_sha256_der() { ); } -#[test] -#[cfg(feature = "pem")] -fn decode_ed25519_encpriv_aes256_pbkdf2_sha256_pem() { - let pkcs8_doc: EncryptedPrivateKeyDocument = - ED25519_PEM_AES256_PBKDF2_SHA256_EXAMPLE.parse().unwrap(); - - assert_eq!(pkcs8_doc.as_ref(), ED25519_DER_AES256_PBKDF2_SHA256_EXAMPLE); - - // Ensure `EncryptedPrivateKeyDocument` parses successfully - assert_eq!( - pkcs8_doc.encrypted_private_key_info(), - EncryptedPrivateKeyInfo::try_from(ED25519_DER_AES256_PBKDF2_SHA256_EXAMPLE).unwrap() - ); -} - #[cfg(feature = "encryption")] #[test] fn decrypt_ed25519_der_encpriv_aes256_pbkdf2_sha256() { let enc_pk = EncryptedPrivateKeyInfo::try_from(ED25519_DER_AES256_PBKDF2_SHA256_EXAMPLE).unwrap(); let pk = enc_pk.decrypt(PASSWORD).unwrap(); - assert_eq!(pk.as_ref(), ED25519_DER_PLAINTEXT_EXAMPLE); + assert_eq!(pk.as_bytes(), ED25519_DER_PLAINTEXT_EXAMPLE); } #[cfg(feature = "encryption")] @@ -173,7 +155,7 @@ fn decrypt_ed25519_der_encpriv_aes256_pbkdf2_sha256() { fn decrypt_ed25519_der_encpriv_aes256_scrypt() { let enc_pk = EncryptedPrivateKeyInfo::try_from(ED25519_DER_AES256_SCRYPT_EXAMPLE).unwrap(); let pk = enc_pk.decrypt(PASSWORD).unwrap(); - assert_eq!(pk.as_ref(), ED25519_DER_PLAINTEXT_EXAMPLE); + assert_eq!(pk.as_bytes(), ED25519_DER_PLAINTEXT_EXAMPLE); } #[cfg(feature = "encryption")] @@ -186,13 +168,13 @@ fn encrypt_ed25519_der_encpriv_aes256_pbkdf2_sha256() { ) .unwrap(); - let pk_plaintext = PrivateKeyDocument::try_from(ED25519_DER_PLAINTEXT_EXAMPLE).unwrap(); + let pk_plaintext = PrivateKeyInfo::try_from(ED25519_DER_PLAINTEXT_EXAMPLE).unwrap(); let pk_encrypted = pk_plaintext .encrypt_with_params(pbes2_params, PASSWORD) .unwrap(); assert_eq!( - pk_encrypted.as_ref(), + pk_encrypted.as_bytes(), ED25519_DER_AES256_PBKDF2_SHA256_EXAMPLE ); } @@ -207,12 +189,12 @@ fn encrypt_ed25519_der_encpriv_aes256_scrypt() { ) .unwrap(); - let pk_plaintext = PrivateKeyDocument::try_from(ED25519_DER_PLAINTEXT_EXAMPLE).unwrap(); + let pk_plaintext = PrivateKeyInfo::try_from(ED25519_DER_PLAINTEXT_EXAMPLE).unwrap(); let pk_encrypted = pk_plaintext .encrypt_with_params(scrypt_params, PASSWORD) .unwrap(); - assert_eq!(pk_encrypted.as_ref(), ED25519_DER_AES256_SCRYPT_EXAMPLE); + assert_eq!(pk_encrypted.as_bytes(), ED25519_DER_AES256_SCRYPT_EXAMPLE); } #[test] @@ -221,7 +203,7 @@ fn encode_ed25519_encpriv_aes256_pbkdf2_sha256_der() { let pk = EncryptedPrivateKeyInfo::try_from(ED25519_DER_AES256_PBKDF2_SHA256_EXAMPLE).unwrap(); assert_eq!( ED25519_DER_AES256_PBKDF2_SHA256_EXAMPLE, - pk.to_der().unwrap().as_ref() + &pk.to_vec().unwrap() ); } @@ -231,36 +213,16 @@ fn encode_ed25519_encpriv_aes256_pbkdf2_sha256_pem() { let pk = EncryptedPrivateKeyInfo::try_from(ED25519_DER_AES256_PBKDF2_SHA256_EXAMPLE).unwrap(); assert_eq!( ED25519_PEM_AES256_PBKDF2_SHA256_EXAMPLE, - &*pk.to_pem(Default::default()).unwrap() + pk.to_pem(Default::default()).unwrap() ); } -#[test] -#[cfg(feature = "std")] -fn read_der_file() { - let pkcs8_doc = EncryptedPrivateKeyDocument::read_der_file( - "tests/examples/ed25519-encpriv-aes256-pbkdf2-sha256.der", - ) - .unwrap(); - assert_eq!(pkcs8_doc.as_ref(), ED25519_DER_AES256_PBKDF2_SHA256_EXAMPLE); -} - -#[test] -#[cfg(all(feature = "pem", feature = "std"))] -fn read_pem_file() { - let pkcs8_doc = EncryptedPrivateKeyDocument::read_pem_file( - "tests/examples/ed25519-encpriv-aes256-pbkdf2-sha256.pem", - ) - .unwrap(); - assert_eq!(pkcs8_doc.as_ref(), ED25519_DER_AES256_PBKDF2_SHA256_EXAMPLE); -} - #[test] #[cfg(feature = "3des")] fn decrypt_ed25519_der_encpriv_des3_pbkdf2_sha256() { let enc_pk = EncryptedPrivateKeyInfo::try_from(ED25519_DER_DES3_PBKDF2_SHA256_EXAMPLE).unwrap(); let pk = enc_pk.decrypt(PASSWORD).unwrap(); - assert_eq!(pk.as_ref(), ED25519_DER_PLAINTEXT_EXAMPLE); + assert_eq!(pk.as_bytes(), ED25519_DER_PLAINTEXT_EXAMPLE); } #[test] @@ -268,5 +230,5 @@ fn decrypt_ed25519_der_encpriv_des3_pbkdf2_sha256() { fn decrypt_ed25519_der_encpriv_des_pbkdf2_sha256() { let enc_pk = EncryptedPrivateKeyInfo::try_from(ED25519_DER_DES_PBKDF2_SHA256_EXAMPLE).unwrap(); let pk = enc_pk.decrypt(PASSWORD).unwrap(); - assert_eq!(pk.as_ref(), ED25519_DER_PLAINTEXT_EXAMPLE); + assert_eq!(pk.as_bytes(), ED25519_DER_PLAINTEXT_EXAMPLE); } diff --git a/pkcs8/tests/private_key.rs b/pkcs8/tests/private_key.rs index 0ec6d4956..15d669495 100644 --- a/pkcs8/tests/private_key.rs +++ b/pkcs8/tests/private_key.rs @@ -3,14 +3,11 @@ use hex_literal::hex; use pkcs8::{PrivateKeyInfo, Version}; -#[cfg(feature = "pem")] -use der::Document; - -#[cfg(any(feature = "pem", feature = "std"))] -use pkcs8::PrivateKeyDocument; +#[cfg(feature = "alloc")] +use der::Encode; -#[cfg(feature = "std")] -use pkcs8::DecodePrivateKey; +#[cfg(feature = "pem")] +use der::{pem::LineEnding, EncodePem}; /// Elliptic Curve (P-256) PKCS#8 private key encoded as ASN.1 DER const EC_P256_DER_EXAMPLE: &[u8] = include_bytes!("examples/p256-priv.der"); @@ -123,121 +120,63 @@ fn decode_x25519_der() { ); } -#[test] -#[cfg(feature = "pem")] -fn decode_ec_p256_pem() { - let pkcs8_doc: PrivateKeyDocument = EC_P256_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(pkcs8_doc.as_ref(), EC_P256_DER_EXAMPLE); - - // Ensure `PrivateKeyDocument` parses successfully - let pk_info = PrivateKeyInfo::try_from(EC_P256_DER_EXAMPLE).unwrap(); - assert_eq!(pkcs8_doc.decode().algorithm, pk_info.algorithm); -} - -#[test] -#[cfg(feature = "pem")] -fn decode_ed25519_pem() { - let pkcs8_doc: PrivateKeyDocument = ED25519_PEM_V1_EXAMPLE.parse().unwrap(); - assert_eq!(pkcs8_doc.as_ref(), ED25519_DER_V1_EXAMPLE); - - // Ensure `PrivateKeyDocument` parses successfully - let pk_info = PrivateKeyInfo::try_from(ED25519_DER_V1_EXAMPLE).unwrap(); - assert_eq!(pkcs8_doc.decode().algorithm, pk_info.algorithm); -} - -#[test] -#[cfg(feature = "pem")] -fn decode_rsa_2048_pem() { - let pkcs8_doc: PrivateKeyDocument = RSA_2048_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(pkcs8_doc.as_ref(), RSA_2048_DER_EXAMPLE); - - // Ensure `PrivateKeyDocument` parses successfully - let pk_info = PrivateKeyInfo::try_from(RSA_2048_DER_EXAMPLE).unwrap(); - assert_eq!(pkcs8_doc.decode().algorithm, pk_info.algorithm); -} - -#[test] -#[cfg(feature = "pem")] -fn decode_x25519_pem() { - let pkcs8_doc: PrivateKeyDocument = X25519_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(pkcs8_doc.as_ref(), X25519_DER_EXAMPLE); - - // Ensure `PrivateKeyDocument` parses successfully - let pk_info = PrivateKeyInfo::try_from(X25519_DER_EXAMPLE).unwrap(); - assert_eq!(pkcs8_doc.decode().algorithm, pk_info.algorithm); -} - #[test] #[cfg(feature = "alloc")] fn encode_ec_p256_der() { let pk = PrivateKeyInfo::try_from(EC_P256_DER_EXAMPLE).unwrap(); - let pk_encoded = pk.to_der().unwrap(); - assert_eq!(EC_P256_DER_EXAMPLE, pk_encoded.as_ref()); + let pk_encoded = pk.to_vec().unwrap(); + assert_eq!(EC_P256_DER_EXAMPLE, pk_encoded); } #[test] #[cfg(feature = "alloc")] fn encode_ed25519_der_v1() { let pk = PrivateKeyInfo::try_from(ED25519_DER_V1_EXAMPLE).unwrap(); - assert_eq!(ED25519_DER_V1_EXAMPLE, pk.to_der().unwrap().as_ref()); + assert_eq!(ED25519_DER_V1_EXAMPLE, pk.to_vec().unwrap()); } #[test] #[cfg(all(feature = "alloc", feature = "subtle"))] fn encode_ed25519_der_v2() { - let pk = PrivateKeyInfo::try_from(ED25519_DER_V2_EXAMPLE).unwrap(); - assert_eq!(pk.to_der().unwrap().decode(), pk); + let private_key = PrivateKeyInfo::try_from(ED25519_DER_V2_EXAMPLE).unwrap(); + let private_der = private_key.to_vec().unwrap(); + assert_eq!( + private_key, + PrivateKeyInfo::try_from(private_der.as_ref()).unwrap() + ); } #[test] #[cfg(feature = "alloc")] fn encode_rsa_2048_der() { let pk = PrivateKeyInfo::try_from(RSA_2048_DER_EXAMPLE).unwrap(); - assert_eq!(RSA_2048_DER_EXAMPLE, pk.to_der().unwrap().as_ref()); + assert_eq!(RSA_2048_DER_EXAMPLE, &pk.to_vec().unwrap()); } #[test] #[cfg(feature = "pem")] fn encode_ec_p256_pem() { let pk = PrivateKeyInfo::try_from(EC_P256_DER_EXAMPLE).unwrap(); - assert_eq!( - EC_P256_PEM_EXAMPLE, - &*pk.to_pem(Default::default()).unwrap() - ); + assert_eq!(EC_P256_PEM_EXAMPLE, pk.to_pem(LineEnding::LF).unwrap()); } #[test] #[cfg(feature = "pem")] fn encode_ed25519_pem() { let pk = PrivateKeyInfo::try_from(ED25519_DER_V1_EXAMPLE).unwrap(); - assert_eq!( - ED25519_PEM_V1_EXAMPLE, - &*pk.to_pem(Default::default()).unwrap() - ); + assert_eq!(ED25519_PEM_V1_EXAMPLE, pk.to_pem(LineEnding::LF).unwrap()); } #[test] #[cfg(feature = "pem")] fn encode_rsa_2048_pem() { let pk = PrivateKeyInfo::try_from(RSA_2048_DER_EXAMPLE).unwrap(); - assert_eq!( - RSA_2048_PEM_EXAMPLE, - &*pk.to_pem(Default::default()).unwrap() - ); + assert_eq!(RSA_2048_PEM_EXAMPLE, pk.to_pem(LineEnding::LF).unwrap()); } #[test] -#[cfg(feature = "std")] -fn read_der_file() { - let pkcs8_doc = - PrivateKeyDocument::read_pkcs8_der_file("tests/examples/p256-priv.der").unwrap(); - assert_eq!(pkcs8_doc.as_ref(), EC_P256_DER_EXAMPLE); -} - -#[test] -#[cfg(all(feature = "pem", feature = "std"))] -fn read_pem_file() { - let pkcs8_doc = - PrivateKeyDocument::read_pkcs8_pem_file("tests/examples/p256-priv.pem").unwrap(); - assert_eq!(pkcs8_doc.as_ref(), EC_P256_DER_EXAMPLE); +#[cfg(feature = "pem")] +fn encode_x25519_pem() { + let pk = PrivateKeyInfo::try_from(X25519_DER_EXAMPLE).unwrap(); + assert_eq!(X25519_PEM_EXAMPLE, pk.to_pem(LineEnding::LF).unwrap()); } diff --git a/pkcs8/tests/public_key.rs b/pkcs8/tests/public_key.rs deleted file mode 100644 index 00b4063c9..000000000 --- a/pkcs8/tests/public_key.rs +++ /dev/null @@ -1,183 +0,0 @@ -//! Public key (`SubjectPublicKeyInfo`) tests - -use hex_literal::hex; -use pkcs8::SubjectPublicKeyInfo; - -#[cfg(feature = "alloc")] -use der::Encode; - -#[cfg(feature = "pem")] -use pkcs8::{der::Document, EncodePublicKey}; - -#[cfg(feature = "std")] -use pkcs8::DecodePublicKey; - -#[cfg(any(feature = "pem", feature = "std"))] -use pkcs8::PublicKeyDocument; - -/// Elliptic Curve (P-256) `SubjectPublicKeyInfo` encoded as ASN.1 DER -const EC_P256_DER_EXAMPLE: &[u8] = include_bytes!("examples/p256-pub.der"); - -/// Ed25519 `SubjectPublicKeyInfo` encoded as ASN.1 DER -const ED25519_DER_EXAMPLE: &[u8] = include_bytes!("examples/ed25519-pub.der"); - -/// RSA-2048 `SubjectPublicKeyInfo` encoded as ASN.1 DER -const RSA_2048_DER_EXAMPLE: &[u8] = include_bytes!("examples/rsa2048-pub.der"); - -/// Elliptic Curve (P-256) public key encoded as PEM -#[cfg(feature = "pem")] -const EC_P256_PEM_EXAMPLE: &str = include_str!("examples/p256-pub.pem"); - -/// Ed25519 public key encoded as PEM -#[cfg(feature = "pem")] -const ED25519_PEM_EXAMPLE: &str = include_str!("examples/ed25519-pub.pem"); - -/// RSA-2048 PKCS#8 public key encoded as PEM -#[cfg(feature = "pem")] -const RSA_2048_PEM_EXAMPLE: &str = include_str!("examples/rsa2048-pub.pem"); - -#[test] -fn decode_ec_p256_der() { - let spki = SubjectPublicKeyInfo::try_from(EC_P256_DER_EXAMPLE).unwrap(); - - assert_eq!(spki.algorithm.oid, "1.2.840.10045.2.1".parse().unwrap()); - - assert_eq!( - spki.algorithm.parameters.unwrap().oid().unwrap(), - "1.2.840.10045.3.1.7".parse().unwrap() - ); - - assert_eq!(spki.subject_public_key, &hex!("041CACFFB55F2F2CEFD89D89EB374B2681152452802DEEA09916068137D839CF7FC481A44492304D7EF66AC117BEFE83A8D08F155F2B52F9F618DD447029048E0F")[..]); -} - -#[test] -fn decode_ed25519_der() { - let spki = SubjectPublicKeyInfo::try_from(ED25519_DER_EXAMPLE).unwrap(); - - assert_eq!(spki.algorithm.oid, "1.3.101.112".parse().unwrap()); - assert_eq!(spki.algorithm.parameters, None); - assert_eq!( - spki.subject_public_key, - &hex!("4D29167F3F1912A6F7ADFA293A051A15C05EC67B8F17267B1C5550DCE853BD0D")[..] - ); -} - -#[test] -fn decode_rsa_2048_der() { - let spki = SubjectPublicKeyInfo::try_from(RSA_2048_DER_EXAMPLE).unwrap(); - - assert_eq!(spki.algorithm.oid, "1.2.840.113549.1.1.1".parse().unwrap()); - assert!(spki.algorithm.parameters.unwrap().is_null()); - assert_eq!(spki.subject_public_key, &hex!("3082010A0282010100B6C42C515F10A6AAF282C63EDBE24243A170F3FA2633BD4833637F47CA4F6F36E03A5D29EFC3191AC80F390D874B39E30F414FCEC1FCA0ED81E547EDC2CD382C76F61C9018973DB9FA537972A7C701F6B77E0982DFC15FC01927EE5E7CD94B4F599FF07013A7C8281BDF22DCBC9AD7CABB7C4311C982F58EDB7213AD4558B332266D743AED8192D1884CADB8B14739A8DADA66DC970806D9C7AC450CB13D0D7C575FB198534FC61BC41BC0F0574E0E0130C7BBBFBDFDC9F6A6E2E3E2AFF1CBEAC89BA57884528D55CFB08327A1E8C89F4E003CF2888E933241D9D695BCBBACDC90B44E3E095FA37058EA25B13F5E295CBEAC6DE838AB8C50AF61E298975B872F0203010001")[..]); -} - -#[test] -#[cfg(feature = "pem")] -fn decode_ec_p256_pem() { - let doc: PublicKeyDocument = EC_P256_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(doc.as_ref(), EC_P256_DER_EXAMPLE); - - // Ensure `PublicKeyDocument` parses successfully - let spki = SubjectPublicKeyInfo::try_from(EC_P256_DER_EXAMPLE).unwrap(); - assert_eq!(doc.decode(), spki); -} - -#[test] -#[cfg(feature = "pem")] -fn decode_ed25519_pem() { - let doc: PublicKeyDocument = ED25519_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(doc.as_ref(), ED25519_DER_EXAMPLE); - - // Ensure `PublicKeyDocument` parses successfully - let spki = SubjectPublicKeyInfo::try_from(ED25519_DER_EXAMPLE).unwrap(); - assert_eq!(doc.decode(), spki); -} - -#[test] -#[cfg(feature = "pem")] -fn decode_rsa_2048_pem() { - let doc: PublicKeyDocument = RSA_2048_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(doc.as_ref(), RSA_2048_DER_EXAMPLE); - - // Ensure `PublicKeyDocument` parses successfully - let spki = SubjectPublicKeyInfo::try_from(RSA_2048_DER_EXAMPLE).unwrap(); - assert_eq!(doc.decode(), spki); -} - -#[test] -#[cfg(feature = "alloc")] -fn encode_ec_p256_der() { - let pk = SubjectPublicKeyInfo::try_from(EC_P256_DER_EXAMPLE).unwrap(); - let pk_encoded = pk.to_vec().unwrap(); - assert_eq!(EC_P256_DER_EXAMPLE, pk_encoded.as_slice()); -} - -#[test] -#[cfg(feature = "alloc")] -fn encode_ed25519_der() { - let pk = SubjectPublicKeyInfo::try_from(ED25519_DER_EXAMPLE).unwrap(); - let pk_encoded = pk.to_vec().unwrap(); - assert_eq!(ED25519_DER_EXAMPLE, pk_encoded.as_slice()); -} - -#[test] -#[cfg(feature = "alloc")] -fn encode_rsa_2048_der() { - let pk = SubjectPublicKeyInfo::try_from(RSA_2048_DER_EXAMPLE).unwrap(); - let pk_encoded = pk.to_vec().unwrap(); - assert_eq!(RSA_2048_DER_EXAMPLE, pk_encoded.as_slice()); -} - -#[test] -#[cfg(feature = "pem")] -fn encode_ec_p256_pem() { - let pk = SubjectPublicKeyInfo::try_from(EC_P256_DER_EXAMPLE).unwrap(); - let pk_encoded = PublicKeyDocument::try_from(pk) - .unwrap() - .to_public_key_pem(Default::default()) - .unwrap(); - - assert_eq!(EC_P256_PEM_EXAMPLE, pk_encoded); -} - -#[test] -#[cfg(feature = "pem")] -fn encode_ed25519_pem() { - let pk = SubjectPublicKeyInfo::try_from(ED25519_DER_EXAMPLE).unwrap(); - let pk_encoded = PublicKeyDocument::try_from(pk) - .unwrap() - .to_public_key_pem(Default::default()) - .unwrap(); - - assert_eq!(ED25519_PEM_EXAMPLE, pk_encoded); -} - -#[test] -#[cfg(feature = "pem")] -fn encode_rsa_2048_pem() { - let pk = SubjectPublicKeyInfo::try_from(RSA_2048_DER_EXAMPLE).unwrap(); - let pk_encoded = PublicKeyDocument::try_from(pk) - .unwrap() - .to_public_key_pem(Default::default()) - .unwrap(); - - assert_eq!(RSA_2048_PEM_EXAMPLE, pk_encoded); -} - -#[test] -#[cfg(feature = "std")] -fn read_der_file() { - let pkcs8_doc = - PublicKeyDocument::read_public_key_der_file("tests/examples/p256-pub.der").unwrap(); - - assert_eq!(pkcs8_doc.as_ref(), EC_P256_DER_EXAMPLE); -} - -#[test] -#[cfg(all(feature = "pem", feature = "std"))] -fn read_pem_file() { - let pkcs8_doc = - PublicKeyDocument::read_public_key_pem_file("tests/examples/p256-pub.pem").unwrap(); - - assert_eq!(pkcs8_doc.as_ref(), EC_P256_DER_EXAMPLE); -} diff --git a/pkcs8/tests/traits.rs b/pkcs8/tests/traits.rs new file mode 100644 index 000000000..1c8a969bc --- /dev/null +++ b/pkcs8/tests/traits.rs @@ -0,0 +1,108 @@ +//! Tests for PKCS#8 encoding/decoding traits. + +#![cfg(any(feature = "pem", feature = "std"))] + +use der::Encode; +use pkcs8::{DecodePrivateKey, EncodePrivateKey, Error, PrivateKeyInfo, Result, SecretDocument}; + +#[cfg(feature = "pem")] +use pkcs8::der::pem::LineEnding; + +#[cfg(feature = "std")] +use tempfile::tempdir; + +#[cfg(all(feature = "pem", feature = "std"))] +use std::fs; + +/// Ed25519 `PrivateKeyInfo` encoded as ASN.1 DER +const ED25519_DER_EXAMPLE: &[u8] = include_bytes!("examples/ed25519-priv-pkcs8v1.der"); + +/// Ed25519 private key encoded as PEM +#[cfg(feature = "pem")] +const ED25519_PEM_EXAMPLE: &str = include_str!("examples/ed25519-priv-pkcs8v1.pem"); + +/// Mock key type for testing trait impls against. +pub struct MockKey(Vec); + +impl AsRef<[u8]> for MockKey { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl DecodePrivateKey for MockKey { + fn from_pkcs8_der(bytes: &[u8]) -> Result { + Ok(MockKey(bytes.to_vec())) + } +} + +impl EncodePrivateKey for MockKey { + fn to_pkcs8_der(&self) -> Result { + Ok(SecretDocument::try_from(self.as_ref())?) + } +} + +impl TryFrom> for MockKey { + type Error = Error; + + fn try_from(pkcs8: PrivateKeyInfo<'_>) -> Result { + Ok(MockKey(pkcs8.to_vec()?)) + } +} + +#[cfg(feature = "pem")] +#[test] +fn from_pkcs8_pem() { + let key = MockKey::from_pkcs8_pem(ED25519_PEM_EXAMPLE).unwrap(); + assert_eq!(key.as_ref(), ED25519_DER_EXAMPLE); +} + +#[cfg(feature = "std")] +#[test] +fn read_pkcs8_der_file() { + let key = MockKey::read_pkcs8_der_file("tests/examples/ed25519-priv-pkcs8v1.der").unwrap(); + assert_eq!(key.as_ref(), ED25519_DER_EXAMPLE); +} + +#[cfg(all(feature = "pem", feature = "std"))] +#[test] +fn read_pkcs8_pem_file() { + let key = MockKey::read_pkcs8_pem_file("tests/examples/ed25519-priv-pkcs8v1.pem").unwrap(); + assert_eq!(key.as_ref(), ED25519_DER_EXAMPLE); +} + +#[cfg(feature = "pem")] +#[test] +fn to_pkcs8_pem() { + let pem = MockKey(ED25519_DER_EXAMPLE.to_vec()) + .to_pkcs8_pem(LineEnding::LF) + .unwrap(); + + assert_eq!(&*pem, ED25519_PEM_EXAMPLE); +} + +#[cfg(feature = "std")] +#[test] +fn write_pkcs8_der_file() { + let dir = tempdir().unwrap(); + let path = dir.path().join("example.der"); + MockKey(ED25519_DER_EXAMPLE.to_vec()) + .write_pkcs8_der_file(&path) + .unwrap(); + + let key = MockKey::read_pkcs8_der_file(&path).unwrap(); + assert_eq!(key.as_ref(), ED25519_DER_EXAMPLE); +} + +#[cfg(all(feature = "pem", feature = "std"))] +#[test] +fn write_pkcs8_pem_file() { + let dir = tempdir().unwrap(); + let path = dir.path().join("example.pem"); + MockKey(ED25519_DER_EXAMPLE.to_vec()) + .write_pkcs8_pem_file(&path, LineEnding::LF) + .unwrap(); + + let pem = fs::read_to_string(path).unwrap(); + assert_eq!(&pem, ED25519_PEM_EXAMPLE); +} diff --git a/sec1/Cargo.toml b/sec1/Cargo.toml index 6a88e6607..fd1a54b3a 100644 --- a/sec1/Cargo.toml +++ b/sec1/Cargo.toml @@ -16,7 +16,6 @@ edition = "2021" rust-version = "1.57" [dependencies] -# optional dependencies der = { version = "=0.6.0-pre.3", optional = true, features = ["oid"], path = "../der" } generic-array = { version = "0.14.4", optional = true, default-features = false } pkcs8 = { version = "=0.9.0-pre.1", optional = true, default-features = false, path = "../pkcs8" } @@ -26,6 +25,7 @@ zeroize = { version = "1", optional = true, default-features = false } [dev-dependencies] hex-literal = "0.3" +tempfile = "3" [features] default = ["der", "point"] diff --git a/sec1/src/error.rs b/sec1/src/error.rs index 107b7fa40..3700ac5f2 100644 --- a/sec1/src/error.rs +++ b/sec1/src/error.rs @@ -2,6 +2,9 @@ use core::fmt; +#[cfg(feature = "pem")] +use der::pem; + /// Result type with `sec1` crate's [`Error`] type. pub type Result = core::result::Result; @@ -56,6 +59,13 @@ impl From for Error { } } +#[cfg(feature = "pem")] +impl From for Error { + fn from(err: pem::Error) -> Error { + der::Error::from(err).into() + } +} + #[cfg(feature = "pkcs8")] impl From for Error { fn from(err: pkcs8::Error) -> Error { diff --git a/sec1/src/lib.rs b/sec1/src/lib.rs index 34d816848..c92e54fb9 100644 --- a/sec1/src/lib.rs +++ b/sec1/src/lib.rs @@ -19,6 +19,7 @@ //! encoding is being used, and if so encode the points as hexadecimal. #[cfg(feature = "alloc")] +#[allow(unused_extern_crates)] extern crate alloc; #[cfg(feature = "std")] extern crate std; @@ -49,7 +50,7 @@ pub use generic_array::typenum::consts; pub use crate::{parameters::EcParameters, private_key::EcPrivateKey, traits::DecodeEcPrivateKey}; #[cfg(feature = "alloc")] -pub use crate::{private_key::document::EcPrivateKeyDocument, traits::EncodeEcPrivateKey}; +pub use crate::traits::EncodeEcPrivateKey; #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] diff --git a/sec1/src/private_key.rs b/sec1/src/private_key.rs index 9fec3fbd5..0505b570d 100644 --- a/sec1/src/private_key.rs +++ b/sec1/src/private_key.rs @@ -5,16 +5,19 @@ //! //! -#[cfg(feature = "alloc")] -pub(crate) mod document; - -use crate::{EcParameters, Error}; +use crate::{EcParameters, Error, Result}; use core::fmt; use der::{ asn1::{BitString, ContextSpecific, OctetString}, Decode, Decoder, Encode, Sequence, Tag, TagMode, TagNumber, }; +#[cfg(feature = "alloc")] +use der::SecretDocument; + +#[cfg(feature = "pem")] +use der::pem::PemLabel; + /// `ECPrivateKey` version. /// /// From [RFC5913 Section 3]: @@ -120,7 +123,7 @@ impl<'a> Sequence<'a> for EcPrivateKey<'a> { impl<'a> TryFrom<&'a [u8]> for EcPrivateKey<'a> { type Error = Error; - fn try_from(bytes: &'a [u8]) -> Result, Error> { + fn try_from(bytes: &'a [u8]) -> Result> { Ok(Self::from_der(bytes)?) } } @@ -133,3 +136,29 @@ impl<'a> fmt::Debug for EcPrivateKey<'a> { .finish_non_exhaustive() } } + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl TryFrom> for SecretDocument { + type Error = Error; + + fn try_from(private_key: EcPrivateKey<'_>) -> Result { + SecretDocument::try_from(&private_key) + } +} + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl TryFrom<&EcPrivateKey<'_>> for SecretDocument { + type Error = Error; + + fn try_from(private_key: &EcPrivateKey<'_>) -> Result { + Ok(Self::encode_msg(private_key)?) + } +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl PemLabel for EcPrivateKey<'_> { + const PEM_LABEL: &'static str = "EC PRIVATE KEY"; +} diff --git a/sec1/src/private_key/document.rs b/sec1/src/private_key/document.rs deleted file mode 100644 index df1af5177..000000000 --- a/sec1/src/private_key/document.rs +++ /dev/null @@ -1,149 +0,0 @@ -//! SEC1 EC private key document. - -use crate::{DecodeEcPrivateKey, EcPrivateKey, EncodeEcPrivateKey, Error, Result}; -use alloc::vec::Vec; -use core::fmt; -use der::{Decode, Document, Encode}; -use zeroize::{Zeroize, Zeroizing}; - -#[cfg(feature = "pem")] -use { - crate::{pem, LineEnding}, - alloc::string::String, - core::str::FromStr, -}; - -#[cfg(feature = "std")] -use std::path::Path; - -/// SEC1 `EC PRIVATE KEY` document. -/// -/// This type provides storage for [`EcPrivateKey`] encoded as ASN.1 DER -/// with the invariant that the contained-document is "well-formed", i.e. it -/// will parse successfully according to this crate's parsing rules. -#[derive(Clone)] -#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] -pub struct EcPrivateKeyDocument(Zeroizing>); - -impl<'a> Document<'a> for EcPrivateKeyDocument { - type Message = EcPrivateKey<'a>; - const SENSITIVE: bool = true; -} - -impl DecodeEcPrivateKey for EcPrivateKeyDocument { - fn from_sec1_der(bytes: &[u8]) -> Result { - Ok(Self::from_der(bytes)?) - } - - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - fn from_sec1_pem(s: &str) -> Result { - Ok(Self::from_pem(s)?) - } - - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn read_sec1_der_file(path: impl AsRef) -> Result { - Ok(Self::read_der_file(path)?) - } - - #[cfg(all(feature = "pem", feature = "std"))] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn read_sec1_pem_file(path: impl AsRef) -> Result { - Ok(Self::read_pem_file(path)?) - } -} - -impl EncodeEcPrivateKey for EcPrivateKeyDocument { - fn to_sec1_der(&self) -> Result { - Ok(self.clone()) - } - - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - fn to_sec1_pem(&self, line_ending: LineEnding) -> Result> { - Ok(Zeroizing::new(self.to_pem(line_ending)?)) - } - - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn write_sec1_der_file(&self, path: impl AsRef) -> Result<()> { - Ok(self.write_der_file(path)?) - } - - #[cfg(all(feature = "pem", feature = "std"))] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn write_sec1_pem_file(&self, path: impl AsRef, line_ending: LineEnding) -> Result<()> { - Ok(self.write_pem_file(path, line_ending)?) - } -} - -impl AsRef<[u8]> for EcPrivateKeyDocument { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl TryFrom<&[u8]> for EcPrivateKeyDocument { - type Error = Error; - - fn try_from(bytes: &[u8]) -> Result { - Ok(Self::from_der(bytes)?) - } -} - -impl TryFrom> for EcPrivateKeyDocument { - type Error = Error; - - fn try_from(private_key: EcPrivateKey<'_>) -> Result { - Self::try_from(&private_key) - } -} - -impl TryFrom<&EcPrivateKey<'_>> for EcPrivateKeyDocument { - type Error = Error; - - fn try_from(private_key: &EcPrivateKey<'_>) -> Result { - Ok(Self(Zeroizing::new(private_key.to_vec()?))) - } -} - -impl TryFrom> for EcPrivateKeyDocument { - type Error = der::Error; - - fn try_from(mut bytes: Vec) -> der::Result { - // Ensure document is well-formed - if let Err(err) = EcPrivateKey::from_der(bytes.as_slice()) { - bytes.zeroize(); - return Err(err); - } - - Ok(Self(Zeroizing::new(bytes))) - } -} - -impl fmt::Debug for EcPrivateKeyDocument { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_tuple("EcPrivateKeyDocument") - .field(&self.decode()) - .finish() - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl FromStr for EcPrivateKeyDocument { - type Err = Error; - - fn from_str(s: &str) -> Result { - Self::from_sec1_pem(s) - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl pem::PemLabel for EcPrivateKeyDocument { - const PEM_LABEL: &'static str = "EC PRIVATE KEY"; -} diff --git a/sec1/src/traits.rs b/sec1/src/traits.rs index c63780efe..cf0d9711e 100644 --- a/sec1/src/traits.rs +++ b/sec1/src/traits.rs @@ -3,10 +3,10 @@ use crate::Result; #[cfg(feature = "alloc")] -use crate::EcPrivateKeyDocument; +use der::SecretDocument; #[cfg(feature = "pem")] -use {crate::LineEnding, alloc::string::String}; +use {crate::LineEnding, alloc::string::String, der::pem::PemLabel}; #[cfg(feature = "pkcs8")] use { @@ -14,9 +14,6 @@ use { der::Decode, }; -#[cfg(all(feature = "alloc", feature = "pkcs8"))] -use der::Document; - #[cfg(feature = "std")] use std::path::Path; @@ -40,7 +37,9 @@ pub trait DecodeEcPrivateKey: Sized { #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] fn from_sec1_pem(s: &str) -> Result { - EcPrivateKeyDocument::from_sec1_pem(s).and_then(|doc| Self::from_sec1_der(doc.as_der())) + let (label, doc) = SecretDocument::from_pem(s)?; + EcPrivateKey::validate_pem_label(label)?; + Self::from_sec1_der(doc.as_bytes()) } /// Load SEC1 private key from an ASN.1 DER-encoded file on the local @@ -48,8 +47,7 @@ pub trait DecodeEcPrivateKey: Sized { #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn read_sec1_der_file(path: impl AsRef) -> Result { - EcPrivateKeyDocument::read_sec1_der_file(path) - .and_then(|doc| Self::from_sec1_der(doc.as_der())) + Self::from_sec1_der(SecretDocument::read_der_file(path)?.as_bytes()) } /// Load SEC1 private key from a PEM-encoded file on the local filesystem. @@ -57,8 +55,9 @@ pub trait DecodeEcPrivateKey: Sized { #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn read_sec1_pem_file(path: impl AsRef) -> Result { - EcPrivateKeyDocument::read_sec1_pem_file(path) - .and_then(|doc| Self::from_sec1_der(doc.as_der())) + let (label, doc) = SecretDocument::read_pem_file(path)?; + EcPrivateKey::validate_pem_label(&label)?; + Self::from_sec1_der(doc.as_bytes()) } } @@ -66,8 +65,8 @@ pub trait DecodeEcPrivateKey: Sized { #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(all(feature = "alloc", feature = "der"))))] pub trait EncodeEcPrivateKey { - /// Serialize a [`EcPrivateKeyDocument`] containing a SEC1-encoded private key. - fn to_sec1_der(&self) -> Result; + /// Serialize a [`SecretDocument`] containing a SEC1-encoded private key. + fn to_sec1_der(&self) -> Result; /// Serialize this private key as PEM-encoded SEC1 with the given [`LineEnding`]. /// @@ -75,14 +74,15 @@ pub trait EncodeEcPrivateKey { #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] fn to_sec1_pem(&self, line_ending: LineEnding) -> Result> { - self.to_sec1_der()?.to_sec1_pem(line_ending) + let doc = self.to_sec1_der()?; + Ok(doc.to_pem(EcPrivateKey::PEM_LABEL, line_ending)?) } /// Write ASN.1 DER-encoded SEC1 private key to the given path. #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn write_sec1_der_file(&self, path: impl AsRef) -> Result<()> { - self.to_sec1_der()?.write_sec1_der_file(path) + Ok(self.to_sec1_der()?.write_der_file(path)?) } /// Write ASN.1 DER-encoded SEC1 private key to the given path. @@ -90,7 +90,8 @@ pub trait EncodeEcPrivateKey { #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn write_sec1_pem_file(&self, path: impl AsRef, line_ending: LineEnding) -> Result<()> { - self.to_sec1_der()?.write_sec1_pem_file(path, line_ending) + let doc = self.to_sec1_der()?; + Ok(doc.write_pem_file(path, EcPrivateKey::PEM_LABEL, line_ending)?) } } @@ -118,9 +119,11 @@ impl DecodeEcPrivateKey for T { #[cfg(all(feature = "alloc", feature = "pkcs8"))] #[cfg_attr(docsrs, doc(cfg(all(feature = "alloc", feature = "pkcs8"))))] impl EncodeEcPrivateKey for T { - fn to_sec1_der(&self) -> Result { + fn to_sec1_der(&self) -> Result { let doc = self.to_pkcs8_der()?; - let pkcs8_key = pkcs8::PrivateKeyInfo::from_der(doc.as_der())?; + let pkcs8_key = pkcs8::PrivateKeyInfo::from_der(doc.as_bytes())?; + pkcs8_key.algorithm.assert_algorithm_oid(ALGORITHM_OID)?; + let mut pkcs1_key = EcPrivateKey::from_der(pkcs8_key.private_key)?; pkcs1_key.parameters = Some(pkcs8_key.algorithm.parameters_oid()?.into()); pkcs1_key.try_into() diff --git a/sec1/tests/private_key.rs b/sec1/tests/private_key.rs index e163e0638..5b985da84 100644 --- a/sec1/tests/private_key.rs +++ b/sec1/tests/private_key.rs @@ -6,19 +6,12 @@ use der::asn1::ObjectIdentifier; use hex_literal::hex; use sec1::{EcParameters, EcPrivateKey}; -#[cfg(feature = "pem")] -use sec1::{der::Document, EcPrivateKeyDocument}; - /// NIST P-256 SEC1 private key encoded as ASN.1 DER. /// /// Note: this key is extracted from the corresponding `p256-priv.der` /// example key in the `pkcs8` crate. const P256_DER_EXAMPLE: &[u8] = include_bytes!("examples/p256-priv.der"); -/// NIST P-256 SEC1 private key encoded as PEM. -#[cfg(feature = "pem")] -const P256_PEM_EXAMPLE: &str = include_str!("examples/p256-priv.pem"); - #[test] fn decode_p256_der() { let key = EcPrivateKey::try_from(P256_DER_EXAMPLE).unwrap(); @@ -37,14 +30,3 @@ fn decode_p256_der() { ); assert_eq!(key.public_key, Some(hex!("041CACFFB55F2F2CEFD89D89EB374B2681152452802DEEA09916068137D839CF7FC481A44492304D7EF66AC117BEFE83A8D08F155F2B52F9F618DD447029048E0F").as_ref())); } - -#[cfg(feature = "pem")] -#[test] -fn decode_p256_pem() { - let sec1_doc: EcPrivateKeyDocument = P256_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(sec1_doc.as_ref(), P256_DER_EXAMPLE); - - // Ensure `EcPrivateKeyDocument` parses successfully - let pk = EcPrivateKey::try_from(P256_DER_EXAMPLE).unwrap(); - assert_eq!(sec1_doc.decode().private_key, pk.private_key); -} diff --git a/sec1/tests/traits.rs b/sec1/tests/traits.rs new file mode 100644 index 000000000..4bcd679b9 --- /dev/null +++ b/sec1/tests/traits.rs @@ -0,0 +1,100 @@ +//! Tests for SEC1 encoding/decoding traits. + +#![cfg(any(feature = "pem", feature = "std"))] + +use der::SecretDocument; +use sec1::{DecodeEcPrivateKey, EncodeEcPrivateKey, Result}; + +#[cfg(feature = "pem")] +use sec1::der::pem::LineEnding; + +#[cfg(feature = "std")] +use tempfile::tempdir; + +#[cfg(all(feature = "pem", feature = "std"))] +use std::fs; + +/// SEC1 `EcPrivateKey` encoded as ASN.1 DER +const P256_DER_EXAMPLE: &[u8] = include_bytes!("examples/p256-priv.der"); + +/// SEC1 `EcPrivateKey` encoded as PEM +#[cfg(feature = "pem")] +const P256_PEM_EXAMPLE: &str = include_str!("examples/p256-priv.pem"); + +/// Mock private key type for testing trait impls against. +pub struct MockPrivateKey(Vec); + +impl AsRef<[u8]> for MockPrivateKey { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl DecodeEcPrivateKey for MockPrivateKey { + fn from_sec1_der(bytes: &[u8]) -> Result { + Ok(MockPrivateKey(bytes.to_vec())) + } +} + +impl EncodeEcPrivateKey for MockPrivateKey { + fn to_sec1_der(&self) -> Result { + Ok(SecretDocument::try_from(self.as_ref())?) + } +} + +#[cfg(feature = "pem")] +#[test] +fn from_sec1_pem() { + let key = MockPrivateKey::from_sec1_pem(P256_PEM_EXAMPLE).unwrap(); + assert_eq!(key.as_ref(), P256_DER_EXAMPLE); +} + +#[cfg(feature = "std")] +#[test] +fn read_sec1_der_file() { + let key = MockPrivateKey::read_sec1_der_file("tests/examples/p256-priv.der").unwrap(); + assert_eq!(key.as_ref(), P256_DER_EXAMPLE); +} + +#[cfg(all(feature = "pem", feature = "std"))] +#[test] +fn read_sec1_pem_file() { + let key = MockPrivateKey::read_sec1_pem_file("tests/examples/p256-priv.pem").unwrap(); + assert_eq!(key.as_ref(), P256_DER_EXAMPLE); +} + +#[cfg(feature = "pem")] +#[test] +fn to_sec1_pem() { + let pem = MockPrivateKey(P256_DER_EXAMPLE.to_vec()) + .to_sec1_pem(LineEnding::LF) + .unwrap(); + + assert_eq!(&*pem, P256_PEM_EXAMPLE); +} + +#[cfg(feature = "std")] +#[test] +fn write_sec1_der_file() { + let dir = tempdir().unwrap(); + let path = dir.path().join("example.der"); + MockPrivateKey(P256_DER_EXAMPLE.to_vec()) + .write_sec1_der_file(&path) + .unwrap(); + + let key = MockPrivateKey::read_sec1_der_file(&path).unwrap(); + assert_eq!(key.as_ref(), P256_DER_EXAMPLE); +} + +#[cfg(all(feature = "pem", feature = "std"))] +#[test] +fn write_sec1_pem_file() { + let dir = tempdir().unwrap(); + let path = dir.path().join("example.pem"); + MockPrivateKey(P256_DER_EXAMPLE.to_vec()) + .write_sec1_pem_file(&path, LineEnding::LF) + .unwrap(); + + let pem = fs::read_to_string(path).unwrap(); + assert_eq!(&pem, P256_PEM_EXAMPLE); +} diff --git a/spki/Cargo.toml b/spki/Cargo.toml index ecf5aa8b5..0dd58bd84 100644 --- a/spki/Cargo.toml +++ b/spki/Cargo.toml @@ -23,6 +23,7 @@ base64ct = { version = "1", path = "../base64ct", optional = true, default-featu [dev-dependencies] hex-literal = "0.3" +tempfile = "3" [features] alloc = ["base64ct/alloc", "der/alloc"] diff --git a/spki/src/document.rs b/spki/src/document.rs deleted file mode 100644 index c092e5e39..000000000 --- a/spki/src/document.rs +++ /dev/null @@ -1,146 +0,0 @@ -//! SPKI public key document. - -use crate::{DecodePublicKey, EncodePublicKey, Error, Result, SubjectPublicKeyInfo}; -use alloc::vec::Vec; -use core::fmt; -use der::{Decode, Document}; - -#[cfg(feature = "std")] -use std::path::Path; - -#[cfg(feature = "pem")] -use { - alloc::string::String, - core::str::FromStr, - der::pem::{self, LineEnding}, -}; - -/// SPKI public key document. -/// -/// This type provides storage for [`SubjectPublicKeyInfo`] encoded as ASN.1 -/// DER with the invariant that the contained-document is "well-formed", i.e. -/// it will parse successfully according to this crate's parsing rules. -#[derive(Clone)] -#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] -pub struct PublicKeyDocument(Vec); - -impl<'a> Document<'a> for PublicKeyDocument { - type Message = SubjectPublicKeyInfo<'a>; - const SENSITIVE: bool = false; -} - -impl DecodePublicKey for PublicKeyDocument { - fn from_public_key_der(bytes: &[u8]) -> Result { - Ok(Self::from_der(bytes)?) - } - - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - fn from_public_key_pem(s: &str) -> Result { - Ok(Self::from_pem(s)?) - } - - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn read_public_key_der_file(path: impl AsRef) -> Result { - Ok(Self::read_der_file(path)?) - } - - #[cfg(all(feature = "pem", feature = "std"))] - #[cfg_attr(docsrs, doc(cfg(all(feature = "pem", feature = "std"))))] - fn read_public_key_pem_file(path: impl AsRef) -> Result { - Ok(Self::read_pem_file(path)?) - } -} - -impl EncodePublicKey for PublicKeyDocument { - fn to_public_key_der(&self) -> Result { - Ok(self.clone()) - } - - #[cfg(feature = "pem")] - #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] - fn to_public_key_pem(&self, line_ending: LineEnding) -> Result { - Ok(self.to_pem(line_ending)?) - } - - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - fn write_public_key_der_file(&self, path: impl AsRef) -> Result<()> { - Ok(self.write_der_file(path)?) - } - - #[cfg(all(feature = "pem", feature = "std"))] - #[cfg_attr(docsrs, doc(cfg(all(feature = "pem", feature = "std"))))] - fn write_public_key_pem_file( - &self, - path: impl AsRef, - line_ending: LineEnding, - ) -> Result<()> { - Ok(self.write_pem_file(path, line_ending)?) - } -} - -impl AsRef<[u8]> for PublicKeyDocument { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl TryFrom<&[u8]> for PublicKeyDocument { - type Error = Error; - - fn try_from(bytes: &[u8]) -> Result { - Ok(Self::from_der(bytes)?) - } -} - -impl TryFrom> for PublicKeyDocument { - type Error = Error; - - fn try_from(spki: SubjectPublicKeyInfo<'_>) -> Result { - Self::try_from(&spki) - } -} - -impl TryFrom<&SubjectPublicKeyInfo<'_>> for PublicKeyDocument { - type Error = Error; - - fn try_from(spki: &SubjectPublicKeyInfo<'_>) -> Result { - Ok(Self::from_msg(spki)?) - } -} - -impl TryFrom> for PublicKeyDocument { - type Error = der::Error; - - fn try_from(bytes: Vec) -> der::Result { - // Ensure document is well-formed - SubjectPublicKeyInfo::from_der(bytes.as_slice())?; - Ok(Self(bytes)) - } -} - -impl fmt::Debug for PublicKeyDocument { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_tuple("PublicKeyDocument") - .field(&self.decode()) - .finish() - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl FromStr for PublicKeyDocument { - type Err = Error; - - fn from_str(s: &str) -> Result { - Self::from_public_key_pem(s) - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl pem::PemLabel for PublicKeyDocument { - const PEM_LABEL: &'static str = "PUBLIC KEY"; -} diff --git a/spki/src/error.rs b/spki/src/error.rs index b19ad6f6c..9d05990f3 100644 --- a/spki/src/error.rs +++ b/spki/src/error.rs @@ -6,6 +6,9 @@ use der::asn1::ObjectIdentifier; /// Result type with `spki` crate's [`Error`] type. pub type Result = core::result::Result; +#[cfg(feature = "pem")] +use der::pem; + /// Error type #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[non_exhaustive] @@ -54,5 +57,12 @@ impl From for Error { } } +#[cfg(feature = "pem")] +impl From for Error { + fn from(err: pem::Error) -> Error { + der::Error::from(err).into() + } +} + #[cfg(feature = "std")] impl std::error::Error for Error {} diff --git a/spki/src/lib.rs b/spki/src/lib.rs index 19d1e9dc9..d76bc10ce 100644 --- a/spki/src/lib.rs +++ b/spki/src/lib.rs @@ -28,6 +28,7 @@ //! ``` #[cfg(feature = "alloc")] +#[allow(unused_extern_crates)] extern crate alloc; #[cfg(feature = "std")] extern crate std; @@ -37,9 +38,6 @@ mod error; mod spki; mod traits; -#[cfg(feature = "alloc")] -mod document; - pub use crate::{ algorithm::AlgorithmIdentifier, error::{Error, Result}, @@ -49,4 +47,4 @@ pub use crate::{ pub use der::{self, asn1::ObjectIdentifier}; #[cfg(feature = "alloc")] -pub use crate::{document::PublicKeyDocument, traits::EncodePublicKey}; +pub use {crate::traits::EncodePublicKey, der::Document}; diff --git a/spki/src/spki.rs b/spki/src/spki.rs index 80c9494a0..e8bcf56d7 100644 --- a/spki/src/spki.rs +++ b/spki/src/spki.rs @@ -4,6 +4,9 @@ use crate::{AlgorithmIdentifier, Error, Result}; use core::cmp::Ordering; use der::{asn1::BitString, Decode, Decoder, DerOrd, Encode, Sequence, ValueOrd}; +#[cfg(feature = "alloc")] +use der::Document; + #[cfg(feature = "fingerprint")] use sha2::{digest, Digest, Sha256}; @@ -13,6 +16,9 @@ use { base64ct::{Base64, Encoding}, }; +#[cfg(feature = "pem")] +use der::pem::PemLabel; + /// X.509 `SubjectPublicKeyInfo` (SPKI) as defined in [RFC 5280 Section 4.1.2.7]. /// /// ASN.1 structure containing an [`AlgorithmIdentifier`] and public key @@ -101,3 +107,29 @@ impl ValueOrd for SubjectPublicKeyInfo<'_> { } } } + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl TryFrom> for Document { + type Error = Error; + + fn try_from(spki: SubjectPublicKeyInfo<'_>) -> Result { + Self::try_from(&spki) + } +} + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +impl TryFrom<&SubjectPublicKeyInfo<'_>> for Document { + type Error = Error; + + fn try_from(spki: &SubjectPublicKeyInfo<'_>) -> Result { + Ok(Self::encode_msg(spki)?) + } +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl PemLabel for SubjectPublicKeyInfo<'_> { + const PEM_LABEL: &'static str = "PUBLIC KEY"; +} diff --git a/spki/src/traits.rs b/spki/src/traits.rs index e63a43961..c16e3974d 100644 --- a/spki/src/traits.rs +++ b/spki/src/traits.rs @@ -3,10 +3,13 @@ use crate::{Error, Result, SubjectPublicKeyInfo}; #[cfg(feature = "alloc")] -use {crate::PublicKeyDocument, der::Document}; +use der::Document; #[cfg(feature = "pem")] -use {alloc::string::String, der::pem::LineEnding}; +use { + alloc::string::String, + der::pem::{LineEnding, PemLabel}, +}; #[cfg(feature = "std")] use std::path::Path; @@ -21,13 +24,6 @@ pub trait DecodePublicKey: Self::try_from(SubjectPublicKeyInfo::try_from(bytes)?) } - /// Deserialize SPKI public key from a [`PublicKeyDocument`]. - #[cfg(feature = "alloc")] - #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] - fn from_public_key_doc(doc: &PublicKeyDocument) -> Result { - Self::try_from(doc.decode()) - } - /// Deserialize PEM-encoded [`SubjectPublicKeyInfo`]. /// /// Keys in this format begin with the following delimiter: @@ -38,7 +34,9 @@ pub trait DecodePublicKey: #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] fn from_public_key_pem(s: &str) -> Result { - PublicKeyDocument::from_public_key_pem(s).and_then(|doc| Self::from_public_key_doc(&doc)) + let (label, doc) = Document::from_pem(s)?; + SubjectPublicKeyInfo::validate_pem_label(label)?; + Self::from_public_key_der(doc.as_bytes()) } /// Load public key object from an ASN.1 DER-encoded file on the local @@ -46,16 +44,17 @@ pub trait DecodePublicKey: #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn read_public_key_der_file(path: impl AsRef) -> Result { - PublicKeyDocument::read_public_key_der_file(path) - .and_then(|doc| Self::from_public_key_doc(&doc)) + let doc = Document::read_der_file(path)?; + Self::from_public_key_der(doc.as_bytes()) } /// Load public key object from a PEM-encoded file on the local filesystem. #[cfg(all(feature = "pem", feature = "std"))] #[cfg_attr(docsrs, doc(cfg(all(feature = "pem", feature = "std"))))] fn read_public_key_pem_file(path: impl AsRef) -> Result { - PublicKeyDocument::read_public_key_pem_file(path) - .and_then(|doc| Self::from_public_key_doc(&doc)) + let (label, doc) = Document::read_pem_file(path)?; + SubjectPublicKeyInfo::validate_pem_label(&label)?; + Self::from_public_key_der(doc.as_bytes()) } } @@ -63,21 +62,22 @@ pub trait DecodePublicKey: #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] pub trait EncodePublicKey { - /// Serialize a [`PublicKeyDocument`] containing a SPKI-encoded public key. - fn to_public_key_der(&self) -> Result; + /// Serialize a [`Document`] containing a SPKI-encoded public key. + fn to_public_key_der(&self) -> Result; /// Serialize this public key as PEM-encoded SPKI with the given [`LineEnding`]. #[cfg(feature = "pem")] #[cfg_attr(docsrs, doc(cfg(feature = "pem")))] fn to_public_key_pem(&self, line_ending: LineEnding) -> Result { - self.to_public_key_der()?.to_public_key_pem(line_ending) + let doc = self.to_public_key_der()?; + Ok(doc.to_pem(SubjectPublicKeyInfo::PEM_LABEL, line_ending)?) } /// Write ASN.1 DER-encoded public key to the given path #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] fn write_public_key_der_file(&self, path: impl AsRef) -> Result<()> { - self.to_public_key_der()?.write_public_key_der_file(path) + Ok(self.to_public_key_der()?.write_der_file(path)?) } /// Write ASN.1 DER-encoded public key to the given path @@ -88,7 +88,7 @@ pub trait EncodePublicKey { path: impl AsRef, line_ending: LineEnding, ) -> Result<()> { - self.to_public_key_der()? - .write_public_key_pem_file(path, line_ending) + let doc = self.to_public_key_der()?; + Ok(doc.write_pem_file(path, SubjectPublicKeyInfo::PEM_LABEL, line_ending)?) } } diff --git a/spki/tests/examples/p256-pub.der b/spki/tests/examples/p256-pub.der new file mode 100644 index 000000000..67c719c76 Binary files /dev/null and b/spki/tests/examples/p256-pub.der differ diff --git a/spki/tests/examples/p256-pub.pem b/spki/tests/examples/p256-pub.pem new file mode 100644 index 000000000..ee7e5b612 --- /dev/null +++ b/spki/tests/examples/p256-pub.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHKz/tV8vLO/YnYnrN0smgRUkUoAt +7qCZFgaBN9g5z3/EgaREkjBNfvZqwRe+/oOo0I8VXytS+fYY3URwKQSODw== +-----END PUBLIC KEY----- diff --git a/spki/tests/examples/rsa2048-pub.der b/spki/tests/examples/rsa2048-pub.der new file mode 100644 index 000000000..4148aaaaa Binary files /dev/null and b/spki/tests/examples/rsa2048-pub.der differ diff --git a/spki/tests/examples/rsa2048-pub.pem b/spki/tests/examples/rsa2048-pub.pem new file mode 100644 index 000000000..5ecd89239 --- /dev/null +++ b/spki/tests/examples/rsa2048-pub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtsQsUV8QpqrygsY+2+JC +Q6Fw8/omM71IM2N/R8pPbzbgOl0p78MZGsgPOQ2HSznjD0FPzsH8oO2B5Uftws04 +LHb2HJAYlz25+lN5cqfHAfa3fgmC38FfwBkn7l582UtPWZ/wcBOnyCgb3yLcvJrX +yrt8QxHJgvWO23ITrUVYszImbXQ67YGS0YhMrbixRzmo2tpm3JcIBtnHrEUMsT0N +fFdfsZhTT8YbxBvA8FdODgEwx7u/vf3J9qbi4+Kv8cvqyJuleIRSjVXPsIMnoejI +n04APPKIjpMyQdnWlby7rNyQtE4+CV+jcFjqJbE/Xilcvqxt6DirjFCvYeKYl1uH +LwIDAQAB +-----END PUBLIC KEY----- diff --git a/spki/tests/spki.rs b/spki/tests/spki.rs index bf424fa55..ec51bd987 100644 --- a/spki/tests/spki.rs +++ b/spki/tests/spki.rs @@ -1,23 +1,36 @@ //! `SubjectPublicKeyInfo` tests. -#[cfg(all(feature = "alloc", feature = "fingerprint"))] -use spki::der::Encode; +use hex_literal::hex; +use spki::SubjectPublicKeyInfo; -#[cfg(feature = "fingerprint")] -use {hex_literal::hex, spki::SubjectPublicKeyInfo}; +#[cfg(feature = "alloc")] +use der::Encode; -#[cfg(all(feature = "pem", feature = "fingerprint"))] -use spki::{der::Document, EncodePublicKey, PublicKeyDocument}; +#[cfg(feature = "pem")] +use der::{pem::LineEnding, EncodePem}; + +/// Elliptic Curve (P-256) `SubjectPublicKeyInfo` encoded as ASN.1 DER +const EC_P256_DER_EXAMPLE: &[u8] = include_bytes!("examples/p256-pub.der"); -#[cfg(feature = "fingerprint")] -// Taken from pkcs8/tests/public_key.rs /// Ed25519 `SubjectPublicKeyInfo` encoded as ASN.1 DER +#[cfg(any(feature = "alloc", feature = "fingerprint"))] const ED25519_DER_EXAMPLE: &[u8] = include_bytes!("examples/ed25519-pub.der"); +/// RSA-2048 `SubjectPublicKeyInfo` encoded as ASN.1 DER +const RSA_2048_DER_EXAMPLE: &[u8] = include_bytes!("examples/rsa2048-pub.der"); + +/// Elliptic Curve (P-256) public key encoded as PEM +#[cfg(feature = "pem")] +const EC_P256_PEM_EXAMPLE: &str = include_str!("examples/p256-pub.pem"); + /// Ed25519 public key encoded as PEM -#[cfg(all(feature = "pem", feature = "fingerprint"))] +#[cfg(feature = "pem")] const ED25519_PEM_EXAMPLE: &str = include_str!("examples/ed25519-pub.pem"); +/// RSA-2048 PKCS#8 public key encoded as PEM +#[cfg(feature = "pem")] +const RSA_2048_PEM_EXAMPLE: &str = include_str!("examples/rsa2048-pub.pem"); + /// The SPKI fingerprint for `ED25519_SPKI_FINGERPRINT` as a Base64 string /// /// Generated using `cat ed25519-pub.der | openssl dgst -binary -sha256 | base64` @@ -32,8 +45,22 @@ const ED25519_SPKI_FINGERPRINT: &[u8] = &hex!("55dd4c74b0e48534e2f4e173ceceb50df8f27a7ac2aa8991cc7ae914e030bced"); #[test] -#[cfg(all(feature = "fingerprint", feature = "alloc"))] -fn decode_and_base64fingerprint_spki() { +fn decode_ec_p256_der() { + let spki = SubjectPublicKeyInfo::try_from(EC_P256_DER_EXAMPLE).unwrap(); + + assert_eq!(spki.algorithm.oid, "1.2.840.10045.2.1".parse().unwrap()); + + assert_eq!( + spki.algorithm.parameters.unwrap().oid().unwrap(), + "1.2.840.10045.3.1.7".parse().unwrap() + ); + + assert_eq!(spki.subject_public_key, &hex!("041CACFFB55F2F2CEFD89D89EB374B2681152452802DEEA09916068137D839CF7FC481A44492304D7EF66AC117BEFE83A8D08F155F2B52F9F618DD447029048E0F")[..]); +} + +#[test] +#[cfg(feature = "fingerprint")] +fn decode_ed25519_and_fingerprint_spki() { // Repeat the decode test from the pkcs8 crate let spki = SubjectPublicKeyInfo::try_from(ED25519_DER_EXAMPLE).unwrap(); @@ -46,14 +73,14 @@ fn decode_and_base64fingerprint_spki() { // Check the fingerprint assert_eq!( - spki.fingerprint_base64().unwrap(), - ED25519_SPKI_FINGERPRINT_BASE64 + spki.fingerprint().unwrap().as_slice(), + ED25519_SPKI_FINGERPRINT ); } #[test] -#[cfg(feature = "fingerprint")] -fn decode_and_fingerprint_spki() { +#[cfg(all(feature = "fingerprint", feature = "alloc"))] +fn decode_ed25519_and_fingerprint_base64() { // Repeat the decode test from the pkcs8 crate let spki = SubjectPublicKeyInfo::try_from(ED25519_DER_EXAMPLE).unwrap(); @@ -66,24 +93,30 @@ fn decode_and_fingerprint_spki() { // Check the fingerprint assert_eq!( - spki.fingerprint().unwrap().as_slice(), - ED25519_SPKI_FINGERPRINT + spki.fingerprint_base64().unwrap(), + ED25519_SPKI_FINGERPRINT_BASE64 ); } #[test] -#[cfg(all(feature = "pem", feature = "fingerprint"))] -fn decode_ed25519_pem() { - let doc: PublicKeyDocument = ED25519_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(doc.as_ref(), ED25519_DER_EXAMPLE); +fn decode_rsa_2048_der() { + let spki = SubjectPublicKeyInfo::try_from(RSA_2048_DER_EXAMPLE).unwrap(); - // Ensure `PublicKeyDocument` parses successfully - let spki = SubjectPublicKeyInfo::try_from(ED25519_DER_EXAMPLE).unwrap(); - assert_eq!(doc.decode(), spki); + assert_eq!(spki.algorithm.oid, "1.2.840.113549.1.1.1".parse().unwrap()); + assert!(spki.algorithm.parameters.unwrap().is_null()); + assert_eq!(spki.subject_public_key, &hex!("3082010A0282010100B6C42C515F10A6AAF282C63EDBE24243A170F3FA2633BD4833637F47CA4F6F36E03A5D29EFC3191AC80F390D874B39E30F414FCEC1FCA0ED81E547EDC2CD382C76F61C9018973DB9FA537972A7C701F6B77E0982DFC15FC01927EE5E7CD94B4F599FF07013A7C8281BDF22DCBC9AD7CABB7C4311C982F58EDB7213AD4558B332266D743AED8192D1884CADB8B14739A8DADA66DC970806D9C7AC450CB13D0D7C575FB198534FC61BC41BC0F0574E0E0130C7BBBFBDFDC9F6A6E2E3E2AFF1CBEAC89BA57884528D55CFB08327A1E8C89F4E003CF2888E933241D9D695BCBBACDC90B44E3E095FA37058EA25B13F5E295CBEAC6DE838AB8C50AF61E298975B872F0203010001")[..]); } #[test] -#[cfg(all(feature = "alloc", feature = "fingerprint"))] +#[cfg(feature = "alloc")] +fn encode_ec_p256_der() { + let pk = SubjectPublicKeyInfo::try_from(EC_P256_DER_EXAMPLE).unwrap(); + let pk_encoded = pk.to_vec().unwrap(); + assert_eq!(EC_P256_DER_EXAMPLE, pk_encoded.as_slice()); +} + +#[test] +#[cfg(feature = "alloc")] fn encode_ed25519_der() { let pk = SubjectPublicKeyInfo::try_from(ED25519_DER_EXAMPLE).unwrap(); let pk_encoded = pk.to_vec().unwrap(); @@ -91,13 +124,33 @@ fn encode_ed25519_der() { } #[test] -#[cfg(all(feature = "pem", feature = "fingerprint"))] +#[cfg(feature = "alloc")] +fn encode_rsa_2048_der() { + let pk = SubjectPublicKeyInfo::try_from(RSA_2048_DER_EXAMPLE).unwrap(); + let pk_encoded = pk.to_vec().unwrap(); + assert_eq!(RSA_2048_DER_EXAMPLE, pk_encoded.as_slice()); +} + +#[test] +#[cfg(feature = "pem")] +fn encode_ec_p256_pem() { + let pk = SubjectPublicKeyInfo::try_from(EC_P256_DER_EXAMPLE).unwrap(); + let pk_encoded = pk.to_pem(LineEnding::LF).unwrap(); + assert_eq!(EC_P256_PEM_EXAMPLE, pk_encoded); +} + +#[test] +#[cfg(feature = "pem")] fn encode_ed25519_pem() { let pk = SubjectPublicKeyInfo::try_from(ED25519_DER_EXAMPLE).unwrap(); - let pk_encoded = PublicKeyDocument::try_from(pk) - .unwrap() - .to_public_key_pem(Default::default()) - .unwrap(); - + let pk_encoded = pk.to_pem(LineEnding::LF).unwrap(); assert_eq!(ED25519_PEM_EXAMPLE, pk_encoded); } + +#[test] +#[cfg(feature = "pem")] +fn encode_rsa_2048_pem() { + let pk = SubjectPublicKeyInfo::try_from(RSA_2048_DER_EXAMPLE).unwrap(); + let pk_encoded = pk.to_pem(LineEnding::LF).unwrap(); + assert_eq!(RSA_2048_PEM_EXAMPLE, pk_encoded); +} diff --git a/spki/tests/traits.rs b/spki/tests/traits.rs new file mode 100644 index 000000000..597399e44 --- /dev/null +++ b/spki/tests/traits.rs @@ -0,0 +1,108 @@ +//! Tests for SPKI encoding/decoding traits. + +#![cfg(any(feature = "pem", feature = "std"))] + +use der::{Decode, Encode}; +use spki::{DecodePublicKey, Document, EncodePublicKey, Error, Result, SubjectPublicKeyInfo}; + +#[cfg(feature = "pem")] +use spki::der::pem::LineEnding; + +#[cfg(feature = "std")] +use tempfile::tempdir; + +#[cfg(all(feature = "pem", feature = "std"))] +use std::fs; + +/// Ed25519 `SubjectPublicKeyInfo` encoded as ASN.1 DER +const ED25519_DER_EXAMPLE: &[u8] = include_bytes!("examples/ed25519-pub.der"); + +/// Ed25519 public key encoded as PEM +#[cfg(feature = "pem")] +const ED25519_PEM_EXAMPLE: &str = include_str!("examples/ed25519-pub.pem"); + +/// Mock key type for testing trait impls against. +pub struct MockKey(Vec); + +impl AsRef<[u8]> for MockKey { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl DecodePublicKey for MockKey { + fn from_public_key_der(bytes: &[u8]) -> Result { + Ok(MockKey(bytes.to_vec())) + } +} + +impl EncodePublicKey for MockKey { + fn to_public_key_der(&self) -> Result { + Ok(Document::from_der(self.as_ref())?) + } +} + +impl TryFrom> for MockKey { + type Error = Error; + + fn try_from(spki: SubjectPublicKeyInfo<'_>) -> Result { + Ok(MockKey(spki.to_vec()?)) + } +} + +#[cfg(feature = "pem")] +#[test] +fn from_public_key_pem() { + let key = MockKey::from_public_key_pem(ED25519_PEM_EXAMPLE).unwrap(); + assert_eq!(key.as_ref(), ED25519_DER_EXAMPLE); +} + +#[cfg(feature = "std")] +#[test] +fn read_public_key_der_file() { + let key = MockKey::read_public_key_der_file("tests/examples/ed25519-pub.der").unwrap(); + assert_eq!(key.as_ref(), ED25519_DER_EXAMPLE); +} + +#[cfg(all(feature = "pem", feature = "std"))] +#[test] +fn read_public_key_pem_file() { + let key = MockKey::read_public_key_pem_file("tests/examples/ed25519-pub.pem").unwrap(); + assert_eq!(key.as_ref(), ED25519_DER_EXAMPLE); +} + +#[cfg(feature = "pem")] +#[test] +fn to_public_key_pem() { + let pem = MockKey(ED25519_DER_EXAMPLE.to_vec()) + .to_public_key_pem(LineEnding::LF) + .unwrap(); + + assert_eq!(pem, ED25519_PEM_EXAMPLE); +} + +#[cfg(feature = "std")] +#[test] +fn write_public_key_der_file() { + let dir = tempdir().unwrap(); + let path = dir.path().join("example.der"); + MockKey(ED25519_DER_EXAMPLE.to_vec()) + .write_public_key_der_file(&path) + .unwrap(); + + let key = MockKey::read_public_key_der_file(&path).unwrap(); + assert_eq!(key.as_ref(), ED25519_DER_EXAMPLE); +} + +#[cfg(all(feature = "pem", feature = "std"))] +#[test] +fn write_public_key_pem_file() { + let dir = tempdir().unwrap(); + let path = dir.path().join("example.pem"); + MockKey(ED25519_DER_EXAMPLE.to_vec()) + .write_public_key_pem_file(&path, LineEnding::LF) + .unwrap(); + + let pem = fs::read_to_string(path).unwrap(); + assert_eq!(&pem, ED25519_PEM_EXAMPLE); +} diff --git a/ssh-key/src/error.rs b/ssh-key/src/error.rs index 7053f7b56..cce158e76 100644 --- a/ssh-key/src/error.rs +++ b/ssh-key/src/error.rs @@ -1,7 +1,7 @@ //! Error types +use crate::pem; use core::fmt; -use pem_rfc7468 as pem; /// Result type with `ssh-key`'s [`Error`] as the error type. pub type Result = core::result::Result; diff --git a/ssh-key/src/lib.rs b/ssh-key/src/lib.rs index ea189352d..9d79845c8 100644 --- a/ssh-key/src/lib.rs +++ b/ssh-key/src/lib.rs @@ -151,6 +151,7 @@ pub use crate::{ public::PublicKey, }; pub use base64ct::LineEnding; +pub use pem_rfc7468 as pem; #[cfg(feature = "alloc")] pub use crate::{certificate::Certificate, mpint::MPInt}; diff --git a/ssh-key/src/private.rs b/ssh-key/src/private.rs index 3551ded13..58c37d66c 100644 --- a/ssh-key/src/private.rs +++ b/ssh-key/src/private.rs @@ -123,10 +123,10 @@ use crate::{ checked::CheckedSum, decoder::{Decode, Decoder}, encoder::{Encode, Encoder}, + pem::{self, LineEnding, PemLabel}, public, Algorithm, Cipher, Error, Kdf, PublicKey, Result, }; use core::str; -use pem_rfc7468::{self as pem, LineEnding, PemLabel}; #[cfg(feature = "alloc")] use {crate::encoder::base64_encoded_len, alloc::string::String, zeroize::Zeroizing}; diff --git a/x509/src/certificate.rs b/x509/src/certificate.rs index eb94c9e44..1e65a99cf 100644 --- a/x509/src/certificate.rs +++ b/x509/src/certificate.rs @@ -9,8 +9,6 @@ use der::asn1::{BitString, UIntBytes}; use der::{Decode, Enumerated, Error, ErrorKind, Newtype, Sequence}; use spki::{AlgorithmIdentifier, SubjectPublicKeyInfo}; -pub mod document; - /// Certificate `Version` as defined in [RFC 5280 Section 4.1]. /// /// ```text diff --git a/x509/src/certificate/document.rs b/x509/src/certificate/document.rs deleted file mode 100644 index 3ab67e781..000000000 --- a/x509/src/certificate/document.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! CertificateDocument implementation - -use crate::Certificate; -use der::{Error, Result}; - -use alloc::vec::Vec; -use core::fmt; -use der::{Decode, Document}; - -#[cfg(feature = "pem")] -use {core::str::FromStr, der::pem}; - -/// Certificate document. -/// -/// This type provides storage for [`Certificate`] encoded as ASN.1 -/// DER with the invariant that the contained-document is "well-formed", i.e. -/// it will parse successfully according to this crate's parsing rules. -#[derive(Clone)] -#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] -pub struct CertificateDocument(Vec); - -impl<'a> TryFrom<&'a [u8]> for Certificate<'a> { - type Error = Error; - - fn try_from(bytes: &'a [u8]) -> Result { - Self::from_der(bytes) - } -} - -impl<'a> Document<'a> for CertificateDocument { - type Message = Certificate<'a>; - const SENSITIVE: bool = false; -} - -impl AsRef<[u8]> for CertificateDocument { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl TryFrom<&[u8]> for CertificateDocument { - type Error = Error; - - fn try_from(bytes: &[u8]) -> Result { - Self::from_der(bytes) - } -} - -impl TryFrom> for CertificateDocument { - type Error = Error; - - fn try_from(cert: Certificate<'_>) -> Result { - Self::try_from(&cert) - } -} - -impl TryFrom<&Certificate<'_>> for CertificateDocument { - type Error = Error; - - fn try_from(cert: &Certificate<'_>) -> Result { - Self::from_msg(cert) - } -} - -impl TryFrom> for CertificateDocument { - type Error = Error; - - fn try_from(bytes: Vec) -> Result { - // Ensure document is well-formed - Certificate::from_der(bytes.as_slice())?; - Ok(Self(bytes)) - } -} - -impl fmt::Debug for CertificateDocument { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_tuple("CertificateDocument") - .field(&self.decode()) - .finish() - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl FromStr for CertificateDocument { - type Err = Error; - - fn from_str(s: &str) -> Result { - Self::from_pem(s) - } -} - -#[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl pem::PemLabel for CertificateDocument { - const PEM_LABEL: &'static str = "CERTIFICATE"; -} diff --git a/x509/tests/document.rs b/x509/tests/document.rs deleted file mode 100644 index 744b4a970..000000000 --- a/x509/tests/document.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Certificate document tests -use der::Document; -use x509_cert::certificate::document::CertificateDocument; - -#[cfg(all(feature = "pem", any(feature = "alloc", feature = "std")))] -use der::Encode; - -use x509_cert::*; - -#[cfg(feature = "std")] -use std::path::Path; - -#[cfg(feature = "pem")] -use der::pem::LineEnding; - -/// `Certificate` encoded as ASN.1 DER -const CERT_DER_EXAMPLE: &[u8] = include_bytes!("examples/amazon.der"); - -/// `Certificate` encoded as PEM -#[cfg(all(feature = "pem"))] -const CERT_PEM_EXAMPLE: &str = include_str!("examples/amazon.pem"); - -#[test] -#[cfg(all(feature = "pem", feature = "std"))] -fn decode_cert_pem_file() { - let doc: CertificateDocument = - CertificateDocument::read_pem_file(Path::new("tests/examples/amazon.pem")).unwrap(); - assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); -} - -#[test] -#[cfg(all(feature = "std", feature = "alloc"))] -fn decode_cert_der_file() { - use x509_cert::certificate::document::CertificateDocument; - let doc: CertificateDocument = - CertificateDocument::read_der_file(Path::new("tests/examples/amazon.der")).unwrap(); - assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); -} - -#[test] -#[cfg(all(feature = "pem", any(feature = "alloc", feature = "std")))] -fn decode_cert_pem() { - let doc: CertificateDocument = CERT_PEM_EXAMPLE.parse().unwrap(); - assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); - - // Ensure `CertificateDocument` parses successfully - let cert = Certificate::try_from(CERT_DER_EXAMPLE).unwrap(); - assert_eq!(doc.decode(), cert); - assert_eq!(doc.to_pem(LineEnding::default()).unwrap(), CERT_PEM_EXAMPLE); - - let doc: CertificateDocument = CertificateDocument::from_pem(CERT_PEM_EXAMPLE).unwrap(); - assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); - - // Ensure `CertificateDocument` parses successfully - let cert = Certificate::try_from(CERT_DER_EXAMPLE).unwrap(); - assert_eq!(doc.decode(), cert); - assert_eq!(doc.to_pem(LineEnding::default()).unwrap(), CERT_PEM_EXAMPLE); -} - -#[test] -fn decode_cert_der() { - let doc: CertificateDocument = CertificateDocument::from_der(CERT_DER_EXAMPLE).unwrap(); - assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); - - // Ensure `CertificateDocument` parses successfully - let cert = Certificate::try_from(CERT_DER_EXAMPLE).unwrap(); - assert_eq!(doc.decode(), cert); -} - -#[test] -#[cfg(all(feature = "pem", any(feature = "alloc", feature = "std")))] -fn encode_cert_der() { - let pk = Certificate::try_from(CERT_DER_EXAMPLE).unwrap(); - let pk_encoded = pk.to_vec().unwrap(); - assert_eq!(CERT_DER_EXAMPLE, pk_encoded.as_slice()); -} - -#[test] -#[cfg(feature = "std")] -fn write_cert_der() { - let doc: CertificateDocument = CertificateDocument::from_der(CERT_DER_EXAMPLE).unwrap(); - assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); - assert_eq!(doc.to_der().as_ref(), CERT_DER_EXAMPLE); - - let r = doc.write_der_file(Path::new("tests/examples/amazon.der.regen")); - if r.is_err() { - panic!("Failed to write file") - } - - let doc: CertificateDocument = - CertificateDocument::read_der_file(Path::new("tests/examples/amazon.der.regen")).unwrap(); - assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); - assert_eq!(doc.to_der().as_ref(), CERT_DER_EXAMPLE); - let r = std::fs::remove_file("tests/examples/amazon.der.regen"); - if r.is_err() {} -} - -#[test] -#[cfg(all(feature = "pem", any(feature = "alloc", feature = "std")))] -fn encode_cert_pem() { - let pk = Certificate::try_from(CERT_DER_EXAMPLE).unwrap(); - let pk_encoded = CertificateDocument::try_from(pk) - .unwrap() - .to_pem(Default::default()) - .unwrap(); - - assert_eq!(CERT_PEM_EXAMPLE, pk_encoded); -} - -#[test] -#[cfg(all(feature = "std", feature = "pem"))] -fn write_cert_pem() { - let doc: CertificateDocument = CertificateDocument::from_der(CERT_DER_EXAMPLE).unwrap(); - assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); - assert_eq!(doc.to_der().as_ref(), CERT_DER_EXAMPLE); - - let r = doc.write_pem_file( - Path::new("tests/examples/amazon.pem.regen"), - LineEnding::default(), - ); - if r.is_err() { - panic!("Failed to write file") - } - - let doc: CertificateDocument = - CertificateDocument::read_pem_file(Path::new("tests/examples/amazon.pem.regen")).unwrap(); - assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); - assert_eq!(doc.to_der().as_ref(), CERT_DER_EXAMPLE); - let r = std::fs::remove_file("tests/examples/amazon.pem.regen"); - if r.is_err() {} -}