mirror of
https://github.com/saymrwulf/risc0-curve25519-dalek-source.git
synced 2026-09-04 20:03:40 +00:00
PKCS#8 support (#224)
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
This commit is contained in:
parent
cfcdf536a0
commit
55620dcde5
12 changed files with 274 additions and 13 deletions
5
.github/workflows/rust.yml
vendored
5
.github/workflows/rust.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
17
Cargo.toml
17
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,5 @@ 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"]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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>
|
||||
|
|
|
|||
43
src/lib.rs
43
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;
|
||||
|
|
|
|||
|
|
@ -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<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>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
BIN
tests/examples/pkcs8-v1.der
Normal file
BIN
tests/examples/pkcs8-v1.der
Normal file
Binary file not shown.
BIN
tests/examples/pkcs8-v2.der
Normal file
BIN
tests/examples/pkcs8-v2.der
Normal file
Binary file not shown.
BIN
tests/examples/pubkey.der
Normal file
BIN
tests/examples/pubkey.der
Normal file
Binary file not shown.
74
tests/pkcs8.rs
Normal file
74
tests/pkcs8.rs
Normal file
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in a new issue