From 7693dfb5bfd2fc23afd2303a9b61ad836a9e45d0 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Sun, 20 Nov 2022 17:50:25 -0700 Subject: [PATCH] PKCS#8 support Adds optional integration with `ed25519::pkcs8` with support for decoding/encoding `Keypair` from/to PKCS#8-encoded documents as well as `PublicKey` from/to SPKI-encoded documents. Includes test vectors generated for the `ed25519` crate from: https://github.com/RustCrypto/signatures/tree/master/ed25519/tests/examples --- .github/workflows/rust.yml | 4 +- Cargo.toml | 11 +++-- README.md | 2 +- src/keypair.rs | 80 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 43 +++++++++++++++++++ src/public.rs | 62 ++++++++++++++++++++++++++++ tests/examples/pkcs8-v1.der | Bin 0 -> 48 bytes tests/examples/pkcs8-v2.der | Bin 0 -> 116 bytes tests/examples/pubkey.der | Bin 0 -> 44 bytes tests/pkcs8.rs | 74 +++++++++++++++++++++++++++++++++ 10 files changed, 270 insertions(+), 6 deletions(-) create mode 100644 tests/examples/pkcs8-v1.der create mode 100644 tests/examples/pkcs8-v2.der create mode 100644 tests/examples/pubkey.der create mode 100644 tests/pkcs8.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 770193af..6abb7377 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -46,7 +46,7 @@ jobs: run: cargo build --target x86_64-unknown-linux-gnu msrv: - name: Current MSRV is 1.56.1 + name: Current MSRV is 1.57.0 runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -56,7 +56,7 @@ jobs: - run: cargo -Z minimal-versions check --no-default-features --features serde # Now check that `cargo build` works with respect to the oldest possible # deps and the stated MSRV - - uses: dtolnay/rust-toolchain@1.56.1 + - uses: dtolnay/rust-toolchain@1.57.0 - run: cargo build bench: diff --git a/Cargo.toml b/Cargo.toml index 31cfe1f7..f87f4302 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["cryptography", "ed25519", "curve25519", "signature", "ECC"] categories = ["cryptography", "no-std"] description = "Fast and efficient ed25519 EdDSA key generations, signing, and verification in pure Rust." exclude = [ ".gitignore", "TESTVECTORS", "res/*" ] +rust-version = "1.57" [badges] travis-ci = { repository = "dalek-cryptography/ed25519-dalek", branch = "master"} @@ -19,7 +20,8 @@ travis-ci = { repository = "dalek-cryptography/ed25519-dalek", branch = "master" [package.metadata.docs.rs] # Disabled for now since this is borked; tracking https://github.com/rust-lang/docs.rs/issues/302 # rustdoc-args = ["--html-in-header", ".cargo/registry/src/github.com-1ecc6299db9ec823/curve25519-dalek-0.13.2/rustdoc-include-katex-header.html"] -features = ["nightly", "batch"] +rustdoc-args = ["--cfg", "docsrs"] +features = ["nightly", "batch", "pkcs8"] [dependencies] curve25519-dalek = { version = "=4.0.0-pre.2", default-features = false, features = ["digest", "rand_core"] } @@ -37,6 +39,7 @@ hex = "^0.4" bincode = "1.0" serde_json = "1.0" criterion = "0.3" +hex-literal = "0.3" rand = "0.8" serde_crate = { package = "serde", version = "1.0", features = ["derive"] } toml = { version = "0.5" } @@ -49,7 +52,7 @@ required-features = ["batch"] [features] default = ["std", "rand"] std = ["alloc", "ed25519/std", "serde_crate/std", "sha2/std", "rand/std"] -alloc = ["curve25519-dalek/alloc", "rand/alloc", "zeroize/alloc"] +alloc = ["curve25519-dalek/alloc", "ed25519/alloc", "rand/alloc", "zeroize/alloc"] serde = ["serde_crate", "serde_bytes", "ed25519/serde"] batch = ["alloc", "merlin", "rand/std"] # This feature enables deterministic batch verification. @@ -57,7 +60,9 @@ batch_deterministic = ["alloc", "merlin", "rand", "rand_core"] asm = ["sha2/asm"] # This features turns off stricter checking for scalar malleability in signatures legacy_compatibility = [] +pkcs8 = ["ed25519/pkcs8"] +pem = ["alloc", "ed25519/pem", "pkcs8"] [patch.crates-io] curve25519-dalek = { git = "https://github.com/dalek-cryptography/curve25519-dalek.git", branch = "release/4.0" } -ed25519 = { git = "https://github.com/RustCrypto/signatures.git"} +ed25519 = { git = "https://github.com/RustCrypto/signatures.git" } diff --git a/README.md b/README.md index 29af18e1..42ce8234 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ version = "1" # Minimum Supported Rust Version -This crate requires Rust 1.56.1 at a minimum. 1.x releases of this crate supported an MSRV of 1.41. +This crate requires Rust 1.57.0 at a minimum. 1.x releases of this crate supported an MSRV of 1.41. In the future, MSRV changes will be accompanied by a minor version bump. diff --git a/src/keypair.rs b/src/keypair.rs index 592486ca..5e442534 100644 --- a/src/keypair.rs +++ b/src/keypair.rs @@ -9,6 +9,9 @@ //! ed25519 keypairs. +#[cfg(feature = "pkcs8")] +use ed25519::pkcs8::{self, DecodePrivateKey}; + #[cfg(feature = "rand")] use rand::{CryptoRng, RngCore}; @@ -431,6 +434,83 @@ impl Verifier for Keypair { } } +impl TryFrom<&[u8]> for Keypair { + type Error = SignatureError; + + fn try_from(bytes: &[u8]) -> Result { + Keypair::from_bytes(bytes) + } +} + +#[cfg(feature = "pkcs8")] +impl DecodePrivateKey for Keypair {} + +#[cfg(all(feature = "alloc", feature = "pkcs8"))] +impl pkcs8::EncodePrivateKey for Keypair { + fn to_pkcs8_der(&self) -> pkcs8::Result { + pkcs8::KeypairBytes::from(self).to_pkcs8_der() + } +} + +#[cfg(feature = "pkcs8")] +impl TryFrom for Keypair { + type Error = pkcs8::Error; + + fn try_from(pkcs8_key: pkcs8::KeypairBytes) -> pkcs8::Result { + Keypair::try_from(&pkcs8_key) + } +} + +#[cfg(feature = "pkcs8")] +impl TryFrom<&pkcs8::KeypairBytes> for Keypair { + type Error = pkcs8::Error; + + fn try_from(pkcs8_key: &pkcs8::KeypairBytes) -> pkcs8::Result { + let secret = SecretKey::from_bytes(&pkcs8_key.secret_key) + .map_err(|_| pkcs8::Error::KeyMalformed)?; + + let public = PublicKey::from(&secret); + + // Validate the public key in the PKCS#8 document if present + if let Some(public_bytes) = pkcs8_key.public_key { + let pk = PublicKey::from_bytes(public_bytes.as_ref()) + .map_err(|_| pkcs8::Error::KeyMalformed)?; + + if public != pk { + return Err(pkcs8::Error::KeyMalformed); + } + } + + Ok(Keypair { secret, public }) + } +} + +#[cfg(feature = "pkcs8")] +impl From for pkcs8::KeypairBytes { + fn from(keypair: Keypair) -> pkcs8::KeypairBytes { + pkcs8::KeypairBytes::from(&keypair) + } +} + +#[cfg(feature = "pkcs8")] +impl From<&Keypair> for pkcs8::KeypairBytes { + fn from(keypair: &Keypair) -> pkcs8::KeypairBytes { + pkcs8::KeypairBytes { + secret_key: keypair.secret.to_bytes(), + public_key: Some(pkcs8::PublicKeyBytes(keypair.public.to_bytes())) + } + } +} + +#[cfg(feature = "pkcs8")] +impl TryFrom> for Keypair { + type Error = pkcs8::Error; + + fn try_from(private_key: pkcs8::PrivateKeyInfo<'_>) -> pkcs8::Result { + pkcs8::KeypairBytes::try_from(private_key)?.try_into() + } +} + #[cfg(feature = "serde")] impl Serialize for Keypair { fn serialize(&self, serializer: S) -> Result diff --git a/src/lib.rs b/src/lib.rs index ee3a8dd7..07e9cbf5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -138,6 +138,44 @@ //! # } //! ``` //! +//! ### PKCS#8 Key Encoding +//! +//! PKCS#8 is a private key format with support for multiple algorithms. +//! It can be encoded as binary (DER) or text (PEM). +//! +//! You can recognize PEM-encoded PKCS#8 keys by the following: +//! +//! ```text +//! -----BEGIN PRIVATE KEY----- +//! ``` +//! +//! To use PKCS#8, you need to enable the `pkcs8` crate feature. +//! +//! The following traits can be used to decode/encode [`Keypair`] and +//! [`PublicKey`] as PKCS#8. Note that [`pkcs8`] is re-exported from the +//! toplevel of the crate: +//! +//! - [`pkcs8::DecodePrivateKey`]: decode private keys from PKCS#8 +//! - [`pkcs8::EncodePrivateKey`]: encode private keys to PKCS#8 +//! - [`pkcs8::DecodePublicKey`]: decode public keys from PKCS#8 +//! - [`pkcs8::EncodePublicKey`]: encode public keys to PKCS#8 +//! +//! #### Example +//! +//! NOTE: this requires the `pem` crate feature. +//! +#![cfg_attr(feature = "pem", doc = "```")] +#![cfg_attr(not(feature = "pem"), doc = "```ignore")] +//! use ed25519_dalek::{PublicKey, pkcs8::DecodePublicKey}; +//! +//! let pem = "-----BEGIN PUBLIC KEY----- +//! MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE= +//! -----END PUBLIC KEY-----"; +//! +//! let public_key = PublicKey::from_public_key_pem(pem) +//! .expect("invalid public key PEM"); +//! ``` +//! //! ### Using Serde //! //! If you prefer the bytes to be wrapped in another serialisation format, all @@ -208,6 +246,8 @@ #![warn(future_incompatible, rust_2018_idioms)] #![deny(missing_docs)] // refuse to compile if documentation is missing #![cfg_attr(not(test), forbid(unsafe_code))] +#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg, doc_cfg_hide))] +#![cfg_attr(docsrs, doc(cfg_hide(docsrs)))] #[cfg(any(feature = "batch", feature = "batch_deterministic"))] extern crate alloc; @@ -243,3 +283,6 @@ pub use crate::secret::*; // Re-export the `Signer` and `Verifier` traits from the `signature` crate pub use ed25519::signature::{Signer, Verifier}; pub use ed25519::Signature; + +#[cfg(feature = "pkcs8")] +pub use ed25519::pkcs8; diff --git a/src/public.rs b/src/public.rs index fdbced25..a16dbedd 100644 --- a/src/public.rs +++ b/src/public.rs @@ -23,6 +23,9 @@ use ed25519::signature::Verifier; pub use sha2::Sha512; +#[cfg(feature = "pkcs8")] +use ed25519::pkcs8::{self, DecodePublicKey}; + #[cfg(feature = "serde")] use serde::de::Error as SerdeError; #[cfg(feature = "serde")] @@ -350,6 +353,65 @@ impl Verifier for PublicKey { } } +impl TryFrom<&[u8]> for PublicKey { + type Error = SignatureError; + + fn try_from(bytes: &[u8]) -> Result { + PublicKey::from_bytes(bytes) + } +} + +#[cfg(feature = "pkcs8")] +impl DecodePublicKey for PublicKey {} + +#[cfg(all(feature = "alloc", feature = "pkcs8"))] +impl pkcs8::EncodePublicKey for PublicKey { + fn to_public_key_der(&self) -> pkcs8::spki::Result { + pkcs8::PublicKeyBytes::from(self).to_public_key_der() + } +} + +#[cfg(feature = "pkcs8")] +impl TryFrom for PublicKey { + type Error = pkcs8::spki::Error; + + fn try_from(pkcs8_key: pkcs8::PublicKeyBytes) -> pkcs8::spki::Result { + PublicKey::try_from(&pkcs8_key) + } +} + +#[cfg(feature = "pkcs8")] +impl TryFrom<&pkcs8::PublicKeyBytes> for PublicKey { + type Error = pkcs8::spki::Error; + + fn try_from(pkcs8_key: &pkcs8::PublicKeyBytes) -> pkcs8::spki::Result { + PublicKey::from_bytes(pkcs8_key.as_ref()).map_err(|_| pkcs8::spki::Error::KeyMalformed) + } +} + +#[cfg(feature = "pkcs8")] +impl From for pkcs8::PublicKeyBytes { + fn from(public_key: PublicKey) -> pkcs8::PublicKeyBytes { + pkcs8::PublicKeyBytes::from(&public_key) + } +} + +#[cfg(feature = "pkcs8")] +impl From<&PublicKey> for pkcs8::PublicKeyBytes { + fn from(public_key: &PublicKey) -> pkcs8::PublicKeyBytes { + pkcs8::PublicKeyBytes(public_key.to_bytes()) + } +} + +#[cfg(feature = "pkcs8")] +impl TryFrom> for PublicKey { + type Error = pkcs8::spki::Error; + + fn try_from(public_key: pkcs8::spki::SubjectPublicKeyInfo<'_>) -> pkcs8::spki::Result { + pkcs8::PublicKeyBytes::try_from(public_key)?.try_into() + } +} + #[cfg(feature = "serde")] impl Serialize for PublicKey { fn serialize(&self, serializer: S) -> Result diff --git a/tests/examples/pkcs8-v1.der b/tests/examples/pkcs8-v1.der new file mode 100644 index 0000000000000000000000000000000000000000..cb780b362c9dbb7e62b9159ac40c45b1242bec2f GIT binary patch literal 48 zcmV-00MGw0E&>4nFa-t!D`jv5A_O4R?sD7t6Ie>sw%GCaY51)={(LCQ@znd^m#B|K Gbyz~LI~HgF literal 0 HcmV?d00001 diff --git a/tests/examples/pkcs8-v2.der b/tests/examples/pkcs8-v2.der new file mode 100644 index 0000000000000000000000000000000000000000..3358e8a730ac3daf865b6eab869bb9a921ab9ef4 GIT binary patch literal 116 zcmV-)0E_=HasmMXFa-t!D`jv5A_O4R?sD7t6Ie>sw%GCaY51)={(LCQ@znd^m#B|K zbyz~6A21yT3Mz(3hW8Bt2?-Q24-5@Mb#i2EWgtUnVQF%6fgu1HzeEXXgw6hiLAt?b W+&h-YP==~7wzkU*TsW<8F=pYUFECsH literal 0 HcmV?d00001 diff --git a/tests/examples/pubkey.der b/tests/examples/pubkey.der new file mode 100644 index 0000000000000000000000000000000000000000..d1002c4a4e624c322cc71015e6685a25808df374 GIT binary patch literal 44 zcmXreGGJw6)=n*8R%DRe@4}hca`s=V