diff --git a/Cargo.toml b/Cargo.toml index 7f4d711..9f90b39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "certkit" -version = "0.1.1" +version = "0.1.2" edition = "2024" license = "MIT OR Apache-2.0" description = "A pure Rust library for X.509 certificate management, creation, and validation, supporting RSA, ECDSA, and Ed25519 keys, with no OpenSSL or ring dependencies." @@ -12,22 +12,28 @@ keywords = ["x509", "certificate", "crypto", "pki", "tls"] categories = ["cryptography", "authentication"] authors = ["Nick Cardin "] +[features] +default = ["rsa", "p256", "p384", "p521","ed25519"] +p521 = ["p384", "dep:p521", "ecdsa"] #For some reason p521 does not compile without p384... +ed25519 = ["ed25519-dalek"] + + [dependencies] bon = "3" const-oid = { version = "0.9.6", features = ["db"] } -rsa = { version = "0.9" } -p256 = { version = "0.13", features = ["ecdsa", "pkcs8"] } -p384 = { version = "0.13", features = ["ecdsa", "pkcs8"] } -p521 = { version = "0.13", features = ["ecdsa", "pkcs8"] } -ecdsa = { version = "0.16", features = ["verifying"] } -ed25519-dalek = { version = "2", features = ["rand_core", "pkcs8", "pem"] } +rsa = { version = "0.9", optional = true } +p256 = { version = "0.13", features = ["ecdsa", "pkcs8"], optional = true } +p384 = { version = "0.13", features = ["ecdsa", "pkcs8"], optional = true } +p521 = { version = "0.13", features = ["ecdsa", "pkcs8"], optional = true } +ecdsa = { version = "0.16", features = ["verifying"], optional = true } +ed25519-dalek = { version = "2", features = ["rand_core", "pkcs8", "pem"], optional = true} sha2 = { version = "0.10", default-features = false, features = ["oid"] } -rand_core = { version = "0.6" } +rand_core = { version = "0.6", features = ["getrandom"]} der = "0.7" time = "0.3" pem = "3" x509-cert = "0.2.5" -pkcs8 = "0.10.2" +pkcs8 = { version = "0.10.2", features = ["alloc", "pem"] } rand = "0.9.1" base64 = "0.22.1" sha1 = "0.10" diff --git a/compile_feature_combinations.sh b/compile_feature_combinations.sh new file mode 100755 index 0000000..7bfcf5c --- /dev/null +++ b/compile_feature_combinations.sh @@ -0,0 +1,18 @@ +#/usr/bin/env bash +cargo build + +cargo build --no-default-features --features rsa +cargo build --no-default-features --features p256 +cargo build --no-default-features --features p384 +cargo build --no-default-features --features p521 +cargo build --no-default-features --features ed25519 + +cargo build --no-default-features --features rsa,p256 +cargo build --no-default-features --features rsa,ed25519 +cargo build --no-default-features --features p256,ed25519 +cargo build --no-default-features --features p521,p256 +cargo build --no-default-features --features rsa,p521 +cargo build --no-default-features --features rsa,ed25519 +cargo build --no-default-features --features p521,ed25519 + + diff --git a/src/error.rs b/src/error.rs index baaad9a..723093a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -191,12 +191,14 @@ impl From for CertKitError { } } +#[cfg(feature = "rsa")] impl From for CertKitError { fn from(err: rsa::Error) -> Self { CertKitError::RsaError(err.to_string()) } } +#[cfg(feature = "rsa")] impl From for CertKitError { fn from(err: rsa::pkcs1::Error) -> Self { CertKitError::RsaPkcs1Error(err.to_string()) diff --git a/src/issuer.rs b/src/issuer.rs index cc3b02a..d4d1b53 100644 --- a/src/issuer.rs +++ b/src/issuer.rs @@ -212,10 +212,15 @@ pub trait Issuer { /// ``` fn issue(&self, cert_request: &CertificationRequestInfo, validity: Validity) -> Certificate { let signature_algo = match self.signing_key() { + #[cfg(feature = "rsa")] KeyPair::Rsa { .. } => SignatureAlgorithm::Sha256WithRSA, + #[cfg(feature = "p256")] KeyPair::EcdsaP256 { .. } => SignatureAlgorithm::Sha256WithECDSA, + #[cfg(feature = "p384")] KeyPair::EcdsaP384 { .. } => SignatureAlgorithm::Sha256WithECDSA, + #[cfg(feature = "p521")] KeyPair::EcdsaP521 { .. } => SignatureAlgorithm::Sha256WithECDSA, + #[cfg(feature = "ed25519")] KeyPair::Ed25519 { .. } => SignatureAlgorithm::Sha256WithEdDSA, }; diff --git a/src/key.rs b/src/key.rs index 0b59839..fb19dea 100644 --- a/src/key.rs +++ b/src/key.rs @@ -2,21 +2,33 @@ use crate::error::CertKitError; use der::pem::LineEnding; pub type Result = std::result::Result; +#[cfg(feature = "p521")] use ecdsa::VerifyingKey; +#[cfg(feature = "ed25519")] use ed25519_dalek::SigningKey as Ed25519SigningKey; +#[cfg(feature = "ed25519")] use ed25519_dalek::VerifyingKey as Ed25519VerifyingKey; +#[cfg(feature = "p256")] use p256::ecdsa::{SigningKey as P256SigningKey, VerifyingKey as P256VerifyingKey}; +#[cfg(feature = "p384")] use p384::ecdsa::{SigningKey as P384SigningKey, VerifyingKey as P384VerifyingKey}; +#[cfg(feature = "p521")] use p521::NistP521; +#[cfg(feature = "p521")] use p521::ecdsa::SigningKey as P521SigningKey; +#[cfg(feature = "rsa")] use rsa::pkcs1v15::SigningKey as RsaSigningKey; +#[cfg(feature = "rsa")] use rsa::signature::SignatureEncoding; +#[cfg(feature = "rsa")] use rsa::signature::Signer as RsaSigner; +#[cfg(feature = "rsa")] use rsa::{ RsaPrivateKey, RsaPublicKey, pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey, EncodeRsaPublicKey}, }; -use sha2::Sha256; +#[cfg(feature = "rsa")] +use sha2::Sha256; //only used with RSA keys. /// Supported key types for certificate operations. /// @@ -66,6 +78,7 @@ pub enum KeyPair { /// # Fields /// * `private` - The private key. /// * `public` - The public key. + #[cfg(feature = "rsa")] Rsa { private: Box, public: RsaPublicKey, @@ -75,6 +88,7 @@ pub enum KeyPair { /// # Fields /// * `signing_key` - The signing key. /// * `verifying_key` - The verifying key. + #[cfg(feature = "p256")] EcdsaP256 { signing_key: P256SigningKey, verifying_key: P256VerifyingKey, @@ -84,6 +98,7 @@ pub enum KeyPair { /// # Fields /// * `signing_key` - The signing key. /// * `verifying_key` - The verifying key. + #[cfg(feature = "p384")] EcdsaP384 { signing_key: P384SigningKey, verifying_key: P384VerifyingKey, @@ -93,6 +108,7 @@ pub enum KeyPair { /// # Fields /// * `signing_key` - The signing key. /// * `verifying_key` - The verifying key. + #[cfg(feature = "p521")] EcdsaP521 { signing_key: ecdsa::SigningKey, verifying_key: ecdsa::VerifyingKey, @@ -101,10 +117,11 @@ pub enum KeyPair { /// /// # Fields /// * `signing_key` - The signing key. + #[cfg(feature = "ed25519")] Ed25519 { signing_key: Ed25519SigningKey }, } -use p256::pkcs8::DecodePrivateKey; -use pkcs8::EncodePrivateKey; + +use pkcs8::{EncodePrivateKey, PrivateKeyInfo}; impl KeyPair { /// Generate an RSA key pair with the specified number of bits. @@ -147,6 +164,7 @@ impl KeyPair { /// - 2048-bit keys are considered secure for most applications /// - 3072-bit keys provide additional security margin /// - 4096-bit keys offer maximum security but with performance trade-offs + #[cfg(feature = "rsa")] pub fn generate_rsa(bits: usize) -> Result { let mut rng = rand_core::OsRng; let private = RsaPrivateKey::new(&mut rng, bits)?; @@ -187,6 +205,7 @@ impl KeyPair { /// - Widely supported across different systems and libraries /// - Smaller key and signature sizes compared to RSA /// - Fast signature generation and verification + #[cfg(feature = "p256")] pub fn generate_ecdsa_p256() -> Self { let mut rng = rand_core::OsRng; let signing_key = P256SigningKey::random(&mut rng); @@ -224,6 +243,7 @@ impl KeyPair { /// - Higher security level than P-256 /// - Suitable for high-security applications /// - Slightly larger signatures than P-256 + #[cfg(feature = "p384")] pub fn generate_ecdsa_p384() -> Self { let mut rng = rand_core::OsRng; let signing_key = P384SigningKey::random(&mut rng); @@ -261,6 +281,7 @@ impl KeyPair { /// - Highest security level among NIST curves /// - Suitable for applications requiring maximum security /// - Larger key and signature sizes than P-256/P-384 + #[cfg(feature = "p521")] pub fn generate_ecdsa_p521() -> Self { let mut rng = rand_core::OsRng; let signing_key: ecdsa::SigningKey = @@ -303,6 +324,7 @@ impl KeyPair { /// - Resistant to side-channel attacks /// - Fixed 32-byte public keys and 64-byte signatures /// - Deterministic signatures (no random nonce required) + #[cfg(feature = "ed25519")] pub fn generate_ed25519() -> Self { let mut rng = rand_core::OsRng; let signing_key: Ed25519SigningKey = Ed25519SigningKey::generate(&mut rng); @@ -343,10 +365,15 @@ impl KeyPair { /// ``` pub fn get_public_key_der(&self) -> Vec { match self { + #[cfg(feature = "rsa")] KeyPair::Rsa { public, .. } => public.to_pkcs1_der().unwrap().as_bytes().to_vec(), + #[cfg(feature = "p256")] KeyPair::EcdsaP256 { verifying_key, .. } => verifying_key.to_sec1_bytes().to_vec(), + #[cfg(feature = "p384")] KeyPair::EcdsaP384 { verifying_key, .. } => verifying_key.to_sec1_bytes().to_vec(), + #[cfg(feature = "p521")] KeyPair::EcdsaP521 { verifying_key, .. } => verifying_key.to_sec1_bytes().to_vec(), + #[cfg(feature = "ed25519")] KeyPair::Ed25519 { signing_key } => signing_key.verifying_key().to_bytes().to_vec(), } } @@ -366,16 +393,21 @@ impl KeyPair { /// ``` pub fn encode_private_key_pem(&self) -> Result { let key = (match &self { + #[cfg(feature = "rsa")] KeyPair::Rsa { private, .. } => private.to_pkcs8_pem(LineEnding::default()), + #[cfg(feature = "p256")] KeyPair::EcdsaP256 { signing_key, .. } => { EncodePrivateKey::to_pkcs8_pem(signing_key, LineEnding::default()) } + #[cfg(feature = "p384")] KeyPair::EcdsaP384 { signing_key, .. } => { EncodePrivateKey::to_pkcs8_pem(signing_key, LineEnding::default()) } + #[cfg(feature = "p521")] KeyPair::EcdsaP521 { signing_key, .. } => { EncodePrivateKey::to_pkcs8_pem(signing_key, LineEnding::default()) } + #[cfg(feature = "ed25519")] KeyPair::Ed25519 { signing_key, .. } => { EncodePrivateKey::to_pkcs8_pem(signing_key, LineEnding::default()) } @@ -427,6 +459,7 @@ impl KeyPair { /// ``` pub fn import_from_der(der: &[u8]) -> Result { // Try RSA PKCS#1 first + #[cfg(feature = "rsa")] if let (Ok(private), Ok(public)) = ( RsaPrivateKey::from_pkcs1_der(der), RsaPublicKey::from_pkcs1_der(der), @@ -436,8 +469,14 @@ impl KeyPair { public, }); } + + let private_key_info = PrivateKeyInfo::try_from(der).map_err(|_| { + CertKitError::DecodingError("Unsupported or invalid key DER encoding".to_string()) + })?; + // Try RSA PKCS#8 - if let Ok(private) = RsaPrivateKey::from_pkcs8_der(der) { + #[cfg(feature = "rsa")] + if let Ok(private) = RsaPrivateKey::try_from(private_key_info.clone()) { let public = RsaPublicKey::from(&private); return Ok(KeyPair::Rsa { private: Box::new(private), @@ -446,7 +485,8 @@ impl KeyPair { } // Try ECDSA P-256 PKCS#8 - if let Ok(signing_key) = P256SigningKey::from_pkcs8_der(der) { + #[cfg(feature = "p256")] + if let Ok(signing_key) = P256SigningKey::try_from(private_key_info.clone()) { let verifying_key = signing_key.verifying_key().to_owned(); return Ok(KeyPair::EcdsaP256 { signing_key, @@ -454,7 +494,8 @@ impl KeyPair { }); } // Try ECDSA P-384 PKCS#8 - if let Ok(signing_key) = P384SigningKey::from_pkcs8_der(der) { + #[cfg(feature = "p384")] + if let Ok(signing_key) = P384SigningKey::try_from(private_key_info.clone()) { let verifying_key = signing_key.verifying_key().to_owned(); return Ok(KeyPair::EcdsaP384 { signing_key, @@ -462,7 +503,8 @@ impl KeyPair { }); } // Try ECDSA P-521 PKCS#8 - if let Ok(signing_key) = ecdsa::SigningKey::::from_pkcs8_der(der) { + #[cfg(feature = "p521")] + if let Ok(signing_key) = ecdsa::SigningKey::::try_from(private_key_info.clone()) { let verifying_key = signing_key.verifying_key().to_owned(); return Ok(KeyPair::EcdsaP521 { signing_key, @@ -471,9 +513,11 @@ impl KeyPair { } // Try Ed25519 - if let Ok(signing_key) = Ed25519SigningKey::from_pkcs8_der(der) { + #[cfg(feature = "ed25519")] + if let Ok(signing_key) = Ed25519SigningKey::try_from(private_key_info.clone()) { return Ok(KeyPair::Ed25519 { signing_key }); } + Err(CertKitError::DecodingError( "Unsupported or invalid key DER encoding".to_string(), )) @@ -567,18 +611,23 @@ impl KeyPair { /// - Ed25519: id-Ed25519 (1.3.101.112) pub fn as_spki(&self) -> x509_cert::spki::SubjectPublicKeyInfoOwned { match self { + #[cfg(feature = "rsa")] KeyPair::Rsa { public, .. } => { x509_cert::spki::SubjectPublicKeyInfoOwned::from_key(public.clone()).unwrap() } + #[cfg(feature = "p256")] KeyPair::EcdsaP256 { verifying_key, .. } => { x509_cert::spki::SubjectPublicKeyInfoOwned::from_key(*verifying_key).unwrap() } + #[cfg(feature = "p384")] KeyPair::EcdsaP384 { verifying_key, .. } => { x509_cert::spki::SubjectPublicKeyInfoOwned::from_key(*verifying_key).unwrap() } + #[cfg(feature = "p521")] KeyPair::EcdsaP521 { verifying_key, .. } => { x509_cert::spki::SubjectPublicKeyInfoOwned::from_key(*verifying_key).unwrap() } + #[cfg(feature = "ed25519")] KeyPair::Ed25519 { signing_key } => { let pk_bytes = signing_key.verifying_key().to_bytes(); x509_cert::spki::SubjectPublicKeyInfoOwned { @@ -646,27 +695,35 @@ impl KeyPair { /// - All algorithms provide strong security when used properly pub fn sign_data(&self, data: &[u8]) -> Result> { match self { + #[cfg(feature = "rsa")] KeyPair::Rsa { private, .. } => { // Using RSA-PKCS1v15 (in a real implementation you’d choose a proper hash algorithm) let signing_key: RsaSigningKey = RsaSigningKey::new(*(private.clone())); let signature = signing_key.sign(data); Ok(signature.to_vec()) } + #[cfg(feature = "p256")] KeyPair::EcdsaP256 { signing_key, .. } => { - let signature: p256::ecdsa::Signature = signing_key.sign(data); + let signature: p256::ecdsa::Signature = + p256::ecdsa::signature::Signer::sign(signing_key, data); Ok(signature.to_vec()) } + #[cfg(feature = "p384")] KeyPair::EcdsaP384 { signing_key, .. } => { - let signature: p384::ecdsa::Signature = signing_key.sign(data); + let signature: p384::ecdsa::Signature = + p384::ecdsa::signature::Signer::sign(signing_key, data); Ok(signature.to_vec()) } + #[cfg(feature = "p521")] KeyPair::EcdsaP521 { signing_key, .. } => { let skey: P521SigningKey = signing_key.clone().into(); - - let signature: p521::ecdsa::Signature = skey.sign(data); + let signature: p521::ecdsa::Signature = + p521::ecdsa::signature::Signer::sign(&skey, data); Ok(signature.to_vec()) } + #[cfg(feature = "ed25519")] KeyPair::Ed25519 { signing_key } => { + use ed25519_dalek::Signer; let signature = signing_key.sign(data); Ok(signature.to_bytes().to_vec()) } @@ -709,14 +766,19 @@ impl KeyPair { #[derive(Debug, Clone)] pub enum PublicKey { /// RSA public key. + #[cfg(feature = "rsa")] Rsa(RsaPublicKey), /// ECDSA P-256 public key. + #[cfg(feature = "p256")] EcdsaP256(P256VerifyingKey), /// ECDSA P-384 public key. + #[cfg(feature = "p384")] EcdsaP384(P384VerifyingKey), /// ECDSA P-521 public key. + #[cfg(feature = "p521")] EcdsaP521(VerifyingKey), /// Ed25519 public key. + #[cfg(feature = "ed25519")] Ed25519(Ed25519VerifyingKey), } @@ -751,12 +813,15 @@ impl PublicKey { /// ``` pub fn to_der(&self) -> Result> { match self { + #[cfg(feature = "rsa")] PublicKey::Rsa(public) => Ok(public.to_pkcs1_der()?.as_bytes().to_vec()), - PublicKey::EcdsaP256(verifying_key) => { - Ok(verifying_key.to_pkcs1_der()?.as_bytes().to_vec()) - } + #[cfg(feature = "p256")] + PublicKey::EcdsaP256(verifying_key) => Ok(verifying_key.to_sec1_bytes().to_vec()), + #[cfg(feature = "p384")] PublicKey::EcdsaP384(verifying_key) => Ok(verifying_key.to_sec1_bytes().to_vec()), + #[cfg(feature = "p521")] PublicKey::EcdsaP521(verifying_key) => Ok(verifying_key.to_sec1_bytes().to_vec()), + #[cfg(feature = "ed25519")] PublicKey::Ed25519(verifying_key) => Ok(verifying_key.to_bytes().to_vec()), } } @@ -799,6 +864,7 @@ impl PublicKey { /// # Limitations /// Currently only supports RSA public keys. ECDSA and Ed25519 support /// will be added in future versions. + #[cfg(feature = "rsa")] pub fn from_der(der: &[u8]) -> Result { let public = RsaPublicKey::from_pkcs1_der(der)?; Ok(PublicKey::Rsa(public)) @@ -843,10 +909,15 @@ impl PublicKey { /// - Storing public keys separately from private keys pub fn from_key_pair(key_pair: &KeyPair) -> Self { match key_pair { + #[cfg(feature = "rsa")] KeyPair::Rsa { public, .. } => PublicKey::Rsa(public.clone()), + #[cfg(feature = "p256")] KeyPair::EcdsaP256 { verifying_key, .. } => PublicKey::EcdsaP256(*verifying_key), + #[cfg(feature = "p384")] KeyPair::EcdsaP384 { verifying_key, .. } => PublicKey::EcdsaP384(*verifying_key), + #[cfg(feature = "p521")] KeyPair::EcdsaP521 { verifying_key, .. } => PublicKey::EcdsaP521(*verifying_key), + #[cfg(feature = "ed25519")] KeyPair::Ed25519 { signing_key, .. } => PublicKey::Ed25519(signing_key.verifying_key()), } } @@ -900,20 +971,18 @@ impl PublicKey { /// - Validating certificate chains /// - Interoperability with other X.509 implementations pub fn from_x509spki(spki: &x509_cert::spki::SubjectPublicKeyInfoOwned) -> Result { - use const_oid::db::{ - rfc5912::{ID_EC_PUBLIC_KEY, RSA_ENCRYPTION, SECP_256_R_1, SECP_384_R_1, SECP_521_R_1}, - rfc8410::ID_ED_25519, - }; - use der::asn1::ObjectIdentifier; match spki.algorithm.oid { - RSA_ENCRYPTION => { + #[cfg(feature = "rsa")] + const_oid::db::rfc5912::RSA_ENCRYPTION => { let pk_bytes = spki.subject_public_key.as_bytes().ok_or_else(|| { CertKitError::DecodingError("Invalid RSA public key bitstring".to_string()) })?; let public_key = RsaPublicKey::from_pkcs1_der(pk_bytes)?; Ok(PublicKey::Rsa(public_key)) } - ID_EC_PUBLIC_KEY => { + #[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] + const_oid::db::rfc5912::ID_EC_PUBLIC_KEY => { + use der::asn1::ObjectIdentifier; let params = spki.algorithm.parameters.as_ref().ok_or_else(|| { CertKitError::DecodingError("Missing EC parameters".to_string()) })?; @@ -923,37 +992,46 @@ impl PublicKey { let raw_bytes = spki.subject_public_key.as_bytes().ok_or_else(|| { CertKitError::DecodingError("Invalid EC public key bitstring".to_string()) })?; - if params_oid == SECP_256_R_1 { - let verifying_key = - P256VerifyingKey::from_sec1_bytes(raw_bytes).map_err(|_| { - CertKitError::DecodingError( - "Invalid P-256 public key bytes".to_string(), - ) - })?; - Ok(PublicKey::EcdsaP256(verifying_key)) - } else if params_oid == SECP_384_R_1 { - let verifying_key = - P384VerifyingKey::from_sec1_bytes(raw_bytes).map_err(|_| { - CertKitError::DecodingError( - "Invalid P-384 public key bytes".to_string(), - ) - })?; - Ok(PublicKey::EcdsaP384(verifying_key)) - } else if params_oid == SECP_521_R_1 { - let verifying_key = ecdsa::VerifyingKey::::from_sec1_bytes(raw_bytes) + match params_oid { + #[cfg(feature = "p256")] + const_oid::db::rfc5912::SECP_256_R_1 => { + let verifying_key = + P256VerifyingKey::from_sec1_bytes(raw_bytes).map_err(|_| { + CertKitError::DecodingError( + "Invalid P-256 public key bytes".to_string(), + ) + })?; + Ok(PublicKey::EcdsaP256(verifying_key)) + } + #[cfg(feature = "p384")] + const_oid::db::rfc5912::SECP_384_R_1 => { + let verifying_key = + P384VerifyingKey::from_sec1_bytes(raw_bytes).map_err(|_| { + CertKitError::DecodingError( + "Invalid P-384 public key bytes".to_string(), + ) + })?; + Ok(PublicKey::EcdsaP384(verifying_key)) + } + #[cfg(feature = "p521")] + const_oid::db::rfc5912::SECP_521_R_1 => { + let verifying_key = ecdsa::VerifyingKey::::from_sec1_bytes( + raw_bytes, + ) .map_err(|_| { CertKitError::DecodingError( "Invalid P-521 public key bytes".to_string(), ) })?; - Ok(PublicKey::EcdsaP521(verifying_key)) - } else { - Err(CertKitError::DecodingError(format!( + Ok(PublicKey::EcdsaP521(verifying_key)) + } + _ => Err(CertKitError::DecodingError(format!( "Unsupported EC curve OID: {params_oid}" - ))) + ))), } } - ID_ED_25519 => { + #[cfg(feature = "ed25519")] + const_oid::db::rfc8410::ID_ED_25519 => { let bytes = spki.subject_public_key.as_bytes().ok_or_else(|| { CertKitError::DecodingError("Invalid Ed25519 public key bitstring".to_string()) })?; @@ -978,6 +1056,7 @@ mod test { use super::*; #[test] + #[cfg(feature = "rsa")] fn pem_encode_decode_rsa() { let rsa = KeyPair::generate_rsa(2048).unwrap(); let rsa_der = rsa::pkcs8::EncodePrivateKey::to_pkcs8_der(match &rsa { @@ -991,6 +1070,7 @@ mod test { } #[test] + #[cfg(feature = "p256")] fn pem_encode_decode_ecdsa_p256() { let p256 = KeyPair::generate_ecdsa_p256(); let p256_der = p256::pkcs8::EncodePrivateKey::to_pkcs8_der(match &p256 { @@ -1004,6 +1084,7 @@ mod test { } #[test] + #[cfg(feature = "p384")] fn pem_encode_decode_ecdsa_p384() { let p384 = KeyPair::generate_ecdsa_p384(); let p384_der = p384::pkcs8::EncodePrivateKey::to_pkcs8_der(match &p384 { @@ -1017,6 +1098,7 @@ mod test { } #[test] + #[cfg(feature = "p521")] fn pem_encode_decode_ecdsa_p521() { let p521 = KeyPair::generate_ecdsa_p521(); let p521_der = p521::pkcs8::EncodePrivateKey::to_pkcs8_der(match &p521 { @@ -1030,6 +1112,8 @@ mod test { } #[test] + #[cfg(feature = "ed25519")] + #[allow(unreachable_patterns)] //Depending on feature combination we may only support ED25519 fn pem_encode_decode_ed25519() { let ed = KeyPair::generate_ed25519(); let ed_der = ed25519_dalek::pkcs8::EncodePrivateKey::to_pkcs8_der(match &ed { diff --git a/src/lib.rs b/src/lib.rs index 47fe0fc..5199f0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,6 +179,15 @@ //! - [`error`]: Comprehensive error types and handling //! - [`tbs_certificate`]: Low-level certificate structure manipulation +#[cfg(not(any( + feature = "ed25519", + feature = "rsa", + feature = "p256", + feature = "p384", + feature = "p521" +)))] +compile_error!("Please enable at least 1 cryptographic algorithm to use certkit"); + pub mod cert; pub mod error; pub mod issuer; diff --git a/src/tbs_certificate.rs b/src/tbs_certificate.rs index 9bb9088..e8bbacd 100644 --- a/src/tbs_certificate.rs +++ b/src/tbs_certificate.rs @@ -109,18 +109,23 @@ impl TbsCertificate { // Convert the subject public key to SPKI format let subject_public_key_info = match &self.subject_public_key { + #[cfg(feature = "rsa")] PublicKey::Rsa(public) => { x509_cert::spki::SubjectPublicKeyInfoOwned::from_key(public.clone()).unwrap() } + #[cfg(feature = "p256")] PublicKey::EcdsaP256(verifying_key) => { x509_cert::spki::SubjectPublicKeyInfoOwned::from_key(*verifying_key).unwrap() } + #[cfg(feature = "p384")] PublicKey::EcdsaP384(verifying_key) => { x509_cert::spki::SubjectPublicKeyInfoOwned::from_key(*verifying_key).unwrap() } + #[cfg(feature = "p521")] PublicKey::EcdsaP521(verifying_key) => { x509_cert::spki::SubjectPublicKeyInfoOwned::from_key(*verifying_key).unwrap() } + #[cfg(feature = "ed25519")] PublicKey::Ed25519(verifying_key) => { let pk_bytes = verifying_key.to_bytes(); x509_cert::spki::SubjectPublicKeyInfoOwned { diff --git a/tests/openssl.rs b/tests/openssl.rs index caae79f..a02faf6 100644 --- a/tests/openssl.rs +++ b/tests/openssl.rs @@ -1,183 +1,186 @@ -mod util; - -use certkit::cert::extensions::ExtendedKeyUsageOption; -use certkit::cert::params::{CertificationRequestInfo, DistinguishedName, Validity}; -use certkit::issuer::Issuer; -use certkit::key::{KeyPair, PublicKey}; -use regex::Regex; -use std::fs; -use std::process::Command; -use time::OffsetDateTime; - -#[test] -fn test_openssl_validate_cert() { - // Generate a CA certificate - let ca_cert_with_key = util::generate_ca_cert(); - - // Generate a server certificate signed by the CA - let server_key = KeyPair::generate_ecdsa_p256(); - let server_dn = DistinguishedName::builder() - .common_name("server.myca.local".to_string()) - .build(); - - let server_public_key = PublicKey::from_key_pair(&server_key); - let server_cert_info = CertificationRequestInfo::builder() - .subject(server_dn) - .subject_public_key(server_public_key) - .usages(vec![ExtendedKeyUsageOption::ServerAuth]) - .build(); - - let validity = Validity { - not_before: OffsetDateTime::now_utc(), - not_after: OffsetDateTime::now_utc() + time::Duration::days(365), - }; - let server_cert = ca_cert_with_key.issue(&server_cert_info, validity); - let server_cert_pem = server_cert.to_pem().unwrap(); - - // Save the certificate to a temporary file - let cert_path = "/tmp/test_server_cert.pem"; - fs::write(cert_path, server_cert_pem).expect("Failed to write server certificate"); - - // Use OpenSSL CLI to validate the generated certificate - let output = Command::new("openssl") - .arg("x509") - .arg("-in") - .arg(cert_path) - .arg("-noout") - .arg("-text") - //If this is not added then the output of openssl is in the users system language which breaks this test. - .env("LANG", "C") - .output() - .expect("Failed to execute OpenSSL command"); - - // Check if OpenSSL command was successful - assert!( - output.status.success(), - "OpenSSL command failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - - // Updated test to validate static fields and use partial matching for dynamic fields - let output_text = String::from_utf8_lossy(&output.stdout); - println!("output_text {output_text}"); - - // Validate static fields - // Note: Different openssl versions format this field differently! - assert!( - output_text.contains("Issuer: C=, ST=, L=, O=, OU=, CN=myca.local") - || output_text.contains("Issuer: C = , ST = , L = , O = , OU = , CN = myca.local"), - "Issuer field is incorrect" - ); - - // Note: Different openssl versions format this field differently! - assert!( - output_text.contains("Subject: C=, ST=, L=, O=, OU=, CN=server.myca.local") - || output_text - .contains("Subject: C = , ST = , L = , O = , OU = , CN = server.myca.local"), - "Subject field is incorrect" - ); - assert!( - output_text.contains("Version: 3 (0x2)"), - "Version field is incorrect" - ); - assert!( - output_text.contains("Serial Number: 1 (0x1)"), - "Serial Number field is incorrect" - ); - - // Validate dynamic fields with regex - let not_before_regex = Regex::new(r"Not Before: .+").unwrap(); - let not_after_regex = Regex::new(r"Not After : .+").unwrap(); - - assert!( - not_before_regex.is_match(&output_text), - "Missing or incorrect Not Before field" - ); - assert!( - not_after_regex.is_match(&output_text), - "Missing or incorrect Not After field" - ); - assert!( - output_text.contains("Signature Algorithm: ecdsa-with-SHA256"), - "Signature Algorithm field is incorrect" - ); - - // Clean up temporary files - fs::remove_file(cert_path).expect("Failed to remove test certificate"); -} - -#[test] -fn test_openssl_crate_validate_cert() { - // Generate a CA certificate - let ca_cert_with_key = util::generate_ca_cert(); - - // Generate a server certificate signed by the CA - let server_key = KeyPair::generate_ecdsa_p256(); - let server_dn = DistinguishedName::builder() - .common_name("server.myca.local".to_string()) - .build(); - - let server_public_key = PublicKey::from_key_pair(&server_key); - let server_cert_info = CertificationRequestInfo::builder() - .subject(server_dn) - .subject_public_key(server_public_key) - .usages(vec![ExtendedKeyUsageOption::ServerAuth]) - .build(); - - let validity = Validity { - not_before: OffsetDateTime::now_utc(), - not_after: OffsetDateTime::now_utc() + time::Duration::days(365), - }; - let server_cert = ca_cert_with_key.issue(&server_cert_info, validity); - let server_cert_pem = server_cert.to_pem().unwrap(); - - // Use the openssl crate to parse and validate the certificate - use openssl::x509::X509; - let x509 = X509::from_pem(server_cert_pem.as_bytes()).expect("Failed to parse PEM"); - - // Check subject - let subject = x509 - .subject_name() - .entries_by_nid(openssl::nid::Nid::COMMONNAME) - .next() - .unwrap() - .data() - .as_utf8() - .unwrap(); - assert_eq!( - subject.to_string(), - "server.myca.local", - "Subject CN mismatch" - ); - - // Check issuer - let issuer = x509 - .issuer_name() - .entries_by_nid(openssl::nid::Nid::COMMONNAME) - .next() - .unwrap() - .data() - .as_utf8() - .unwrap(); - assert_eq!(issuer.to_string(), "myca.local", "Issuer CN mismatch"); - - // Check version - assert_eq!( - x509.version(), - 2, - "X509 version should be 3 (0-based index)" - ); - - // Check serial number - let serial = x509.serial_number().to_bn().unwrap().to_dec_str().unwrap(); - assert_eq!(serial.to_string(), "1", "Serial number should be 1"); - - // Check signature algorithm - let sig_alg = x509.signature_algorithm().object().nid(); - - assert_eq!( - sig_alg, - openssl::nid::Nid::ECDSA_WITH_SHA256, - "Signature algorithm should be ecdsa-with-SHA256" - ); +#[cfg(feature = "p256")] //For now these tests only test with p256 +mod test { + use crate::util; + use certkit::cert::extensions::ExtendedKeyUsageOption; + use certkit::cert::params::{CertificationRequestInfo, DistinguishedName, Validity}; + use certkit::issuer::Issuer; + use certkit::key::{KeyPair, PublicKey}; + use regex::Regex; + use std::fs; + use std::process::Command; + use time::OffsetDateTime; + + #[test] + fn test_openssl_validate_cert() { + // Generate a CA certificate + let ca_cert_with_key = util::generate_ca_cert(); + + // Generate a server certificate signed by the CA + let server_key = KeyPair::generate_ecdsa_p256(); + let server_dn = DistinguishedName::builder() + .common_name("server.myca.local".to_string()) + .build(); + + let server_public_key = PublicKey::from_key_pair(&server_key); + let server_cert_info = CertificationRequestInfo::builder() + .subject(server_dn) + .subject_public_key(server_public_key) + .usages(vec![ExtendedKeyUsageOption::ServerAuth]) + .build(); + + let validity = Validity { + not_before: OffsetDateTime::now_utc(), + not_after: OffsetDateTime::now_utc() + time::Duration::days(365), + }; + let server_cert = ca_cert_with_key.issue(&server_cert_info, validity); + let server_cert_pem = server_cert.to_pem().unwrap(); + + // Save the certificate to a temporary file + let cert_path = "/tmp/test_server_cert.pem"; + fs::write(cert_path, server_cert_pem).expect("Failed to write server certificate"); + + // Use OpenSSL CLI to validate the generated certificate + let output = Command::new("openssl") + .arg("x509") + .arg("-in") + .arg(cert_path) + .arg("-noout") + .arg("-text") + //If this is not added then the output of openssl is in the users system language which breaks this test. + .env("LANG", "C") + .output() + .expect("Failed to execute OpenSSL command"); + + // Check if OpenSSL command was successful + assert!( + output.status.success(), + "OpenSSL command failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + // Updated test to validate static fields and use partial matching for dynamic fields + let output_text = String::from_utf8_lossy(&output.stdout); + println!("output_text {output_text}"); + + // Validate static fields + // Note: Different openssl versions format this field differently! + assert!( + output_text.contains("Issuer: C=, ST=, L=, O=, OU=, CN=myca.local") + || output_text.contains("Issuer: C = , ST = , L = , O = , OU = , CN = myca.local"), + "Issuer field is incorrect" + ); + + // Note: Different openssl versions format this field differently! + assert!( + output_text.contains("Subject: C=, ST=, L=, O=, OU=, CN=server.myca.local") + || output_text + .contains("Subject: C = , ST = , L = , O = , OU = , CN = server.myca.local"), + "Subject field is incorrect" + ); + assert!( + output_text.contains("Version: 3 (0x2)"), + "Version field is incorrect" + ); + assert!( + output_text.contains("Serial Number: 1 (0x1)"), + "Serial Number field is incorrect" + ); + + // Validate dynamic fields with regex + let not_before_regex = Regex::new(r"Not Before: .+").unwrap(); + let not_after_regex = Regex::new(r"Not After : .+").unwrap(); + + assert!( + not_before_regex.is_match(&output_text), + "Missing or incorrect Not Before field" + ); + assert!( + not_after_regex.is_match(&output_text), + "Missing or incorrect Not After field" + ); + assert!( + output_text.contains("Signature Algorithm: ecdsa-with-SHA256"), + "Signature Algorithm field is incorrect" + ); + + // Clean up temporary files + fs::remove_file(cert_path).expect("Failed to remove test certificate"); + } + + #[test] + fn test_openssl_crate_validate_cert() { + // Generate a CA certificate + let ca_cert_with_key = util::generate_ca_cert(); + + // Generate a server certificate signed by the CA + let server_key = KeyPair::generate_ecdsa_p256(); + let server_dn = DistinguishedName::builder() + .common_name("server.myca.local".to_string()) + .build(); + + let server_public_key = PublicKey::from_key_pair(&server_key); + let server_cert_info = CertificationRequestInfo::builder() + .subject(server_dn) + .subject_public_key(server_public_key) + .usages(vec![ExtendedKeyUsageOption::ServerAuth]) + .build(); + + let validity = Validity { + not_before: OffsetDateTime::now_utc(), + not_after: OffsetDateTime::now_utc() + time::Duration::days(365), + }; + let server_cert = ca_cert_with_key.issue(&server_cert_info, validity); + let server_cert_pem = server_cert.to_pem().unwrap(); + + // Use the openssl crate to parse and validate the certificate + use openssl::x509::X509; + let x509 = X509::from_pem(server_cert_pem.as_bytes()).expect("Failed to parse PEM"); + + // Check subject + let subject = x509 + .subject_name() + .entries_by_nid(openssl::nid::Nid::COMMONNAME) + .next() + .unwrap() + .data() + .as_utf8() + .unwrap(); + assert_eq!( + subject.to_string(), + "server.myca.local", + "Subject CN mismatch" + ); + + // Check issuer + let issuer = x509 + .issuer_name() + .entries_by_nid(openssl::nid::Nid::COMMONNAME) + .next() + .unwrap() + .data() + .as_utf8() + .unwrap(); + assert_eq!(issuer.to_string(), "myca.local", "Issuer CN mismatch"); + + // Check version + assert_eq!( + x509.version(), + 2, + "X509 version should be 3 (0-based index)" + ); + + // Check serial number + let serial = x509.serial_number().to_bn().unwrap().to_dec_str().unwrap(); + assert_eq!(serial.to_string(), "1", "Serial number should be 1"); + + // Check signature algorithm + let sig_alg = x509.signature_algorithm().object().nid(); + + assert_eq!( + sig_alg, + openssl::nid::Nid::ECDSA_WITH_SHA256, + "Signature algorithm should be ecdsa-with-SHA256" + ); + } } +mod util; diff --git a/tests/test.rs b/tests/test.rs index 314b88a..99cb2e1 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,106 +1,109 @@ mod util; - -use certkit::cert::params; -use certkit::error::CertKitError; -use certkit::{ - cert::params::{CertificationRequestInfo, DistinguishedName}, - issuer::Issuer, - key::KeyPair, -}; -pub type Result = std::result::Result; -use time::OffsetDateTime; - -/// Generates a Certificate Authority (CA) certificate and saves it as a PEM file. -/// This test ensures the CA certificate generation process works as expected. -#[test] -fn generate_ca_cert() -> Result<()> { - let ca_cert_with_key = util::generate_ca_cert(); - - use std::io::Write; - std::fs::create_dir_all(".debug_certs").unwrap(); - std::fs::File::create(".debug_certs/ca_cert.pem") - .unwrap() - .write_all(ca_cert_with_key.cert.to_pem().unwrap().as_bytes()) - .unwrap(); - - eprintln!("CA Certificate: {:?}", ca_cert_with_key.cert); - Ok(()) -} - -/// Generates a server certificate signed by the CA and saves it as a PEM file. -/// This test ensures the server certificate generation and signing process works as expected. -#[test] -fn generate_server_cert() -> Result<()> { - let ca_cert_with_key = util::generate_ca_cert(); - - let server_key = KeyPair::generate_ecdsa_p256(); - let server_dn = DistinguishedName::builder() - .common_name("server.myca.local".to_string()) - .build(); - - let server_public_key = certkit::key::PublicKey::from_key_pair(&server_key); - let server_cert_info = CertificationRequestInfo::builder() - .subject(server_dn) - .subject_public_key(server_public_key) - .usages(vec![ - certkit::cert::extensions::ExtendedKeyUsageOption::ServerAuth, - ]) - .build(); - - let validity = params::Validity { - not_before: OffsetDateTime::now_utc(), - not_after: OffsetDateTime::now_utc() + time::Duration::days(365), - }; - let server_cert = ca_cert_with_key.issue(&server_cert_info, validity); - - eprintln!("Server Certificate: {server_cert:?}"); - let server_cert_pem = server_cert.to_pem().unwrap(); - - use std::io::Write; - std::fs::create_dir_all(".debug_certs").unwrap(); - std::fs::File::create(".debug_certs/server_cert.pem") - .unwrap() - .write_all(server_cert_pem.as_bytes()) - .unwrap(); - - Ok(()) -} - -/// Generates a client certificate signed by the CA and saves it as a PEM file. -/// This test ensures the client certificate generation and signing process works as expected. -#[test] -fn generate_client_cert() -> Result<()> { - let ca_cert_with_key = util::generate_ca_cert(); - - let client_key = KeyPair::generate_ecdsa_p256(); - let client_dn = DistinguishedName::builder() - .common_name("client.myca.local".to_string()) - .build(); - - let client_public_key = certkit::key::PublicKey::from_key_pair(&client_key); - let client_cert_info = CertificationRequestInfo::builder() - .subject(client_dn) - .subject_public_key(client_public_key) - .usages(vec![ - certkit::cert::extensions::ExtendedKeyUsageOption::ClientAuth, - ]) - .build(); - - let validity = params::Validity { - not_before: OffsetDateTime::now_utc(), - not_after: OffsetDateTime::now_utc() + time::Duration::days(365), +#[cfg(feature = "p256")] +mod test { + use certkit::cert::params; + use certkit::error::CertKitError; + use certkit::{ + cert::params::{CertificationRequestInfo, DistinguishedName}, + issuer::Issuer, + key::KeyPair, }; - let client_cert = ca_cert_with_key.issue(&client_cert_info, validity); - - eprintln!("Client Certificate: {client_cert:?}"); - let client_cert_pem = client_cert.to_pem().unwrap(); - - use std::io::Write; - std::fs::create_dir_all(".debug_certs").unwrap(); - std::fs::File::create(".debug_certs/client_cert.pem") - .unwrap() - .write_all(client_cert_pem.as_bytes()) - .unwrap(); - - Ok(()) + pub type Result = std::result::Result; + use crate::util; + use time::OffsetDateTime; + + /// Generates a Certificate Authority (CA) certificate and saves it as a PEM file. + /// This test ensures the CA certificate generation process works as expected. + #[test] + fn generate_ca_cert() -> Result<()> { + let ca_cert_with_key = util::generate_ca_cert(); + + use std::io::Write; + std::fs::create_dir_all(".debug_certs").unwrap(); + std::fs::File::create(".debug_certs/ca_cert.pem") + .unwrap() + .write_all(ca_cert_with_key.cert.to_pem().unwrap().as_bytes()) + .unwrap(); + + eprintln!("CA Certificate: {:?}", ca_cert_with_key.cert); + Ok(()) + } + + /// Generates a server certificate signed by the CA and saves it as a PEM file. + /// This test ensures the server certificate generation and signing process works as expected. + #[test] + fn generate_server_cert() -> Result<()> { + let ca_cert_with_key = util::generate_ca_cert(); + + let server_key = KeyPair::generate_ecdsa_p256(); + let server_dn = DistinguishedName::builder() + .common_name("server.myca.local".to_string()) + .build(); + + let server_public_key = certkit::key::PublicKey::from_key_pair(&server_key); + let server_cert_info = CertificationRequestInfo::builder() + .subject(server_dn) + .subject_public_key(server_public_key) + .usages(vec![ + certkit::cert::extensions::ExtendedKeyUsageOption::ServerAuth, + ]) + .build(); + + let validity = params::Validity { + not_before: OffsetDateTime::now_utc(), + not_after: OffsetDateTime::now_utc() + time::Duration::days(365), + }; + let server_cert = ca_cert_with_key.issue(&server_cert_info, validity); + + eprintln!("Server Certificate: {server_cert:?}"); + let server_cert_pem = server_cert.to_pem().unwrap(); + + use std::io::Write; + std::fs::create_dir_all(".debug_certs").unwrap(); + std::fs::File::create(".debug_certs/server_cert.pem") + .unwrap() + .write_all(server_cert_pem.as_bytes()) + .unwrap(); + + Ok(()) + } + + /// Generates a client certificate signed by the CA and saves it as a PEM file. + /// This test ensures the client certificate generation and signing process works as expected. + #[test] + fn generate_client_cert() -> Result<()> { + let ca_cert_with_key = util::generate_ca_cert(); + + let client_key = KeyPair::generate_ecdsa_p256(); + let client_dn = DistinguishedName::builder() + .common_name("client.myca.local".to_string()) + .build(); + + let client_public_key = certkit::key::PublicKey::from_key_pair(&client_key); + let client_cert_info = CertificationRequestInfo::builder() + .subject(client_dn) + .subject_public_key(client_public_key) + .usages(vec![ + certkit::cert::extensions::ExtendedKeyUsageOption::ClientAuth, + ]) + .build(); + + let validity = params::Validity { + not_before: OffsetDateTime::now_utc(), + not_after: OffsetDateTime::now_utc() + time::Duration::days(365), + }; + let client_cert = ca_cert_with_key.issue(&client_cert_info, validity); + + eprintln!("Client Certificate: {client_cert:?}"); + let client_cert_pem = client_cert.to_pem().unwrap(); + + use std::io::Write; + std::fs::create_dir_all(".debug_certs").unwrap(); + std::fs::File::create(".debug_certs/client_cert.pem") + .unwrap() + .write_all(client_cert_pem.as_bytes()) + .unwrap(); + + Ok(()) + } } diff --git a/tests/util.rs b/tests/util.rs index c0ec730..391edd3 100644 --- a/tests/util.rs +++ b/tests/util.rs @@ -1,29 +1,35 @@ -use certkit::cert::extensions::ExtendedKeyUsageOption; -use certkit::cert::params::{CertificationRequestInfo, DistinguishedName}; -use certkit::cert::{Certificate, CertificateWithPrivateKey}; -use certkit::key::{KeyPair, PublicKey}; +#[cfg(feature = "p256")] +mod impl_p256 { + use certkit::cert::extensions::ExtendedKeyUsageOption; + use certkit::cert::params::{CertificationRequestInfo, DistinguishedName}; + use certkit::cert::{Certificate, CertificateWithPrivateKey}; + use certkit::key::{KeyPair, PublicKey}; -pub fn generate_ca_cert() -> CertificateWithPrivateKey { - let ca_key = KeyPair::generate_ecdsa_p256(); + pub fn generate_ca_cert() -> CertificateWithPrivateKey { + let ca_key = KeyPair::generate_ecdsa_p256(); - let subject_dn = DistinguishedName::builder() - .common_name("myca.local".to_string()) - .build(); + let subject_dn = DistinguishedName::builder() + .common_name("myca.local".to_string()) + .build(); - let subject_public_key = PublicKey::from_key_pair(&ca_key); + let subject_public_key = PublicKey::from_key_pair(&ca_key); - let ca_cert_info = CertificationRequestInfo::builder() - .subject(subject_dn.clone()) - .subject_public_key(subject_public_key) - .usages(vec![ - ExtendedKeyUsageOption::ServerAuth, - ExtendedKeyUsageOption::ClientAuth, - ]) - .extensions(vec![]) - .build(); + let ca_cert_info = CertificationRequestInfo::builder() + .subject(subject_dn.clone()) + .subject_public_key(subject_public_key) + .usages(vec![ + ExtendedKeyUsageOption::ServerAuth, + ExtendedKeyUsageOption::ClientAuth, + ]) + .extensions(vec![]) + .build(); - CertificateWithPrivateKey { - cert: Certificate::new_self_signed(&ca_cert_info, &ca_key), - key: ca_key, + CertificateWithPrivateKey { + cert: Certificate::new_self_signed(&ca_cert_info, &ca_key), + key: ca_key, + } } } + +#[cfg(feature = "p256")] +pub use impl_p256::generate_ca_cert;