Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion x25519-dalek/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ serde = { version = "1", default-features = false, optional = true, features = [
"derive",
] }
zeroize = { version = "1", default-features = false, optional = true }
pkcs8 = { version = "0.11.0-rc.8", optional = true }

[dev-dependencies]
bincode = "1"
Expand All @@ -63,7 +64,9 @@ default = ["alloc", "precomputed-tables", "zeroize"]
os_rng = ["rand_core/os_rng"]
zeroize = ["dep:zeroize", "curve25519-dalek/zeroize"]
serde = ["dep:serde", "curve25519-dalek/serde"]
alloc = ["curve25519-dalek/alloc", "serde?/alloc", "zeroize?/alloc"]
alloc = ["curve25519-dalek/alloc", "serde?/alloc", "zeroize?/alloc", "pkcs8?/alloc"]
precomputed-tables = ["curve25519-dalek/precomputed-tables"]
reusable_secrets = []
static_secrets = []
pkcs8 = ["dep:pkcs8"]
pem = ["alloc", "pkcs8/pem", "pkcs8"]
99 changes: 99 additions & 0 deletions x25519-dalek/src/x25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

use curve25519_dalek::{edwards::EdwardsPoint, montgomery::MontgomeryPoint, traits::IsIdentity};

#[cfg(all(feature = "alloc", feature = "pkcs8"))]
use pkcs8::{EncodePrivateKey, SecretDocument, der::asn1::OctetStringRef};
#[cfg(feature = "pkcs8")]
use pkcs8::{ObjectIdentifier, PrivateKeyInfoRef};
use rand_core::CryptoRng;
#[cfg(feature = "os_rng")]
use rand_core::TryRngCore;
Expand Down Expand Up @@ -392,3 +396,98 @@ pub fn x25519(k: [u8; 32], u: [u8; 32]) -> [u8; 32] {
pub const X25519_BASEPOINT_BYTES: [u8; 32] = [
9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];

/// Algorithm [`ObjectIdentifier`] for the X25519 digital signature algorithm
/// (`id-X25519`).
///
/// <http://oid-info.com/get/1.3.101.110>
#[cfg(feature = "pkcs8")]
pub const ALGORITHM_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.110");

/// X25519 Algorithm Identifier.
#[cfg(feature = "pkcs8")]
pub const ALGORITHM_ID: pkcs8::AlgorithmIdentifierRef<'static> = pkcs8::AlgorithmIdentifierRef {
oid: ALGORITHM_OID,
parameters: None,
};

#[cfg(all(feature = "alloc", feature = "pkcs8"))]
impl EncodePrivateKey for EphemeralSecret {
fn to_pkcs8_der(&self) -> Result<SecretDocument, pkcs8::Error> {
to_pkcs8_der(&self.0, &PublicKey::from(self).0.0)
}
}

#[cfg(all(feature = "alloc", feature = "pkcs8", feature = "static_secrets"))]
impl EncodePrivateKey for StaticSecret {
fn to_pkcs8_der(&self) -> Result<SecretDocument, pkcs8::Error> {
to_pkcs8_der(&self.0, &PublicKey::from(self).0.0)
}
}

#[cfg(all(feature = "alloc", feature = "pkcs8"))]
fn to_pkcs8_der(
private_key_bytes: &[u8; 32],
public_key_bytes: &[u8; 32],
) -> Result<SecretDocument, pkcs8::Error> {
// Serialize private key as nested OCTET STRING
let mut private_key = [0u8; 2 + 32];
private_key[0] = 0x04;
private_key[1] = 0x20;
private_key[2..].copy_from_slice(private_key_bytes);

let private_key_info = PrivateKeyInfoRef {
algorithm: ALGORITHM_ID,
private_key: OctetStringRef::new(&private_key)?,
public_key: Some(pkcs8::der::asn1::BitStringRef::new(0, public_key_bytes)?),
};

let result = SecretDocument::encode_msg(&private_key_info)?;

#[cfg(feature = "zeroize")]
private_key.zeroize();

Ok(result)
}

#[cfg(feature = "pkcs8")]
impl TryFrom<PrivateKeyInfoRef<'_>> for EphemeralSecret {
type Error = pkcs8::Error;

fn try_from(private_key: PrivateKeyInfoRef<'_>) -> Result<Self, pkcs8::Error> {
Ok(Self(to_private_key_bytes(private_key)?))
}
}

#[cfg(all(feature = "pkcs8", feature = "static_secrets"))]
impl TryFrom<PrivateKeyInfoRef<'_>> for StaticSecret {
type Error = pkcs8::Error;

fn try_from(private_key: PrivateKeyInfoRef<'_>) -> Result<Self, pkcs8::Error> {
Ok(Self(to_private_key_bytes(private_key)?))
}
}

#[cfg(feature = "pkcs8")]
fn to_private_key_bytes(private_key: PrivateKeyInfoRef<'_>) -> Result<[u8; 32], pkcs8::Error> {
private_key.algorithm.assert_algorithm_oid(ALGORITHM_OID)?;

if private_key.algorithm.parameters.is_some() {
return Err(pkcs8::Error::ParametersMalformed);
}

// X25519 PKCS#8 keys are represented as a nested OCTET STRING
// (i.e. an OCTET STRING within an OCTET STRING).
//
// This match statement checks and removes the inner OCTET STRING
// header value:
//
// - 0x04: OCTET STRING tag
// - 0x20: 32-byte length
let private_key_bytes = match private_key.private_key.as_bytes() {
[0x04, 0x20, rest @ ..] => rest.try_into().map_err(|_| pkcs8::Error::KeyMalformed),
_ => Err(pkcs8::Error::KeyMalformed),
}?;

Ok(private_key_bytes)
}
98 changes: 98 additions & 0 deletions x25519-dalek/tests/x25519_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,101 @@ mod os_rng {
StaticSecret::random();
}
}

#[cfg(feature = "pkcs8")]
mod pkcs8 {

use ::pkcs8::DecodePrivateKey;
use ::pkcs8::EncodePrivateKey;
use ::pkcs8::LineEnding;

#[cfg(feature = "pem")]
const PEM_DATA: &str = "-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VuBCIEIAgFWQfJv7DnZqs7W/aHM+aa5kXnFTlLQAso2qIAJyVT
-----END PRIVATE KEY-----
";

#[cfg(feature = "pem")]
const PEM_DATA_WITH_PUBLIC_KEY: &str = "-----BEGIN PRIVATE KEY-----
MFECAQEwBQYDK2VuBCIEIAgFWQfJv7DnZqs7W/aHM+aa5kXnFTlLQAso2qIAJyVT
gSEAtKtoeuz21PdNOS7LH5srafvb2Hio7LaogF8aUZ+yrA4=
-----END PRIVATE KEY-----
";

#[test]
#[cfg(feature = "pem")]
fn decode_encode_pem() {
let private_key = x25519_dalek::EphemeralSecret::from_pkcs8_pem(PEM_DATA).unwrap();
assert_eq!(
PEM_DATA_WITH_PUBLIC_KEY,
private_key.to_pkcs8_pem(LineEnding::LF).unwrap().as_str()
);
let private_key =
x25519_dalek::EphemeralSecret::from_pkcs8_pem(PEM_DATA_WITH_PUBLIC_KEY).unwrap();
assert_eq!(
PEM_DATA_WITH_PUBLIC_KEY,
private_key.to_pkcs8_pem(LineEnding::LF).unwrap().as_str()
);

#[cfg(feature = "static_secrets")]
let private_key = x25519_dalek::StaticSecret::from_pkcs8_pem(PEM_DATA).unwrap();
#[cfg(feature = "static_secrets")]
assert_eq!(
PEM_DATA_WITH_PUBLIC_KEY,
private_key.to_pkcs8_pem(LineEnding::LF).unwrap().as_str()
);
#[cfg(feature = "static_secrets")]
let private_key =
x25519_dalek::StaticSecret::from_pkcs8_pem(PEM_DATA_WITH_PUBLIC_KEY).unwrap();
#[cfg(feature = "static_secrets")]
assert_eq!(
PEM_DATA_WITH_PUBLIC_KEY,
private_key.to_pkcs8_pem(LineEnding::LF).unwrap().as_str()
);
}

const DER_DATA: &[u8] = &[
0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04,
0x20, 0x08, 0x05, 0x59, 0x07, 0xc9, 0xbf, 0xb0, 0xe7, 0x66, 0xab, 0x3b, 0x5b, 0xf6, 0x87,
0x33, 0xe6, 0x9a, 0xe6, 0x45, 0xe7, 0x15, 0x39, 0x4b, 0x40, 0x0b, 0x28, 0xda, 0xa2, 0x00,
0x27, 0x25, 0x53,
];

const DER_DATA_WITH_PUBLIC_KEY: &[u8] = &[
0x30, 0x51, 0x02, 0x01, 0x01, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, 0x04, 0x22, 0x04,
0x20, 0x08, 0x05, 0x59, 0x07, 0xc9, 0xbf, 0xb0, 0xe7, 0x66, 0xab, 0x3b, 0x5b, 0xf6, 0x87,
0x33, 0xe6, 0x9a, 0xe6, 0x45, 0xe7, 0x15, 0x39, 0x4b, 0x40, 0x0b, 0x28, 0xda, 0xa2, 0x00,
0x27, 0x25, 0x53, 0x81, 0x21, 0x00, 0xb4, 0xab, 0x68, 0x7a, 0xec, 0xf6, 0xd4, 0xf7, 0x4d,
0x39, 0x2e, 0xcb, 0x1f, 0x9b, 0x2b, 0x69, 0xfb, 0xdb, 0xd8, 0x78, 0xa8, 0xec, 0xb6, 0xa8,
0x80, 0x5f, 0x1a, 0x51, 0x9f, 0xb2, 0xac, 0x0e,
];

#[test]
fn decode_encode_der() {
let private_key = x25519_dalek::EphemeralSecret::from_pkcs8_der(DER_DATA).unwrap();
assert_eq!(
DER_DATA_WITH_PUBLIC_KEY,
private_key.to_pkcs8_der().unwrap().as_bytes()
);
let private_key =
x25519_dalek::EphemeralSecret::from_pkcs8_der(DER_DATA_WITH_PUBLIC_KEY).unwrap();
assert_eq!(
DER_DATA_WITH_PUBLIC_KEY,
private_key.to_pkcs8_der().unwrap().as_bytes()
);

#[cfg(feature = "static_secrets")]
let private_key = x25519_dalek::StaticSecret::from_pkcs8_der(DER_DATA).unwrap();
assert_eq!(
DER_DATA_WITH_PUBLIC_KEY,
private_key.to_pkcs8_der().unwrap().as_bytes()
);
#[cfg(feature = "static_secrets")]
let private_key =
x25519_dalek::StaticSecret::from_pkcs8_der(DER_DATA_WITH_PUBLIC_KEY).unwrap();
assert_eq!(
DER_DATA_WITH_PUBLIC_KEY,
private_key.to_pkcs8_der().unwrap().as_bytes()
);
}
}