From ce5feb730fe98e8529d4d2212e7c2f525ee5d522 Mon Sep 17 00:00:00 2001 From: Arthur Gautier Date: Tue, 1 Oct 2024 23:31:23 -0700 Subject: [PATCH] slh-dsa: adds pkcs8 support --- Cargo.lock | 2 ++ slh-dsa/Cargo.toml | 5 ++- slh-dsa/src/hashes/sha2.rs | 7 +++++ slh-dsa/src/hashes/shake.rs | 7 +++++ slh-dsa/src/lib.rs | 3 ++ slh-dsa/src/signature_encoding.rs | 23 ++++++++++++++ slh-dsa/src/signing_key.rs | 51 +++++++++++++++++++++++++++++++ slh-dsa/src/verifying_key.rs | 38 +++++++++++++++++++++++ slh-dsa/tests/pkcs8.rs | 44 ++++++++++++++++++++++++++ 9 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 slh-dsa/tests/pkcs8.rs diff --git a/Cargo.lock b/Cargo.lock index dd7d4d8a..4de07039 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1095,6 +1095,7 @@ version = "0.2.0-pre" dependencies = [ "aes", "cipher", + "const-oid", "criterion", "ctr", "digest", @@ -1104,6 +1105,7 @@ dependencies = [ "hybrid-array", "num-bigint", "paste", + "pkcs8", "proptest", "quickcheck", "quickcheck_macros", diff --git a/slh-dsa/Cargo.toml b/slh-dsa/Cargo.toml index e3ab2c72..8ec2c90a 100644 --- a/slh-dsa/Cargo.toml +++ b/slh-dsa/Cargo.toml @@ -24,6 +24,8 @@ signature = { version = "2.3.0-pre.4", features = ["rand_core"] } hmac = "=0.13.0-pre.4" sha2 = { version = "=0.11.0-pre.4", default-features = false } digest = "=0.11.0-pre.9" +pkcs8 = { version = "=0.11.0-rc.1", default-features = false } +const-oid = { version = "0.10.0-rc.1", features = ["db"] } [dev-dependencies] hex-literal = "0.4.1" @@ -41,6 +43,7 @@ paste = "1.0.15" rand = "0.8.5" serde_json = "1.0.124" serde = { version = "1.0.207", features = ["derive"] } +pkcs8 = { version = "=0.11.0-rc.1", features = ["pem"] } [lib] bench = false @@ -51,4 +54,4 @@ harness = false [features] alloc = [] -default = ["alloc"] +default = ["alloc", "pkcs8/alloc"] diff --git a/slh-dsa/src/hashes/sha2.rs b/slh-dsa/src/hashes/sha2.rs index 2fa8e19d..5f7ef989 100644 --- a/slh-dsa/src/hashes/sha2.rs +++ b/slh-dsa/src/hashes/sha2.rs @@ -9,6 +9,7 @@ use crate::{ xmss::XmssParams, ParameterSet, }; use crate::{PkSeed, SkPrf, SkSeed}; +use const_oid::db::fips205; use digest::{Digest, KeyInit, Mac}; use hmac::Hmac; use hybrid_array::{Array, ArraySize}; @@ -168,6 +169,7 @@ impl ForsParams for Sha2_128s { } impl ParameterSet for Sha2_128s { const NAME: &'static str = "SLH-DSA-SHA2-128s"; + const ALGORITHM_OID: pkcs8::ObjectIdentifier = fips205::ID_SLH_DSA_SHA_2_128_S; } /// SHA2 at L1 security with fast signatures @@ -190,6 +192,7 @@ impl ForsParams for Sha2_128f { } impl ParameterSet for Sha2_128f { const NAME: &'static str = "SLH-DSA-SHA2-128f"; + const ALGORITHM_OID: pkcs8::ObjectIdentifier = fips205::ID_SLH_DSA_SHA_2_128_F; } /// Implementation of the component hash functions using SHA2 at Security Category 3 and 5 @@ -328,6 +331,7 @@ impl ForsParams for Sha2_192s { } impl ParameterSet for Sha2_192s { const NAME: &'static str = "SLH-DSA-SHA2-192s"; + const ALGORITHM_OID: pkcs8::ObjectIdentifier = fips205::ID_SLH_DSA_SHA_2_192_S; } /// SHA2 at L3 security with fast signatures @@ -350,6 +354,7 @@ impl ForsParams for Sha2_192f { } impl ParameterSet for Sha2_192f { const NAME: &'static str = "SLH-DSA-SHA2-192f"; + const ALGORITHM_OID: pkcs8::ObjectIdentifier = fips205::ID_SLH_DSA_SHA_2_192_F; } /// SHA2 at L5 security with small signatures @@ -372,6 +377,7 @@ impl ForsParams for Sha2_256s { } impl ParameterSet for Sha2_256s { const NAME: &'static str = "SLH-DSA-SHA2-256s"; + const ALGORITHM_OID: pkcs8::ObjectIdentifier = fips205::ID_SLH_DSA_SHA_2_256_S; } /// SHA2 at L5 security with fast signatures @@ -394,4 +400,5 @@ impl ForsParams for Sha2_256f { } impl ParameterSet for Sha2_256f { const NAME: &'static str = "SLH-DSA-SHA2-256f"; + const ALGORITHM_OID: pkcs8::ObjectIdentifier = fips205::ID_SLH_DSA_SHA_2_256_F; } diff --git a/slh-dsa/src/hashes/shake.rs b/slh-dsa/src/hashes/shake.rs index aa267be6..3d21730d 100644 --- a/slh-dsa/src/hashes/shake.rs +++ b/slh-dsa/src/hashes/shake.rs @@ -7,6 +7,7 @@ use crate::hypertree::HypertreeParams; use crate::wots::WotsParams; use crate::xmss::XmssParams; use crate::{ParameterSet, PkSeed, SkPrf, SkSeed}; +use const_oid::db::fips205; use digest::{ExtendableOutput, Update}; use hybrid_array::typenum::consts::{U16, U30, U32}; use hybrid_array::typenum::{U24, U34, U39, U42, U47, U49}; @@ -144,6 +145,7 @@ impl ForsParams for Shake128s { } impl ParameterSet for Shake128s { const NAME: &'static str = "SLH-DSA-SHAKE-128s"; + const ALGORITHM_OID: pkcs8::ObjectIdentifier = fips205::ID_SLH_DSA_SHAKE_128_S; } /// SHAKE256 at L1 security with fast signatures @@ -166,6 +168,7 @@ impl ForsParams for Shake128f { } impl ParameterSet for Shake128f { const NAME: &'static str = "SLH-DSA-SHAKE-128f"; + const ALGORITHM_OID: pkcs8::ObjectIdentifier = fips205::ID_SLH_DSA_SHAKE_128_F; } /// SHAKE256 at L3 security with small signatures @@ -188,6 +191,7 @@ impl ForsParams for Shake192s { } impl ParameterSet for Shake192s { const NAME: &'static str = "SLH-DSA-SHAKE-192s"; + const ALGORITHM_OID: pkcs8::ObjectIdentifier = fips205::ID_SLH_DSA_SHAKE_192_S; } /// SHAKE256 at L3 security with fast signatures @@ -210,6 +214,7 @@ impl ForsParams for Shake192f { } impl ParameterSet for Shake192f { const NAME: &'static str = "SLH-DSA-SHAKE-192f"; + const ALGORITHM_OID: pkcs8::ObjectIdentifier = fips205::ID_SLH_DSA_SHAKE_192_F; } /// SHAKE256 at L5 security with small signatures @@ -232,6 +237,7 @@ impl ForsParams for Shake256s { } impl ParameterSet for Shake256s { const NAME: &'static str = "SLH-DSA-SHAKE-256s"; + const ALGORITHM_OID: pkcs8::ObjectIdentifier = fips205::ID_SLH_DSA_SHAKE_256_S; } /// SHAKE256 at L5 security with fast signatures @@ -254,6 +260,7 @@ impl ForsParams for Shake256f { } impl ParameterSet for Shake256f { const NAME: &'static str = "SLH-DSA-SHAKE-256f"; + const ALGORITHM_OID: pkcs8::ObjectIdentifier = fips205::ID_SLH_DSA_SHAKE_256_F; } #[cfg(test)] diff --git a/slh-dsa/src/lib.rs b/slh-dsa/src/lib.rs index 0887bb3b..869c169a 100644 --- a/slh-dsa/src/lib.rs +++ b/slh-dsa/src/lib.rs @@ -73,6 +73,9 @@ pub trait ParameterSet: { /// Human-readable name for parameter set, matching the FIPS-205 designations const NAME: &'static str; + + /// Associated OID with the Parameter + const ALGORITHM_OID: pkcs8::ObjectIdentifier; } #[cfg(test)] diff --git a/slh-dsa/src/signature_encoding.rs b/slh-dsa/src/signature_encoding.rs index 3d84f3c1..002f7715 100644 --- a/slh-dsa/src/signature_encoding.rs +++ b/slh-dsa/src/signature_encoding.rs @@ -8,8 +8,15 @@ use crate::{fors::ForsSignature, Shake128s}; use ::signature::{Error, SignatureEncoding}; use hybrid_array::sizes::{U16224, U17088, U29792, U35664, U49856, U7856}; use hybrid_array::{Array, ArraySize}; +use pkcs8::{der::AnyRef, spki::AssociatedAlgorithmIdentifier, AlgorithmIdentifierRef}; use typenum::Unsigned; +#[cfg(feature = "alloc")] +use pkcs8::{ + der::{self, asn1::BitString}, + spki::SignatureBitStringEncoding, +}; + #[derive(Debug, Clone, PartialEq, Eq)] /// A parsed SLH-DSA signature for a given parameter set /// @@ -95,6 +102,22 @@ impl SignatureEncoding for Signature

{ } } +#[cfg(feature = "alloc")] +impl SignatureBitStringEncoding for Signature

{ + fn to_bitstring(&self) -> der::Result { + BitString::new(0, self.to_vec()) + } +} + +impl AssociatedAlgorithmIdentifier for Signature

{ + type Params = AnyRef<'static>; + + const ALGORITHM_IDENTIFIER: AlgorithmIdentifierRef<'static> = AlgorithmIdentifierRef { + oid: P::ALGORITHM_OID, + parameters: None, + }; +} + impl From> for Array { fn from(sig: Signature

) -> Array { sig.to_bytes() diff --git a/slh-dsa/src/signing_key.rs b/slh-dsa/src/signing_key.rs index 8f8aa0e8..98a0433f 100644 --- a/slh-dsa/src/signing_key.rs +++ b/slh-dsa/src/signing_key.rs @@ -5,8 +5,18 @@ use crate::verifying_key::VerifyingKey; use crate::{ParameterSet, PkSeed, Sha2L1, Sha2L35, Shake, VerifyingKeyLen}; use ::signature::{Error, KeypairRef, RandomizedSigner, Signer}; use hybrid_array::{Array, ArraySize}; +use pkcs8::{ + der::AnyRef, + spki::{AlgorithmIdentifier, AssociatedAlgorithmIdentifier, SignatureAlgorithmIdentifier}, +}; use typenum::{Unsigned, U, U16, U24, U32}; +#[cfg(feature = "alloc")] +use pkcs8::{ + der::{self, asn1::OctetStringRef}, + EncodePrivateKey, +}; + // NewTypes for ensuring hash argument order correctness #[derive(Clone, PartialEq, Eq, Debug)] pub(crate) struct SkSeed(pub(crate) Array); @@ -216,6 +226,47 @@ impl KeypairRef for SigningKey

{ type VerifyingKey = VerifyingKey

; } +impl

TryFrom> for SigningKey

+where + P: ParameterSet, +{ + type Error = pkcs8::Error; + + fn try_from(private_key_info: pkcs8::PrivateKeyInfoRef<'_>) -> pkcs8::Result { + private_key_info + .algorithm + .assert_algorithm_oid(P::ALGORITHM_OID)?; + + Self::try_from(private_key_info.private_key.as_bytes()) + .map_err(|_| pkcs8::Error::KeyMalformed) + } +} + +#[cfg(feature = "alloc")] +impl

EncodePrivateKey for SigningKey

+where + P: ParameterSet, +{ + fn to_pkcs8_der(&self) -> pkcs8::Result { + let algorithm_identifier = pkcs8::AlgorithmIdentifierRef { + oid: P::ALGORITHM_OID, + parameters: None, + }; + + let private_key = self.to_bytes(); + let pkcs8_key = + pkcs8::PrivateKeyInfoRef::new(algorithm_identifier, OctetStringRef::new(&private_key)?); + Ok(der::SecretDocument::encode_msg(&pkcs8_key)?) + } +} + +impl SignatureAlgorithmIdentifier for SigningKey

{ + type Params = AnyRef<'static>; + + const SIGNATURE_ALGORITHM_IDENTIFIER: AlgorithmIdentifier = + Signature::

::ALGORITHM_IDENTIFIER; +} + impl SigningKeyLen for Sha2L1 { type SkLen = U<{ 4 * 16 }>; } diff --git a/slh-dsa/src/verifying_key.rs b/slh-dsa/src/verifying_key.rs index f67737fb..35bf25c5 100644 --- a/slh-dsa/src/verifying_key.rs +++ b/slh-dsa/src/verifying_key.rs @@ -7,8 +7,12 @@ use crate::Sha2L35; use crate::Shake; use ::signature::{Error, Verifier}; use hybrid_array::{Array, ArraySize}; +use pkcs8::{der, spki}; use typenum::{Unsigned, U, U16, U24, U32}; +#[cfg(feature = "alloc")] +use pkcs8::EncodePublicKey; + /// A trait specifying the length of a serialized verifying key for a given parameter set pub trait VerifyingKeyLen { /// The length of the serialized verifying key in bytes @@ -147,6 +151,40 @@ impl Verifier> for VerifyingKey

{ } } +#[cfg(feature = "alloc")] +impl EncodePublicKey for VerifyingKey

{ + fn to_public_key_der(&self) -> pkcs8::spki::Result { + let algorithm_identifier = pkcs8::AlgorithmIdentifierRef { + oid: P::ALGORITHM_OID, + parameters: None, + }; + + let public_key = self.to_bytes(); + let subject_public_key = der::asn1::BitStringRef::new(0, &public_key)?; + + pkcs8::SubjectPublicKeyInfo { + algorithm: algorithm_identifier, + subject_public_key, + } + .try_into() + } +} + +impl TryFrom> for VerifyingKey

{ + type Error = spki::Error; + + fn try_from(spki: pkcs8::SubjectPublicKeyInfoRef<'_>) -> spki::Result { + spki.algorithm.assert_algorithm_oid(P::ALGORITHM_OID)?; + + Ok(Self::try_from( + spki.subject_public_key + .as_bytes() + .ok_or_else(|| der::Tag::BitString.value_error())?, + ) + .map_err(|_| pkcs8::Error::KeyMalformed)?) + } +} + impl VerifyingKeyLen for Sha2L1 { type VkLen = U<32>; } diff --git a/slh-dsa/tests/pkcs8.rs b/slh-dsa/tests/pkcs8.rs new file mode 100644 index 00000000..230d1ffc --- /dev/null +++ b/slh-dsa/tests/pkcs8.rs @@ -0,0 +1,44 @@ +#![cfg(feature = "alloc")] + +use hex_literal::hex; +use pkcs8::{DecodePrivateKey, EncodePrivateKey, EncodePublicKey, LineEnding}; +use slh_dsa::{Sha2_128s, SigningKey, VerifyingKey}; +use std::ops::Deref; + +// Serialization of the SLH-DSA keys is still a draft +// The vectors used here are taken from the draft-ietf-lamps-x509-slhdsa +// https://github.com/lamps-wg/x509-slhdsa/commit/128c68b6b141e109e3e0ec8f3f47c832a4baaa30 +#[test] +fn pkcs8_output() { + let signing = SigningKey::::try_from(&hex!("A2263BCA45860836523160049523D621677FAD90D51EB6067A327E0D1E64A5012B8109EC777CAA4E1F024CCFCF9497D99180509280F4256AF2B07AF80289B494")[..]).unwrap(); + + let out = signing.to_pkcs8_pem(LineEnding::LF).unwrap(); + + // https://github.com/lamps-wg/x509-slhdsa/blob/main/id-slh-dsa-sha2-128s.priv + assert_eq!( + out.deref(), + r#"-----BEGIN PRIVATE KEY----- +MFICAQAwCwYJYIZIAWUDBAMUBECiJjvKRYYINlIxYASVI9YhZ3+tkNUetgZ6Mn4N +HmSlASuBCex3fKpOHwJMz8+Ul9mRgFCSgPQlavKwevgCibSU +-----END PRIVATE KEY----- +"# + ); + + let parsed = SigningKey::::from_pkcs8_pem(out.deref()).unwrap(); + + assert_eq!(parsed, signing); + + let public: VerifyingKey = parsed.as_ref().clone(); + + let out = public.to_public_key_pem(LineEnding::LF).unwrap(); + + // https://github.com/lamps-wg/x509-slhdsa/blob/main/id-slh-dsa-sha2-128s.pub + assert_eq!( + out.deref(), + r#"-----BEGIN PUBLIC KEY----- +MDAwCwYJYIZIAWUDBAMUAyEAK4EJ7Hd8qk4fAkzPz5SX2ZGAUJKA9CVq8rB6+AKJ +tJQ= +-----END PUBLIC KEY----- +"# + ); +}