Skip to content

Commit 9ac0fbb

Browse files
committed
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
1 parent cfcdf53 commit 9ac0fbb

File tree

12 files changed

+274
-10
lines changed

12 files changed

+274
-10
lines changed

.github/workflows/rust.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ jobs:
3131
- run: cargo test --target ${{ matrix.target }} --features batch
3232
- run: cargo test --target ${{ matrix.target }} --features batch_deterministic
3333
- run: cargo test --target ${{ matrix.target }} --features serde
34+
- run: cargo test --target ${{ matrix.target }} --features pkcs8
3435

3536
build-simd:
3637
name: Test simd backend (nightly)
@@ -46,7 +47,7 @@ jobs:
4647
run: cargo build --target x86_64-unknown-linux-gnu
4748

4849
msrv:
49-
name: Current MSRV is 1.56.1
50+
name: Current MSRV is 1.57.0
5051
runs-on: ubuntu-latest
5152
steps:
5253
- uses: actions/checkout@v3
@@ -56,7 +57,7 @@ jobs:
5657
- run: cargo -Z minimal-versions check --no-default-features --features serde
5758
# Now check that `cargo build` works with respect to the oldest possible
5859
# deps and the stated MSRV
59-
- uses: dtolnay/rust-toolchain@1.56.1
60+
- uses: dtolnay/rust-toolchain@1.57.0
6061
- run: cargo build
6162

6263
bench:

Cargo.toml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,20 @@ keywords = ["cryptography", "ed25519", "curve25519", "signature", "ECC"]
1212
categories = ["cryptography", "no-std"]
1313
description = "Fast and efficient ed25519 EdDSA key generations, signing, and verification in pure Rust."
1414
exclude = [ ".gitignore", "TESTVECTORS", "res/*" ]
15+
rust-version = "1.57"
1516

1617
[badges]
1718
travis-ci = { repository = "dalek-cryptography/ed25519-dalek", branch = "master"}
1819

1920
[package.metadata.docs.rs]
2021
# Disabled for now since this is borked; tracking https://github.com/rust-lang/docs.rs/issues/302
2122
# rustdoc-args = ["--html-in-header", ".cargo/registry/src/github.zerozr99.workers.dev-1ecc6299db9ec823/curve25519-dalek-0.13.2/rustdoc-include-katex-header.html"]
22-
features = ["nightly", "batch"]
23+
rustdoc-args = ["--cfg", "docsrs"]
24+
features = ["nightly", "batch", "pkcs8"]
2325

2426
[dependencies]
25-
curve25519-dalek = { version = "=4.0.0-pre.2", default-features = false, features = ["digest", "rand_core"] }
26-
ed25519 = { version = "=2.0.0-pre.0", default-features = false }
27+
curve25519-dalek = { version = "=4.0.0-pre.3", default-features = false, features = ["digest", "rand_core"] }
28+
ed25519 = { version = "=2.0.0-pre.1", default-features = false }
2729
merlin = { version = "3", default-features = false, optional = true }
2830
rand = { version = "0.8", default-features = false, optional = true }
2931
rand_core = { version = "0.6", default-features = false, optional = true }
@@ -37,6 +39,7 @@ hex = "^0.4"
3739
bincode = "1.0"
3840
serde_json = "1.0"
3941
criterion = "0.3"
42+
hex-literal = "0.3"
4043
rand = "0.8"
4144
serde_crate = { package = "serde", version = "1.0", features = ["derive"] }
4245
toml = { version = "0.5" }
@@ -49,15 +52,16 @@ required-features = ["batch"]
4952
[features]
5053
default = ["std", "rand"]
5154
std = ["alloc", "ed25519/std", "serde_crate/std", "sha2/std", "rand/std"]
52-
alloc = ["curve25519-dalek/alloc", "rand/alloc", "zeroize/alloc"]
55+
alloc = ["curve25519-dalek/alloc", "ed25519/alloc", "rand/alloc", "zeroize/alloc"]
5356
serde = ["serde_crate", "serde_bytes", "ed25519/serde"]
5457
batch = ["alloc", "merlin", "rand/std"]
5558
# This feature enables deterministic batch verification.
5659
batch_deterministic = ["alloc", "merlin", "rand", "rand_core"]
5760
asm = ["sha2/asm"]
5861
# This features turns off stricter checking for scalar malleability in signatures
5962
legacy_compatibility = []
63+
pkcs8 = ["ed25519/pkcs8"]
64+
pem = ["alloc", "ed25519/pem", "pkcs8"]
6065

6166
[patch.crates-io]
6267
curve25519-dalek = { git = "https://github.com/dalek-cryptography/curve25519-dalek.git", branch = "release/4.0" }
63-
ed25519 = { git = "https://github.com/RustCrypto/signatures.git"}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ version = "1"
1818

1919
# Minimum Supported Rust Version
2020

21-
This crate requires Rust 1.56.1 at a minimum. 1.x releases of this crate supported an MSRV of 1.41.
21+
This crate requires Rust 1.57.0 at a minimum. 1.x releases of this crate supported an MSRV of 1.41.
2222

2323
In the future, MSRV changes will be accompanied by a minor version bump.
2424

src/keypair.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
//! ed25519 keypairs.
1111
12+
#[cfg(feature = "pkcs8")]
13+
use ed25519::pkcs8::{self, DecodePrivateKey};
14+
1215
#[cfg(feature = "rand")]
1316
use rand::{CryptoRng, RngCore};
1417

@@ -431,6 +434,83 @@ impl Verifier<ed25519::Signature> for Keypair {
431434
}
432435
}
433436

437+
impl TryFrom<&[u8]> for Keypair {
438+
type Error = SignatureError;
439+
440+
fn try_from(bytes: &[u8]) -> Result<Keypair, SignatureError> {
441+
Keypair::from_bytes(bytes)
442+
}
443+
}
444+
445+
#[cfg(feature = "pkcs8")]
446+
impl DecodePrivateKey for Keypair {}
447+
448+
#[cfg(all(feature = "alloc", feature = "pkcs8"))]
449+
impl pkcs8::EncodePrivateKey for Keypair {
450+
fn to_pkcs8_der(&self) -> pkcs8::Result<pkcs8::SecretDocument> {
451+
pkcs8::KeypairBytes::from(self).to_pkcs8_der()
452+
}
453+
}
454+
455+
#[cfg(feature = "pkcs8")]
456+
impl TryFrom<pkcs8::KeypairBytes> for Keypair {
457+
type Error = pkcs8::Error;
458+
459+
fn try_from(pkcs8_key: pkcs8::KeypairBytes) -> pkcs8::Result<Self> {
460+
Keypair::try_from(&pkcs8_key)
461+
}
462+
}
463+
464+
#[cfg(feature = "pkcs8")]
465+
impl TryFrom<&pkcs8::KeypairBytes> for Keypair {
466+
type Error = pkcs8::Error;
467+
468+
fn try_from(pkcs8_key: &pkcs8::KeypairBytes) -> pkcs8::Result<Self> {
469+
let secret = SecretKey::from_bytes(&pkcs8_key.secret_key)
470+
.map_err(|_| pkcs8::Error::KeyMalformed)?;
471+
472+
let public = PublicKey::from(&secret);
473+
474+
// Validate the public key in the PKCS#8 document if present
475+
if let Some(public_bytes) = pkcs8_key.public_key {
476+
let pk = PublicKey::from_bytes(public_bytes.as_ref())
477+
.map_err(|_| pkcs8::Error::KeyMalformed)?;
478+
479+
if public != pk {
480+
return Err(pkcs8::Error::KeyMalformed);
481+
}
482+
}
483+
484+
Ok(Keypair { secret, public })
485+
}
486+
}
487+
488+
#[cfg(feature = "pkcs8")]
489+
impl From<Keypair> for pkcs8::KeypairBytes {
490+
fn from(keypair: Keypair) -> pkcs8::KeypairBytes {
491+
pkcs8::KeypairBytes::from(&keypair)
492+
}
493+
}
494+
495+
#[cfg(feature = "pkcs8")]
496+
impl From<&Keypair> for pkcs8::KeypairBytes {
497+
fn from(keypair: &Keypair) -> pkcs8::KeypairBytes {
498+
pkcs8::KeypairBytes {
499+
secret_key: keypair.secret.to_bytes(),
500+
public_key: Some(pkcs8::PublicKeyBytes(keypair.public.to_bytes())),
501+
}
502+
}
503+
}
504+
505+
#[cfg(feature = "pkcs8")]
506+
impl TryFrom<pkcs8::PrivateKeyInfo<'_>> for Keypair {
507+
type Error = pkcs8::Error;
508+
509+
fn try_from(private_key: pkcs8::PrivateKeyInfo<'_>) -> pkcs8::Result<Self> {
510+
pkcs8::KeypairBytes::try_from(private_key)?.try_into()
511+
}
512+
}
513+
434514
#[cfg(feature = "serde")]
435515
impl Serialize for Keypair {
436516
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>

src/lib.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,44 @@
138138
//! # }
139139
//! ```
140140
//!
141+
//! ### PKCS#8 Key Encoding
142+
//!
143+
//! PKCS#8 is a private key format with support for multiple algorithms.
144+
//! It can be encoded as binary (DER) or text (PEM).
145+
//!
146+
//! You can recognize PEM-encoded PKCS#8 keys by the following:
147+
//!
148+
//! ```text
149+
//! -----BEGIN PRIVATE KEY-----
150+
//! ```
151+
//!
152+
//! To use PKCS#8, you need to enable the `pkcs8` crate feature.
153+
//!
154+
//! The following traits can be used to decode/encode [`Keypair`] and
155+
//! [`PublicKey`] as PKCS#8. Note that [`pkcs8`] is re-exported from the
156+
//! toplevel of the crate:
157+
//!
158+
//! - [`pkcs8::DecodePrivateKey`]: decode private keys from PKCS#8
159+
//! - [`pkcs8::EncodePrivateKey`]: encode private keys to PKCS#8
160+
//! - [`pkcs8::DecodePublicKey`]: decode public keys from PKCS#8
161+
//! - [`pkcs8::EncodePublicKey`]: encode public keys to PKCS#8
162+
//!
163+
//! #### Example
164+
//!
165+
//! NOTE: this requires the `pem` crate feature.
166+
//!
167+
#![cfg_attr(feature = "pem", doc = "```")]
168+
#![cfg_attr(not(feature = "pem"), doc = "```ignore")]
169+
//! use ed25519_dalek::{PublicKey, pkcs8::DecodePublicKey};
170+
//!
171+
//! let pem = "-----BEGIN PUBLIC KEY-----
172+
//! MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE=
173+
//! -----END PUBLIC KEY-----";
174+
//!
175+
//! let public_key = PublicKey::from_public_key_pem(pem)
176+
//! .expect("invalid public key PEM");
177+
//! ```
178+
//!
141179
//! ### Using Serde
142180
//!
143181
//! If you prefer the bytes to be wrapped in another serialisation format, all
@@ -208,6 +246,8 @@
208246
#![warn(future_incompatible, rust_2018_idioms)]
209247
#![deny(missing_docs)] // refuse to compile if documentation is missing
210248
#![cfg_attr(not(test), forbid(unsafe_code))]
249+
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg, doc_cfg_hide))]
250+
#![cfg_attr(docsrs, doc(cfg_hide(docsrs)))]
211251

212252
#[cfg(any(feature = "batch", feature = "batch_deterministic"))]
213253
extern crate alloc;
@@ -243,3 +283,6 @@ pub use crate::secret::*;
243283
// Re-export the `Signer` and `Verifier` traits from the `signature` crate
244284
pub use ed25519::signature::{Signer, Verifier};
245285
pub use ed25519::Signature;
286+
287+
#[cfg(feature = "pkcs8")]
288+
pub use ed25519::pkcs8;

src/public.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ use ed25519::signature::Verifier;
2323

2424
pub use sha2::Sha512;
2525

26+
#[cfg(feature = "pkcs8")]
27+
use ed25519::pkcs8::{self, DecodePublicKey};
28+
2629
#[cfg(feature = "serde")]
2730
use serde::de::Error as SerdeError;
2831
#[cfg(feature = "serde")]
@@ -350,6 +353,65 @@ impl Verifier<ed25519::Signature> for PublicKey {
350353
}
351354
}
352355

356+
impl TryFrom<&[u8]> for PublicKey {
357+
type Error = SignatureError;
358+
359+
fn try_from(bytes: &[u8]) -> Result<PublicKey, SignatureError> {
360+
PublicKey::from_bytes(bytes)
361+
}
362+
}
363+
364+
#[cfg(feature = "pkcs8")]
365+
impl DecodePublicKey for PublicKey {}
366+
367+
#[cfg(all(feature = "alloc", feature = "pkcs8"))]
368+
impl pkcs8::EncodePublicKey for PublicKey {
369+
fn to_public_key_der(&self) -> pkcs8::spki::Result<pkcs8::Document> {
370+
pkcs8::PublicKeyBytes::from(self).to_public_key_der()
371+
}
372+
}
373+
374+
#[cfg(feature = "pkcs8")]
375+
impl TryFrom<pkcs8::PublicKeyBytes> for PublicKey {
376+
type Error = pkcs8::spki::Error;
377+
378+
fn try_from(pkcs8_key: pkcs8::PublicKeyBytes) -> pkcs8::spki::Result<Self> {
379+
PublicKey::try_from(&pkcs8_key)
380+
}
381+
}
382+
383+
#[cfg(feature = "pkcs8")]
384+
impl TryFrom<&pkcs8::PublicKeyBytes> for PublicKey {
385+
type Error = pkcs8::spki::Error;
386+
387+
fn try_from(pkcs8_key: &pkcs8::PublicKeyBytes) -> pkcs8::spki::Result<Self> {
388+
PublicKey::from_bytes(pkcs8_key.as_ref()).map_err(|_| pkcs8::spki::Error::KeyMalformed)
389+
}
390+
}
391+
392+
#[cfg(feature = "pkcs8")]
393+
impl From<PublicKey> for pkcs8::PublicKeyBytes {
394+
fn from(public_key: PublicKey) -> pkcs8::PublicKeyBytes {
395+
pkcs8::PublicKeyBytes::from(&public_key)
396+
}
397+
}
398+
399+
#[cfg(feature = "pkcs8")]
400+
impl From<&PublicKey> for pkcs8::PublicKeyBytes {
401+
fn from(public_key: &PublicKey) -> pkcs8::PublicKeyBytes {
402+
pkcs8::PublicKeyBytes(public_key.to_bytes())
403+
}
404+
}
405+
406+
#[cfg(feature = "pkcs8")]
407+
impl TryFrom<pkcs8::spki::SubjectPublicKeyInfo<'_>> for PublicKey {
408+
type Error = pkcs8::spki::Error;
409+
410+
fn try_from(public_key: pkcs8::spki::SubjectPublicKeyInfo<'_>) -> pkcs8::spki::Result<Self> {
411+
pkcs8::PublicKeyBytes::try_from(public_key)?.try_into()
412+
}
413+
}
414+
353415
#[cfg(feature = "serde")]
354416
impl Serialize for PublicKey {
355417
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>

src/signature.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ fn check_scalar(bytes: [u8; 32]) -> Result<Scalar, SignatureError> {
9494
return Ok(Scalar::from_bits(bytes));
9595
}
9696

97-
match Scalar::from_canonical_bytes(bytes) {
97+
match Scalar::from_canonical_bytes(bytes).into() {
9898
None => return Err(InternalError::ScalarFormatError.into()),
9999
Some(x) => return Ok(x),
100100
};

tests/ed25519.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ mod vectors {
156156
fn non_null_scalar() -> Scalar {
157157
let mut rng = rand::rngs::OsRng;
158158
let mut s_candidate = Scalar::random(&mut rng);
159-
while s_candidate == Scalar::zero() {
159+
while s_candidate == Scalar::ZERO {
160160
s_candidate = Scalar::random(&mut rng);
161161
}
162162
s_candidate

tests/examples/pkcs8-v1.der

48 Bytes
Binary file not shown.

tests/examples/pkcs8-v2.der

116 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)