diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 770193af..6019bcd7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -31,6 +31,7 @@ jobs: - run: cargo test --target ${{ matrix.target }} --features batch - run: cargo test --target ${{ matrix.target }} --features batch_deterministic - run: cargo test --target ${{ matrix.target }} --features serde + - run: cargo test --target ${{ matrix.target }} --features pkcs8 build-simd: name: Test simd backend (nightly) @@ -46,7 +47,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 +57,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..18365a1a 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,11 +20,12 @@ 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"] } -ed25519 = { version = "=2.0.0-pre.0", default-features = false } +curve25519-dalek = { version = "=4.0.0-pre.3", default-features = false, features = ["digest", "rand_core"] } +ed25519 = { version = "=2.0.0-pre.1", default-features = false } merlin = { version = "3", default-features = false, optional = true } rand = { version = "0.8", default-features = false, optional = true } rand_core = { version = "0.6", default-features = false, optional = true } @@ -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,8 @@ 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"} 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..8c9c6c1d 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/src/signature.rs b/src/signature.rs index de8a4250..795bfada 100644 --- a/src/signature.rs +++ b/src/signature.rs @@ -94,7 +94,7 @@ fn check_scalar(bytes: [u8; 32]) -> Result { return Ok(Scalar::from_bits(bytes)); } - match Scalar::from_canonical_bytes(bytes) { + match Scalar::from_canonical_bytes(bytes).into() { None => return Err(InternalError::ScalarFormatError.into()), Some(x) => return Ok(x), }; diff --git a/tests/ed25519.rs b/tests/ed25519.rs index 0ccb68b2..bd597db7 100644 --- a/tests/ed25519.rs +++ b/tests/ed25519.rs @@ -156,7 +156,7 @@ mod vectors { fn non_null_scalar() -> Scalar { let mut rng = rand::rngs::OsRng; let mut s_candidate = Scalar::random(&mut rng); - while s_candidate == Scalar::zero() { + while s_candidate == Scalar::ZERO { s_candidate = Scalar::random(&mut rng); } s_candidate diff --git a/tests/examples/pkcs8-v1.der b/tests/examples/pkcs8-v1.der new file mode 100644 index 00000000..cb780b36 Binary files /dev/null and b/tests/examples/pkcs8-v1.der differ diff --git a/tests/examples/pkcs8-v2.der b/tests/examples/pkcs8-v2.der new file mode 100644 index 00000000..3358e8a7 Binary files /dev/null and b/tests/examples/pkcs8-v2.der differ diff --git a/tests/examples/pubkey.der b/tests/examples/pubkey.der new file mode 100644 index 00000000..d1002c4a Binary files /dev/null and b/tests/examples/pubkey.der differ diff --git a/tests/pkcs8.rs b/tests/pkcs8.rs new file mode 100644 index 00000000..0af97f5b --- /dev/null +++ b/tests/pkcs8.rs @@ -0,0 +1,74 @@ +//! PKCS#8 private key and SPKI public key tests. +//! +//! These are standard formats for storing public and private keys, defined in +//! RFC5958 (PKCS#8) and RFC5280 (SPKI). + +#![cfg(feature = "pkcs8")] + +use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey}; +use ed25519_dalek::{Keypair, PublicKey}; +use hex_literal::hex; + +#[cfg(feature = "alloc")] +use ed25519_dalek::{ + pkcs8::{EncodePrivateKey, EncodePublicKey}, + SecretKey, +}; + +/// Ed25519 PKCS#8 v1 private key encoded as ASN.1 DER. +const PKCS8_V1_DER: &[u8] = include_bytes!("examples/pkcs8-v1.der"); + +/// Ed25519 PKCS#8 v2 private key + public key encoded as ASN.1 DER. +const PKCS8_V2_DER: &[u8] = include_bytes!("examples/pkcs8-v2.der"); + +/// Ed25519 SubjectPublicKeyInfo encoded as ASN.1 DER. +const PUBLIC_KEY_DER: &[u8] = include_bytes!("examples/pubkey.der"); + +/// Secret key bytes. +/// +/// Extracted with: +/// $ openssl asn1parse -inform der -in tests/examples/pkcs8-v1.der +const SK_BYTES: [u8; 32] = hex!("D4EE72DBF913584AD5B6D8F1F769F8AD3AFE7C28CBF1D4FBE097A88F44755842"); + +/// Public key bytes. +const PK_BYTES: [u8; 32] = hex!("19BF44096984CDFE8541BAC167DC3B96C85086AA30B6B6CB0C5C38AD703166E1"); + +#[test] +fn decode_pkcs8_v1() { + let keypair = Keypair::from_pkcs8_der(PKCS8_V1_DER).unwrap(); + assert_eq!(SK_BYTES, keypair.secret_key().to_bytes()); + assert_eq!(PK_BYTES, keypair.public_key().to_bytes()); +} + +#[test] +fn decode_pkcs8_v2() { + let keypair = Keypair::from_pkcs8_der(PKCS8_V2_DER).unwrap(); + assert_eq!(SK_BYTES, keypair.secret_key().to_bytes()); + assert_eq!(PK_BYTES, keypair.public_key().to_bytes()); +} + +#[test] +fn decode_public_key() { + let public_key = PublicKey::from_public_key_der(PUBLIC_KEY_DER).unwrap(); + assert_eq!(PK_BYTES, public_key.to_bytes()); +} + +#[test] +#[cfg(feature = "alloc")] +fn encode_pkcs8() { + let keypair = Keypair::from(SecretKey::from_bytes(&SK_BYTES).unwrap()); + let pkcs8_key = keypair.to_pkcs8_der().unwrap(); + + let keypair2 = Keypair::from_pkcs8_der(pkcs8_key.as_bytes()).unwrap(); + assert_eq!(keypair.to_bytes(), keypair2.to_bytes()); +} + +#[test] +#[cfg(feature = "alloc")] +fn encode_public_key() { + let public_key = PublicKey::from_bytes(&PK_BYTES).unwrap(); + let public_key_der = public_key.to_public_key_der().unwrap(); + + let public_key2 = PublicKey::from_public_key_der(public_key_der.as_bytes()).unwrap(); + assert_eq!(public_key, public_key2); +}