ed25519: add pkcs8::PublicKeyBytes type (#455)

Adds a type for representing serialized Ed25519 public keys.

The type implements the serialization traits from the `spki` crate,
which is used for representing the public keys associated with a
PKCS#8-encoded private key.
This commit is contained in:
Tony Arcieri
2022-03-18 12:31:28 -06:00
committed by GitHub
parent 6ddc647172
commit 40cab54bb7
6 changed files with 258 additions and 49 deletions
+2 -2
View File
@@ -45,8 +45,8 @@ be accompanied by a minor version bump.
- All on-by-default features of this library are covered by SemVer
- MSRV is considered exempt from SemVer as noted above
- The `pkcs8` crate is exempted as it's a pre-1.0 dependency, however, upgrades
to this crate will be accompanied by a minor version bump.
- The `pkcs8` module is exempted as it uses a pre-1.0 dependency, however,
breaking changes to this module will be accompanied by a minor version bump.
## License
+6 -1
View File
@@ -255,7 +255,12 @@
html_root_url = "https://docs.rs/ed25519/1.4.0"
)]
#![forbid(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms, unused_qualifications)]
#![warn(
missing_docs,
rust_2018_idioms,
unused_lifetimes,
unused_qualifications
)]
#[cfg(feature = "alloc")]
extern crate alloc;
+214 -36
View File
@@ -2,15 +2,32 @@
//!
//! Implements Ed25519 PKCS#8 private keys as described in RFC8410 Section 7:
//! <https://datatracker.ietf.org/doc/html/rfc8410#section-7>
//!
//! ## SemVer Notes
//!
//! The `pkcs8` module of this crate is exempted from SemVer as it uses a
//! pre-1.0 dependency (the `pkcs8` crate).
//!
//! However, breaking changes to this module will be accompanied by a minor
//! version bump.
//!
//! Please lock to a specific minor version of the `ed25519` crate to avoid
//! breaking changes when using this module.
pub use pkcs8::DecodePrivateKey;
pub use pkcs8::{DecodePrivateKey, DecodePublicKey};
#[cfg(feature = "alloc")]
pub use pkcs8::EncodePrivateKey;
pub use pkcs8::{spki::EncodePublicKey, EncodePrivateKey};
use core::fmt;
use pkcs8::ObjectIdentifier;
#[cfg(feature = "pem")]
use {
alloc::string::{String, ToString},
core::str,
};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
@@ -20,6 +37,12 @@ use zeroize::Zeroize;
/// <http://oid-info.com/get/1.3.101.112>
pub const ALGORITHM_OID: ObjectIdentifier = ObjectIdentifier::new("1.3.101.112");
/// Ed25519 Algorithm Identifier.
pub const ALGORITHM_ID: pkcs8::AlgorithmIdentifier<'static> = pkcs8::AlgorithmIdentifier {
oid: ALGORITHM_OID,
parameters: None,
};
/// Ed25519 keypair serialized as bytes.
///
/// This type is primarily useful for decoding/encoding PKCS#8 private key
@@ -27,6 +50,15 @@ pub const ALGORITHM_OID: ObjectIdentifier = ObjectIdentifier::new("1.3.101.112")
///
/// - [`DecodePrivateKey`]: decode DER or PEM encoded PKCS#8 private key.
/// - [`EncodePrivateKey`]: encode DER or PEM encoded PKCS#8 private key.
///
/// PKCS#8 private key files encoded with PEM begin with:
///
/// ```text
/// -----BEGIN PRIVATE KEY-----
/// ```
///
/// Note that this type operates on raw bytes and performs no validation that
/// keys represent valid Ed25519 field elements.
pub struct KeypairBytes {
/// Ed25519 secret key.
///
@@ -45,6 +77,15 @@ impl KeypairBytes {
/// Size of an Ed25519 keypair when serialized as bytes.
const BYTE_SIZE: usize = 64;
/// Parse raw keypair from a 64-byte input.
pub fn from_bytes(bytes: &[u8; Self::BYTE_SIZE]) -> Self {
let (sk, pk) = bytes.split_at(Self::BYTE_SIZE / 2);
Self {
secret_key: sk.try_into().expect("secret key size error"),
public_key: Some(pk.try_into().expect("public key size error")),
}
}
/// Serialize as a 64-byte keypair.
///
/// # Returns
@@ -64,6 +105,39 @@ impl KeypairBytes {
}
}
impl DecodePrivateKey for KeypairBytes {}
impl Drop for KeypairBytes {
fn drop(&mut self) {
#[cfg(feature = "zeroize")]
self.secret_key.zeroize()
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
impl EncodePrivateKey for KeypairBytes {
fn to_pkcs8_der(&self) -> pkcs8::Result<pkcs8::PrivateKeyDocument> {
// Serialize private key as nested OCTET STRING
let mut private_key = [0u8; 2 + (Self::BYTE_SIZE / 2)];
private_key[0] = 0x04;
private_key[1] = 0x20;
private_key[2..].copy_from_slice(&self.secret_key);
let result = pkcs8::PrivateKeyInfo {
algorithm: ALGORITHM_ID,
private_key: &private_key,
public_key: self.public_key.as_ref().map(AsRef::as_ref),
}
.to_der();
#[cfg(feature = "zeroize")]
private_key.zeroize();
result
}
}
impl TryFrom<pkcs8::PrivateKeyInfo<'_>> for KeypairBytes {
type Error = pkcs8::Error;
@@ -100,53 +174,157 @@ impl TryFrom<pkcs8::PrivateKeyInfo<'_>> for KeypairBytes {
}
}
impl DecodePrivateKey for KeypairBytes {}
impl TryFrom<&[u8]> for KeypairBytes {
type Error = pkcs8::Error;
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
impl EncodePrivateKey for KeypairBytes {
fn to_pkcs8_der(&self) -> pkcs8::Result<pkcs8::PrivateKeyDocument> {
let algorithm = pkcs8::AlgorithmIdentifier {
oid: ALGORITHM_OID,
parameters: None,
};
// Serialize private key as nested OCTET STRING
let mut private_key = [0u8; 2 + (Self::BYTE_SIZE / 2)];
private_key[0] = 0x04;
private_key[1] = 0x20;
private_key[2..].copy_from_slice(&self.secret_key);
let result = pkcs8::PrivateKeyInfo {
algorithm,
private_key: &private_key,
public_key: self.public_key.as_ref().map(AsRef::as_ref),
}
.to_der();
#[cfg(feature = "zeroize")]
private_key.zeroize();
result
fn try_from(der_bytes: &[u8]) -> pkcs8::Result<Self> {
Self::from_pkcs8_der(der_bytes)
}
}
impl<'a> fmt::Debug for KeypairBytes {
impl fmt::Debug for KeypairBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KeypairBytes")
.field("public_key", &self.public_key)
.finish() // TODO: use `finish_non_exhaustive` when MSRV 1.53
.finish_non_exhaustive()
}
}
#[cfg(feature = "zeroize")]
#[cfg_attr(docsrs, doc(cfg(feature = "zeroize")))]
impl Drop for KeypairBytes {
fn drop(&mut self) {
self.secret_key.zeroize()
#[cfg(feature = "pem")]
#[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
impl str::FromStr for KeypairBytes {
type Err = pkcs8::Error;
fn from_str(pem: &str) -> pkcs8::Result<Self> {
Self::from_pkcs8_pem(pem)
}
}
/// Ed25519 public key serialized as bytes.
///
/// This type is primarily useful for decoding/encoding SPKI public key
/// files (either DER or PEM) encoded using the following traits:
///
/// - [`DecodePublicKey`]: decode DER or PEM encoded PKCS#8 private key.
/// - [`EncodePublicKey`]: encode DER or PEM encoded PKCS#8 private key.
///
/// SPKI public key files encoded with PEM begin with:
///
/// ```text
/// -----BEGIN PUBLIC KEY-----
/// ```
///
/// Note that this type operates on raw bytes and performs no validation that
/// public keys represent valid compressed Ed25519 y-coordinates.
pub struct PublicKeyBytes(pub [u8; Self::BYTE_SIZE]);
impl PublicKeyBytes {
/// Size of an Ed25519 public key when serialized as bytes.
const BYTE_SIZE: usize = 32;
/// Returns the raw bytes of the public key.
pub fn to_bytes(&self) -> [u8; Self::BYTE_SIZE] {
self.0
}
}
impl AsRef<[u8; Self::BYTE_SIZE]> for PublicKeyBytes {
fn as_ref(&self) -> &[u8; Self::BYTE_SIZE] {
&self.0
}
}
impl DecodePublicKey for PublicKeyBytes {}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
impl EncodePublicKey for PublicKeyBytes {
fn to_public_key_der(&self) -> pkcs8::spki::Result<pkcs8::PublicKeyDocument> {
pkcs8::SubjectPublicKeyInfo {
algorithm: ALGORITHM_ID,
subject_public_key: &self.0,
}
.try_into()
}
}
impl TryFrom<pkcs8::spki::SubjectPublicKeyInfo<'_>> for PublicKeyBytes {
type Error = pkcs8::spki::Error;
fn try_from(spki: pkcs8::spki::SubjectPublicKeyInfo<'_>) -> pkcs8::spki::Result<Self> {
spki.algorithm.assert_algorithm_oid(ALGORITHM_OID)?;
if spki.algorithm.parameters.is_some() {
return Err(pkcs8::spki::Error::KeyMalformed);
}
spki.subject_public_key
.try_into()
.map(Self)
.map_err(|_| pkcs8::spki::Error::KeyMalformed)
}
}
impl TryFrom<&[u8]> for PublicKeyBytes {
type Error = pkcs8::spki::Error;
fn try_from(der_bytes: &[u8]) -> pkcs8::spki::Result<Self> {
Self::from_public_key_der(der_bytes)
}
}
impl TryFrom<KeypairBytes> for PublicKeyBytes {
type Error = pkcs8::spki::Error;
fn try_from(keypair: KeypairBytes) -> pkcs8::spki::Result<PublicKeyBytes> {
PublicKeyBytes::try_from(&keypair)
}
}
impl TryFrom<&KeypairBytes> for PublicKeyBytes {
type Error = pkcs8::spki::Error;
fn try_from(keypair: &KeypairBytes) -> pkcs8::spki::Result<PublicKeyBytes> {
keypair
.public_key
.map(PublicKeyBytes)
.ok_or(pkcs8::spki::Error::KeyMalformed)
}
}
impl fmt::Debug for PublicKeyBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("PublicKeyBytes(")?;
for &byte in self.as_ref() {
write!(f, "{:02X}", byte)?;
}
f.write_str(")")
}
}
#[cfg(feature = "pem")]
#[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
impl str::FromStr for PublicKeyBytes {
type Err = pkcs8::spki::Error;
fn from_str(pem: &str) -> pkcs8::spki::Result<Self> {
Self::from_public_key_pem(pem)
}
}
#[cfg(feature = "pem")]
#[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
impl ToString for PublicKeyBytes {
fn to_string(&self) -> String {
self.to_public_key_pem(Default::default())
.expect("PEM serialization error")
}
}
#[cfg(feature = "pem")]
#[cfg_attr(docsrs, doc(cfg(feature = "pem")))]
#[cfg(test)]
mod tests {
use super::KeypairBytes;
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE=
-----END PUBLIC KEY-----
+33 -10
View File
@@ -2,49 +2,64 @@
#![cfg(feature = "pkcs8")]
use ed25519::pkcs8::{DecodePrivateKey, KeypairBytes};
use ed25519::pkcs8::{DecodePrivateKey, DecodePublicKey, KeypairBytes, PublicKeyBytes};
use hex_literal::hex;
#[cfg(feature = "alloc")]
use ed25519::pkcs8::EncodePrivateKey;
use ed25519::pkcs8::{EncodePrivateKey, EncodePublicKey};
/// Ed25519 PKCS#8 v1 private key encoded as ASN.1 DER
/// 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
/// 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");
#[test]
fn decode_pkcs8_v1() {
let pk = KeypairBytes::from_pkcs8_der(PKCS8_V1_DER).unwrap();
let keypair = KeypairBytes::from_pkcs8_der(PKCS8_V1_DER).unwrap();
// Extracted with:
// $ openssl asn1parse -inform der -in tests/examples/p256-priv.der
assert_eq!(
pk.secret_key,
keypair.secret_key,
&hex!("D4EE72DBF913584AD5B6D8F1F769F8AD3AFE7C28CBF1D4FBE097A88F44755842")[..]
);
assert_eq!(pk.public_key, None);
assert_eq!(keypair.public_key, None);
}
#[test]
fn decode_pkcs8_v2() {
let pk = KeypairBytes::from_pkcs8_der(PKCS8_V2_DER).unwrap();
let keypair = KeypairBytes::from_pkcs8_der(PKCS8_V2_DER).unwrap();
// Extracted with:
// $ openssl asn1parse -inform der -in tests/examples/p256-priv.der
assert_eq!(
pk.secret_key,
keypair.secret_key,
&hex!("D4EE72DBF913584AD5B6D8F1F769F8AD3AFE7C28CBF1D4FBE097A88F44755842")[..]
);
assert_eq!(
pk.public_key.unwrap(),
keypair.public_key.unwrap(),
hex!("19BF44096984CDFE8541BAC167DC3B96C85086AA30B6B6CB0C5C38AD703166E1")
);
}
#[test]
fn decode_public_key() {
let public_key = PublicKeyBytes::from_public_key_der(PUBLIC_KEY_DER).unwrap();
// Extracted with:
// $ openssl pkey -inform der -in pkcs8-v1.der -pubout -text
assert_eq!(
public_key.as_ref(),
&hex!("19BF44096984CDFE8541BAC167DC3B96C85086AA30B6B6CB0C5C38AD703166E1")
);
}
#[cfg(feature = "alloc")]
#[test]
fn encode_pkcs8_v1() {
@@ -61,3 +76,11 @@ fn encode_pkcs8_v2() {
assert_eq!(pk.secret_key, pk2.secret_key);
assert_eq!(pk.public_key, pk2.public_key);
}
#[cfg(feature = "alloc")]
#[test]
fn encode_public_key() {
let pk = PublicKeyBytes::from_public_key_der(PUBLIC_KEY_DER).unwrap();
let pk_der = pk.to_public_key_der().unwrap();
assert_eq!(pk_der.as_ref(), PUBLIC_KEY_DER);
}