Skip to content

Commit

Permalink
PKCS#8 support (#224)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tarcieri authored Dec 13, 2022
1 parent cfcdf53 commit 55620dc
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 13 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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:
Expand Down
17 changes: 9 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ 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"}

[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 }
Expand All @@ -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" }
Expand All @@ -49,15 +52,13 @@ 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.
batch_deterministic = ["alloc", "merlin", "rand", "rand_core"]
asm = ["sha2/asm"]
# This features turns off stricter checking for scalar malleability in signatures
legacy_compatibility = []

[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"}
pkcs8 = ["ed25519/pkcs8"]
pem = ["alloc", "ed25519/pem", "pkcs8"]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
80 changes: 80 additions & 0 deletions src/keypair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

//! ed25519 keypairs.
#[cfg(feature = "pkcs8")]
use ed25519::pkcs8::{self, DecodePrivateKey};

#[cfg(feature = "rand")]
use rand::{CryptoRng, RngCore};

Expand Down Expand Up @@ -431,6 +434,83 @@ impl Verifier<ed25519::Signature> for Keypair {
}
}

impl TryFrom<&[u8]> for Keypair {
type Error = SignatureError;

fn try_from(bytes: &[u8]) -> Result<Keypair, SignatureError> {
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::SecretDocument> {
pkcs8::KeypairBytes::from(self).to_pkcs8_der()
}
}

#[cfg(feature = "pkcs8")]
impl TryFrom<pkcs8::KeypairBytes> for Keypair {
type Error = pkcs8::Error;

fn try_from(pkcs8_key: pkcs8::KeypairBytes) -> pkcs8::Result<Self> {
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<Self> {
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<Keypair> 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<pkcs8::PrivateKeyInfo<'_>> for Keypair {
type Error = pkcs8::Error;

fn try_from(private_key: pkcs8::PrivateKeyInfo<'_>) -> pkcs8::Result<Self> {
pkcs8::KeypairBytes::try_from(private_key)?.try_into()
}
}

#[cfg(feature = "serde")]
impl Serialize for Keypair {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
Expand Down
43 changes: 43 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
62 changes: 62 additions & 0 deletions src/public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -350,6 +353,65 @@ impl Verifier<ed25519::Signature> for PublicKey {
}
}

impl TryFrom<&[u8]> for PublicKey {
type Error = SignatureError;

fn try_from(bytes: &[u8]) -> Result<PublicKey, SignatureError> {
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::Document> {
pkcs8::PublicKeyBytes::from(self).to_public_key_der()
}
}

#[cfg(feature = "pkcs8")]
impl TryFrom<pkcs8::PublicKeyBytes> for PublicKey {
type Error = pkcs8::spki::Error;

fn try_from(pkcs8_key: pkcs8::PublicKeyBytes) -> pkcs8::spki::Result<Self> {
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<Self> {
PublicKey::from_bytes(pkcs8_key.as_ref()).map_err(|_| pkcs8::spki::Error::KeyMalformed)
}
}

#[cfg(feature = "pkcs8")]
impl From<PublicKey> 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<pkcs8::spki::SubjectPublicKeyInfo<'_>> for PublicKey {
type Error = pkcs8::spki::Error;

fn try_from(public_key: pkcs8::spki::SubjectPublicKeyInfo<'_>) -> pkcs8::spki::Result<Self> {
pkcs8::PublicKeyBytes::try_from(public_key)?.try_into()
}
}

#[cfg(feature = "serde")]
impl Serialize for PublicKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
Expand Down
2 changes: 1 addition & 1 deletion src/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ fn check_scalar(bytes: [u8; 32]) -> Result<Scalar, SignatureError> {
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),
};
Expand Down
2 changes: 1 addition & 1 deletion tests/ed25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file added tests/examples/pkcs8-v1.der
Binary file not shown.
Binary file added tests/examples/pkcs8-v2.der
Binary file not shown.
Binary file added tests/examples/pubkey.der
Binary file not shown.
Loading

0 comments on commit 55620dc

Please sign in to comment.