Skip to content

Commit

Permalink
PKCS#8 support
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 committed Dec 10, 2022
1 parent cfcdf53 commit 7693dfb
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 6 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
11 changes: 8 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ 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"] }
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,17 @@ 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 = []
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" }
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
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.
74 changes: 74 additions & 0 deletions tests/pkcs8.rs
Original file line number Diff line number Diff line change
@@ -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::{PublicKey, Keypair};
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);
}

0 comments on commit 7693dfb

Please sign in to comment.