diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5eb6e9a..1739f14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: hack - args: build --feature-powerset --clean-per-run --no-dev-deps + args: check --feature-powerset --clean-per-run --no-dev-deps cargo-publish: needs: - cargo-lint diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af3d172..3c6bf08 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -79,11 +79,6 @@ repos: hooks: - id: shellcheck - - repo: https://github.com/doublify/pre-commit-rust - rev: v1.0 - hooks: - - id: cargo-check - - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: diff --git a/.vscode/settings.json b/.vscode/settings.json index f6143dc..b18a04d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "Cloudproof", "cosmian", "Cosmian", + "decrypter", "decryptor", "Decryptor", "decryptors", @@ -14,12 +15,16 @@ "encryptor", "Encryptor", "encryptors", + "FIPS", "hasher", "hashers", "keypair", "libsodium", "montgomery", "nist", + "oaep", + "Oaep", + "OAEP", "openssl", "pkcs", "sealbox", @@ -27,6 +32,7 @@ "xchacha", "zeroize", "zeroized", + "Zeroizes", "Zeroizing" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index b14ab2b..965632e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file. +## [9.3.0] - 2023-09-26 + +### Features + +- Add support for RSA key generation, key-wrapping and PKCS#8 import/export + +### Fixed + +- Fixed export of Curve25519Secret +- Aligned NIST Curves PKC8 import/export with the pkcs8 crate + ## [9.2.1] - 2023-09-26 ### Features diff --git a/Cargo.toml b/Cargo.toml index d140895..f4a8218 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "cosmian_crypto_core" -version = "9.2.1" +version = "9.3.0" authors = [ - "Bruno Grieder ", - "Théophile BRÉZOT ", + "Bruno GRIEDER ", + "Théophile BRÉZOT ", + "Emmanuel COSTE ", ] categories = ["cryptography", "security"] edition = "2021" @@ -24,19 +25,21 @@ certificate = ["curve25519", "x509-cert", "uuid", "pkcs8"] chacha = ["aead", "chacha20poly1305", "chacha20"] curve25519 = ["curve25519-dalek", "ed25519-dalek", "signature", "sha2"] default = [ - "aes", - "chacha", - "certificate", - "ecies", - "curve25519", - "certificate", - "nist_curves", - "ser", - "blake", - "sha3", - "rfc5649", + "aes", + "chacha", + "certificate", + "ecies", + "curve25519", + "certificate", + "nist_curves", + "rsa", + "ser", + "blake", + "sha3", + "rfc5649", ] ecies = ["aead", "crypto_box"] +rsa = ["digest", "dep:rsa", "sha1", "sha2", "dep:sha3", "pkcs8", "rfc5649"] nist_curves = ["p384", "p256", "p224", "p192", "elliptic-curve", "pkcs8"] rfc5649 = ["aes", "chacha"] ser = ["leb128"] @@ -50,22 +53,45 @@ chacha20 = { version = "0.9", optional = true } chacha20poly1305 = { version = "0.10", optional = true } crypto_box = { version = "0.9.1", features = ["seal"], optional = true } curve25519-dalek = { version = "4.1.0", optional = true } -ed25519-dalek = { version = "2.0.0", optional = true, features = ["default", "hazmat"] } -elliptic-curve = { version = "0.13.5", default-features = false, features = ["hazmat", "sec1", "pkcs8", "ecdh"], optional = true } +digest = { version = "0.10", optional = true } +ed25519-dalek = { version = "2.0.0", optional = true, features = [ + "default", + "hazmat", +] } +elliptic-curve = { version = "0.13.5", default-features = false, features = [ + "hazmat", + "sec1", + "pkcs8", + "ecdh", +], optional = true } getrandom = { version = "0.2", features = ["js"] } # needed to compile into WASM leb128 = { version = "0.2", optional = true } p192 = { version = "0.13", optional = true } p224 = { version = "0.13", optional = true } p256 = { version = "0.13", optional = true } p384 = { version = "0.13", optional = true } -pkcs8 = { version = "0.10", features = ["encryption", "std", "alloc", "pem"], optional = true } +pkcs8 = { version = "0.10", features = [ + "encryption", + "std", + "alloc", + "pem", +], optional = true } rand_chacha = "0.3" rand_core = { version = "0.6.4", features = ["getrandom"] } +rsa = { version = "0.9", optional = true } +sha1 = { version = "0.10", optional = true } sha2 = { version = "0.10", optional = true } +sha3 = { version = "0.10", optional = true } signature = { version = "2.1", optional = true } tiny-keccak = { version = "2.0.2", features = ["shake"], optional = true } uuid = { version = "1.4", features = ["v4"], optional = true } -x509-cert = { version = "0.2.4", features = ["pem", "std", "builder", "arbitrary", "hazmat"], optional = true } +x509-cert = { version = "0.2.4", features = [ + "pem", + "std", + "builder", + "arbitrary", + "hazmat", +], optional = true } zeroize = { version = "1.6", features = ["zeroize_derive"] } [dev-dependencies] diff --git a/README.md b/README.md index 31b23c7..027c36e 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ This crate implements the cryptographic primitives (modern encryption and signat - [Security](#security) - [ECIES encryption of a vector of bytes](#ecies-encryption-of-a-vector-of-bytes) - [ECIES encryption of a stream of bytes](#ecies-encryption-of-a-stream-of-bytes) +- [Key wrapping](#key-wrapping) + - [RFC 5649](#rfc-5649) + - [ECIES Key Wrapping](#ecies-key-wrapping) + - [RSA Key Wrapping](#rsa-key-wrapping) - [Signature](#signature) - [Static implementation](#static-implementation) - [Cached implementation](#cached-implementation) @@ -470,6 +474,156 @@ assert_eq!( ); ``` +## Key Wrapping + +Key wrapping and unwrapping is supported using: + +- a symmetric key wrapping scheme based on the [RFC 5649](https://tools.ietf.org/html/rfc5649). +- an Elliptic Curve keypair using one of the ECIES schemes above +- using an RSA keypair + +### RFC 5649 + +The RFC 5649 key wrapping scheme is implemented using the `key_wrap` and `Key_unwrap` methods exposed in the [key_wrapping_rfc_5649.rs](src/symmetric_crypto/key_wrapping_rfc_5649.rs). These methods are compatible with the PKCS#11 CKM_AES_KEY_WRAP_KWP mechanism (which is used in the hybrid RSA-AES key wrapping scheme, see below) + +**Example** + +```Rust +let kek = + b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; +let key_to_wrap = + b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; +let wrapped_key = [ + 199, 131, 191, 63, 110, 233, 156, 72, 218, 187, 196, 16, 226, 132, 197, 44, 191, 117, + 133, 120, 152, 157, 225, 138, 50, 148, 201, 164, 209, 151, 200, 162, 98, 112, 72, 139, + 28, 233, 128, 22, +]; + +assert_eq!( + key_wrap(key_to_wrap, kek).expect("Failed to wrap"), + wrapped_key +); +assert_eq!( + key_unwrap(&wrapped_key, kek).expect("Failed to unwrap"), + key_to_wrap +); + +``` + +### ECIES Key Wrapping + +See [Ecies](#ecies---elliptic-curve-integrated-encryption-scheme) above for details on the ECIES schemes. + +### RSA Key Wrapping + +Various PKCS#11 compatible key wrapping schemes are implemented for RSA keys of size, 2048, 3072 and 4096 bits. + +The algorithms are listed below with the corresponding CKM mechanism. Preferably, the **Aes256Sha256** algorithm should be used. + +- **Pkcs1v1_5**, + PKCS #1 v1.5 RS following PKCS#11 CKM_RSA_PKCS + The maximum possible plaintext length is m = k - 11, + where k is the size of the RSA modulus. + +- **OaepSha256**, + PKCS #1 RSA with OAEP block format following PKCS#11 CKM*RSA_PKCS_OAEP + The hash function used is SHA256 + The maximum possible plaintext length is m = k - 2 * h_len - 2, + where k is the size of the RSA modulus + and h_len is the size of the hash of the optional label. + +- **OaepSha1**, + PKCS #1 RSA with OAEP block format following PKCS#11 CKM*RSA_PKCS_OAEP + The hash function used is SHA1. For that reason this algorithm is not recommended + and is only kept here for compatibility with legacy systems. + The maximum possible plaintext length is m = k - 2 * h_len - 2, + where k is the size of the RSA modulus + and h_len is the size of the hash of the optional label. + This algorithm is compatible with Google Cloud KMS + + - RSA_OAEP_3072_SHA256 with RSA 3072 bits key + - RSA_OAEP_4096_SHA256 with RSA 4096 bits key + +- **OaepSha3**, + PKCS #1 RSA with OAEP block format following PKCS#11 CKM_RSA_PKCS_OAEP + The hash function used is SHA3. + and is only kept here for compatibility with legacy systems. + The maximum possible plaintext length is m = k - 2 \* h_len - 2, + where k is the size of the RSA modulus + and h_len is the size of the hash of the optional label. + +- **Aes256Sha256**, + Key wrap with AES following PKCS#11 CKM_RSA_AES_KEY_WRAP + using an AES key of 256 bits. The hash function used is SHA256. + The AES wrapping follows the RFC 5649 which is compatible with PKCS#11 CKM_AES_KEY_WRAP_KWP + and there is no limitation on the size of the plaintext (other than those of AES); the recommended + plaintext format for an EC Private key is PKCS#8. + This algorithm is compatible with Google Cloud KMS + + - RSA_OAEP_3072_SHA256_AES_256 for RSA 3072 bits key + - RSA_OAEP_4096_SHA256_AES_256 for RSA 4096 bits key + +- **Aes256Sha1**, + Key wrap with AES following PKCS#11 CKM_RSA_AES_KEY_WRAP + using an AES key of 256 bits. The hash function used is SHA1. + For that reason this algorithm is not recommended + and is only kept here for compatibility with legacy systems. + The AES wrapping follows the RFC 5649 which is compatible with PKCS#11 CKM_AES_KEY_WRAP_KWP + since there is no limitation on the size of the plaintext; the recommended + plaintext format for an EC Private key is PKCS#8. + This algorithm is compatible with Google Cloud KMS + + - RSA_OAEP_3072_SHA1_AES_256 for RSA 3072 bits key + - RSA_OAEP_4096_SHA1_AES_256 for RSA 4096 bits key + +- **Aes256Sha3**, + Key wrap with AES following PKCS#11 CKM_RSA_AES_KEY_WRAP + using an AES key of 256 bits. The hash function used is SHA3-256 (defined in FIPS 202). + The AES wrapping follows the RFC 5649 which is compatible with PKCS#11 CKM_AES_KEY_WRAP_KWP + since there is no limitation on the size of the plaintext; the recommended + plaintext format for an EC Private key is PKCS#8. + +**Example using CKM_RSA_AES_KEY_WRAP with SHA-256 and AES 256** + +```Rust +use cosmian_crypto_core::{ + reexport::rand_core::{RngCore, SeedableRng}, + CsRng, RsaKeyLength, RsaKeyWrappingAlgorithm, RsaPrivateKey, +}; +use zeroize::Zeroizing; + +let mut rng = CsRng::from_entropy(); +println!("... Generating a 3072 bit RSA key ..."); +let rsa_private_key = RsaPrivateKey::new(&mut rng, RsaKeyLength::Modulus3072).unwrap(); + +let mut key_to_wrap = [0_u8; 32]; +rng.fill_bytes(&mut key_to_wrap); + +let mut key_to_wrap = [0_u8; 189]; +rng.fill_bytes(&mut key_to_wrap); +let key_to_wrap = Zeroizing::from(key_to_wrap.to_vec()); + +let rsa_public_key = rsa_private_key.public_key(); + +print!("Key wrapping with PKCS#11 CKM_RSA_AES_KEY_WRAP SHA-256 AES 256 ..."); +let wrapped_key = rsa_public_key + .wrap_key( + &mut rng, + RsaKeyWrappingAlgorithm::Aes256Sha256, + &key_to_wrap, + ) + .unwrap(); + +print!("unwrapping ...: "); +let unwrapped_key = rsa_private_key + .unwrap_key(RsaKeyWrappingAlgorithm::Aes256Sha256, &wrapped_key) + .unwrap(); + +assert_eq!(unwrapped_key, key_to_wrap); +println!("OK"); + +``` + ## Signature The crate currently exposes the EdDSA (Ed25519) signature scheme. diff --git a/examples/examples/mod.rs b/examples/examples/mod.rs index 26ea129..17c0094 100644 --- a/examples/examples/mod.rs +++ b/examples/examples/mod.rs @@ -1,5 +1,7 @@ #[cfg(feature = "ecies")] mod ecies; +#[cfg(feature = "rsa")] +mod rsa_key_wrapping; #[cfg(feature = "curve25519")] mod signature; #[cfg(feature = "chacha")] @@ -7,6 +9,8 @@ mod symmetric_crypto; #[cfg(feature = "ecies")] pub use self::ecies::*; +#[cfg(feature = "rsa")] +pub use self::rsa_key_wrapping::*; #[cfg(feature = "curve25519")] pub use self::signature::*; #[cfg(feature = "chacha")] diff --git a/examples/examples/rsa_key_wrapping.rs b/examples/examples/rsa_key_wrapping.rs new file mode 100644 index 0000000..faa008b --- /dev/null +++ b/examples/examples/rsa_key_wrapping.rs @@ -0,0 +1,38 @@ +#[cfg(feature = "rsa")] +pub fn rsa_key_wrapping() { + use cosmian_crypto_core::{ + reexport::rand_core::{RngCore, SeedableRng}, + CsRng, RsaKeyLength, RsaKeyWrappingAlgorithm, RsaPrivateKey, + }; + use zeroize::Zeroizing; + + let mut rng = CsRng::from_entropy(); + println!("... Generating a 3072 bit RSA key ..."); + let rsa_private_key = RsaPrivateKey::new(&mut rng, RsaKeyLength::Modulus3072).unwrap(); + + let mut key_to_wrap = [0_u8; 32]; + rng.fill_bytes(&mut key_to_wrap); + + let mut key_to_wrap = [0_u8; 189]; + rng.fill_bytes(&mut key_to_wrap); + let key_to_wrap = Zeroizing::from(key_to_wrap.to_vec()); + + let rsa_public_key = rsa_private_key.public_key(); + + print!("Key wrapping with PKCS#11 CKM_RSA_AES_KEY_WRAP SHA-256 AES 256 ..."); + let wrapped_key = rsa_public_key + .wrap_key( + &mut rng, + RsaKeyWrappingAlgorithm::Aes256Sha256, + &key_to_wrap, + ) + .unwrap(); + + print!("unwrapping ...: "); + let unwrapped_key = rsa_private_key + .unwrap_key(RsaKeyWrappingAlgorithm::Aes256Sha256, &wrapped_key) + .unwrap(); + + assert_eq!(unwrapped_key, key_to_wrap); + println!("OK"); +} diff --git a/examples/main.rs b/examples/main.rs index 1f1477d..f27229a 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -47,4 +47,7 @@ fn main() { self::examples::ed25519_cached(); #[cfg(feature = "curve25519")] self::examples::ed25519_keypair(); + + #[cfg(feature = "rsa")] + self::examples::rsa_key_wrapping(); } diff --git a/src/asymmetric_crypto/curve_25519/curve_secret.rs b/src/asymmetric_crypto/curves/curve_25519/curve_secret.rs similarity index 98% rename from src/asymmetric_crypto/curve_25519/curve_secret.rs rename to src/asymmetric_crypto/curves/curve_25519/curve_secret.rs index 3e98955..8580aaa 100644 --- a/src/asymmetric_crypto/curve_25519/curve_secret.rs +++ b/src/asymmetric_crypto/curves/curve_25519/curve_secret.rs @@ -72,6 +72,7 @@ impl Curve25519Secret { /// Get the underlying bytes slice of the private key /// /// This is a facade to `RandomFixedSizeCBytes::as_bytes` + #[must_use] pub fn as_bytes(&self) -> &[u8] { >::as_bytes(self) } @@ -79,6 +80,7 @@ impl Curve25519Secret { /// Serialize the `PrivateKey` as a non zero scalar /// /// This is a facade to `::to_bytes` + #[must_use] pub fn to_bytes(&self) -> [u8; CURVE_25519_SECRET_LENGTH] { >::to_bytes(self) } diff --git a/src/asymmetric_crypto/curve_25519/ed25519/certificate/encoding.rs b/src/asymmetric_crypto/curves/curve_25519/ed25519/certificate/encoding.rs similarity index 100% rename from src/asymmetric_crypto/curve_25519/ed25519/certificate/encoding.rs rename to src/asymmetric_crypto/curves/curve_25519/ed25519/certificate/encoding.rs diff --git a/src/asymmetric_crypto/curve_25519/ed25519/certificate/mod.rs b/src/asymmetric_crypto/curves/curve_25519/ed25519/certificate/mod.rs similarity index 97% rename from src/asymmetric_crypto/curve_25519/ed25519/certificate/mod.rs rename to src/asymmetric_crypto/curves/curve_25519/ed25519/certificate/mod.rs index a7e7b58..7427e61 100644 --- a/src/asymmetric_crypto/curve_25519/ed25519/certificate/mod.rs +++ b/src/asymmetric_crypto/curves/curve_25519/ed25519/certificate/mod.rs @@ -153,8 +153,7 @@ mod tests { use x509_cert::builder::Profile; use crate::{ - asymmetric_crypto::curve_25519::{ed25519::build_certificate, x25519::X25519Keypair}, - CryptoCoreError, CsRng, Ed25519Keypair, X25519PublicKey, + build_certificate, CryptoCoreError, CsRng, Ed25519Keypair, X25519Keypair, X25519PublicKey, }; #[test] diff --git a/src/asymmetric_crypto/curve_25519/ed25519/ed_dsa/key_pair.rs b/src/asymmetric_crypto/curves/curve_25519/ed25519/ed_dsa/key_pair.rs similarity index 100% rename from src/asymmetric_crypto/curve_25519/ed25519/ed_dsa/key_pair.rs rename to src/asymmetric_crypto/curves/curve_25519/ed25519/ed_dsa/key_pair.rs diff --git a/src/asymmetric_crypto/curve_25519/ed25519/ed_dsa/mod.rs b/src/asymmetric_crypto/curves/curve_25519/ed25519/ed_dsa/mod.rs similarity index 94% rename from src/asymmetric_crypto/curve_25519/ed25519/ed_dsa/mod.rs rename to src/asymmetric_crypto/curves/curve_25519/ed25519/ed_dsa/mod.rs index b3b4a82..0d440f3 100644 --- a/src/asymmetric_crypto/curve_25519/ed25519/ed_dsa/mod.rs +++ b/src/asymmetric_crypto/curves/curve_25519/ed25519/ed_dsa/mod.rs @@ -15,10 +15,7 @@ mod tests { use super::Cached25519Signer; use crate::{ - asymmetric_crypto::{ - curve_25519::ed25519::{Ed25519PrivateKey, Ed25519PublicKey}, - Ed25519Keypair, - }, + asymmetric_crypto::{Ed25519Keypair, Ed25519PrivateKey, Ed25519PublicKey}, reexport::rand_core::SeedableRng, CsRng, FixedSizeCBytes, }; diff --git a/src/asymmetric_crypto/curve_25519/ed25519/ed_dsa/signer.rs b/src/asymmetric_crypto/curves/curve_25519/ed25519/ed_dsa/signer.rs similarity index 95% rename from src/asymmetric_crypto/curve_25519/ed25519/ed_dsa/signer.rs rename to src/asymmetric_crypto/curves/curve_25519/ed25519/ed_dsa/signer.rs index aff589c..928b803 100644 --- a/src/asymmetric_crypto/curve_25519/ed25519/ed_dsa/signer.rs +++ b/src/asymmetric_crypto/curves/curve_25519/ed25519/ed_dsa/signer.rs @@ -2,7 +2,7 @@ use ed25519_dalek::{ed25519, SigningKey}; pub use ed25519_dalek::{SecretKey as EdSecretKey, VerifyingKey as EdPublicKey}; use signature::Signer; -use crate::asymmetric_crypto::curve_25519::ed25519::Ed25519PrivateKey; +use crate::Ed25519PrivateKey; /// Signer implementation for Ed25519. /// diff --git a/src/asymmetric_crypto/curve_25519/ed25519/ed_dsa/verifier.rs b/src/asymmetric_crypto/curves/curve_25519/ed25519/ed_dsa/verifier.rs similarity index 87% rename from src/asymmetric_crypto/curve_25519/ed25519/ed_dsa/verifier.rs rename to src/asymmetric_crypto/curves/curve_25519/ed25519/ed_dsa/verifier.rs index 445c5ad..9f6bc19 100644 --- a/src/asymmetric_crypto/curve_25519/ed25519/ed_dsa/verifier.rs +++ b/src/asymmetric_crypto/curves/curve_25519/ed25519/ed_dsa/verifier.rs @@ -2,7 +2,7 @@ use ed25519_dalek::ed25519; pub use ed25519_dalek::{SecretKey as EdSecretKey, VerifyingKey as EdPublicKey}; use signature::Verifier; -use crate::asymmetric_crypto::curve_25519::ed25519::Ed25519PublicKey; +use crate::Ed25519PublicKey; /// Verifier implementation for Ed25519. impl Verifier for Ed25519PublicKey { diff --git a/src/asymmetric_crypto/curve_25519/ed25519/mod.rs b/src/asymmetric_crypto/curves/curve_25519/ed25519/mod.rs similarity index 100% rename from src/asymmetric_crypto/curve_25519/ed25519/mod.rs rename to src/asymmetric_crypto/curves/curve_25519/ed25519/mod.rs diff --git a/src/asymmetric_crypto/curve_25519/ed25519/private_key.rs b/src/asymmetric_crypto/curves/curve_25519/ed25519/private_key.rs similarity index 63% rename from src/asymmetric_crypto/curve_25519/ed25519/private_key.rs rename to src/asymmetric_crypto/curves/curve_25519/ed25519/private_key.rs index d023075..87558f8 100644 --- a/src/asymmetric_crypto/curve_25519/ed25519/private_key.rs +++ b/src/asymmetric_crypto/curves/curve_25519/ed25519/private_key.rs @@ -1,5 +1,5 @@ pub use ed25519_dalek::{SecretKey as EdSecretKey, VerifyingKey as EdPublicKey}; -use crate::asymmetric_crypto::curve_25519::curve_secret::Curve25519Secret; +use crate::Curve25519Secret; pub type Ed25519PrivateKey = Curve25519Secret; diff --git a/src/asymmetric_crypto/curve_25519/ed25519/public_key.rs b/src/asymmetric_crypto/curves/curve_25519/ed25519/public_key.rs similarity index 100% rename from src/asymmetric_crypto/curve_25519/ed25519/public_key.rs rename to src/asymmetric_crypto/curves/curve_25519/ed25519/public_key.rs diff --git a/src/asymmetric_crypto/curve_25519/mod.rs b/src/asymmetric_crypto/curves/curve_25519/mod.rs similarity index 89% rename from src/asymmetric_crypto/curve_25519/mod.rs rename to src/asymmetric_crypto/curves/curve_25519/mod.rs index 137acda..f0ca72a 100644 --- a/src/asymmetric_crypto/curve_25519/mod.rs +++ b/src/asymmetric_crypto/curves/curve_25519/mod.rs @@ -3,7 +3,7 @@ mod ed25519; mod ristretto_25519; mod x25519; -pub use curve_secret::CURVE_25519_SECRET_LENGTH; +pub use curve_secret::{Curve25519Secret, CURVE_25519_SECRET_LENGTH}; #[cfg(feature = "certificate")] pub use ed25519::{build_certificate, build_certificate_profile}; pub use ed25519::{ diff --git a/src/asymmetric_crypto/curve_25519/ristretto_25519/curve_point.rs b/src/asymmetric_crypto/curves/curve_25519/ristretto_25519/curve_point.rs similarity index 100% rename from src/asymmetric_crypto/curve_25519/ristretto_25519/curve_point.rs rename to src/asymmetric_crypto/curves/curve_25519/ristretto_25519/curve_point.rs diff --git a/src/asymmetric_crypto/curve_25519/ristretto_25519/mod.rs b/src/asymmetric_crypto/curves/curve_25519/ristretto_25519/mod.rs similarity index 100% rename from src/asymmetric_crypto/curve_25519/ristretto_25519/mod.rs rename to src/asymmetric_crypto/curves/curve_25519/ristretto_25519/mod.rs diff --git a/src/asymmetric_crypto/curve_25519/ristretto_25519/private_key.rs b/src/asymmetric_crypto/curves/curve_25519/ristretto_25519/private_key.rs similarity index 100% rename from src/asymmetric_crypto/curve_25519/ristretto_25519/private_key.rs rename to src/asymmetric_crypto/curves/curve_25519/ristretto_25519/private_key.rs diff --git a/src/asymmetric_crypto/curve_25519/x25519/curve_point.rs b/src/asymmetric_crypto/curves/curve_25519/x25519/curve_point.rs similarity index 100% rename from src/asymmetric_crypto/curve_25519/x25519/curve_point.rs rename to src/asymmetric_crypto/curves/curve_25519/x25519/curve_point.rs diff --git a/src/asymmetric_crypto/curve_25519/x25519/encoding.rs b/src/asymmetric_crypto/curves/curve_25519/x25519/encoding.rs similarity index 100% rename from src/asymmetric_crypto/curve_25519/x25519/encoding.rs rename to src/asymmetric_crypto/curves/curve_25519/x25519/encoding.rs diff --git a/src/asymmetric_crypto/curve_25519/x25519/key_pair.rs b/src/asymmetric_crypto/curves/curve_25519/x25519/key_pair.rs similarity index 100% rename from src/asymmetric_crypto/curve_25519/x25519/key_pair.rs rename to src/asymmetric_crypto/curves/curve_25519/x25519/key_pair.rs diff --git a/src/asymmetric_crypto/curve_25519/x25519/mod.rs b/src/asymmetric_crypto/curves/curve_25519/x25519/mod.rs similarity index 100% rename from src/asymmetric_crypto/curve_25519/x25519/mod.rs rename to src/asymmetric_crypto/curves/curve_25519/x25519/mod.rs diff --git a/src/asymmetric_crypto/curve_25519/x25519/private_key.rs b/src/asymmetric_crypto/curves/curve_25519/x25519/private_key.rs similarity index 90% rename from src/asymmetric_crypto/curve_25519/x25519/private_key.rs rename to src/asymmetric_crypto/curves/curve_25519/x25519/private_key.rs index 2aac8d6..61775b2 100644 --- a/src/asymmetric_crypto/curve_25519/x25519/private_key.rs +++ b/src/asymmetric_crypto/curves/curve_25519/x25519/private_key.rs @@ -1,9 +1,6 @@ use sha2::{Digest, Sha512}; -use crate::{ - asymmetric_crypto::curve_25519::curve_secret::Curve25519Secret, Ed25519PrivateKey, - CURVE_25519_SECRET_LENGTH, -}; +use crate::{Curve25519Secret, Ed25519PrivateKey, CURVE_25519_SECRET_LENGTH}; pub type X25519PrivateKey = Curve25519Secret; @@ -16,6 +13,7 @@ impl X25519PrivateKey { /// private key. /// /// See [`X25519PublicKey::from_ed25519_public_key`] for more details. + #[must_use] pub fn from_ed25519_private_key(sk: &Ed25519PrivateKey) -> Self { // see ed25519_dalek::ExpandedSecretKey::to_curve25519_private_key // The spec-compliant way to define an expanded secret key. This computes diff --git a/src/asymmetric_crypto/curves/mod.rs b/src/asymmetric_crypto/curves/mod.rs new file mode 100644 index 0000000..2e9f751 --- /dev/null +++ b/src/asymmetric_crypto/curves/mod.rs @@ -0,0 +1,9 @@ +#[cfg(feature = "curve25519")] +mod curve_25519; +#[cfg(feature = "curve25519")] +pub use curve_25519::*; + +#[cfg(feature = "nist_curves")] +mod nist; +#[cfg(feature = "nist_curves")] +pub use nist::*; diff --git a/src/asymmetric_crypto/nist/curve_point.rs b/src/asymmetric_crypto/curves/nist/curve_point.rs similarity index 100% rename from src/asymmetric_crypto/nist/curve_point.rs rename to src/asymmetric_crypto/curves/nist/curve_point.rs diff --git a/src/asymmetric_crypto/nist/mod.rs b/src/asymmetric_crypto/curves/nist/mod.rs similarity index 100% rename from src/asymmetric_crypto/nist/mod.rs rename to src/asymmetric_crypto/curves/nist/mod.rs diff --git a/src/asymmetric_crypto/nist/private_key.rs b/src/asymmetric_crypto/curves/nist/private_key.rs similarity index 76% rename from src/asymmetric_crypto/nist/private_key.rs rename to src/asymmetric_crypto/curves/nist/private_key.rs index 00a01e6..5d9e3a5 100644 --- a/src/asymmetric_crypto/nist/private_key.rs +++ b/src/asymmetric_crypto/curves/nist/private_key.rs @@ -16,8 +16,8 @@ use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; #[cfg(feature = "ser")] use crate::bytes_ser_de::{Deserializer, Serializable, Serializer}; use crate::{ - reexport::rand_core::CryptoRngCore, CBytes, CryptoCoreError, CsRng, FixedSizeCBytes, - RandomFixedSizeCBytes, SecretCBytes, + pkcs8_fix, reexport::rand_core::CryptoRngCore, CBytes, CryptoCoreError, CsRng, FixedSizeCBytes, + NistPublicKey, RandomFixedSizeCBytes, SecretCBytes, }; /// Nist Curve private key @@ -33,88 +33,6 @@ pub struct NistPrivateKey { pub(super) secret_key: SecretKey, } -impl NistPrivateKey -where - C: Curve + CurveArithmetic + pkcs8::AssociatedOid, - ::FieldBytesSize: sec1::ModulusSize, - ::AffinePoint: sec1::ToEncodedPoint, - ::AffinePoint: sec1::FromEncodedPoint, -{ - pub const LENGTH: usize = LENGTH; - - /// Encode the private key as a `PKCS#8 PrivateKeyInfo` ASN.1 DER - pub fn try_to_pkcs8(&self) -> Result>, CryptoCoreError> { - let bytes = - pkcs8::EncodePrivateKey::to_pkcs8_der(&self.secret_key).map(|d| d.to_bytes())?; - Ok(bytes) - } - - /// Encode the private key as a `PKCS#8 EncryptedPrivateKeyInfo` ASN.1 DER - /// The encryption algorithm used is Scrypt AES-256 CBC - pub fn try_to_encrypted_pkcs8( - &self, - password: impl AsRef<[u8]>, - ) -> Result>, CryptoCoreError> { - let mut rng = CsRng::from_entropy(); - - // Due to in compatibility issues with the openssl library, we use the - // modified parameters for Scrypt and cannot use the default implemented with - - // ```Rust - // let bytes = - // pkcs8::EncodePrivateKey::to_pkcs8_encrypted_der(&self.secret_key, &mut rng, password) - // .map(|d| d.to_bytes())?; - // ``` - - // see this issue for more details and the PR progress that will fix it: - // https://github.com/RustCrypto/formats/issues/1205 - - let doc: SecretDocument = { - let bytes = self.secret_key.to_pkcs8_der()?; - let mut salt = [0u8; 16]; - rng.fill_bytes(&mut salt); - - let mut iv = [0u8; 16]; - rng.fill_bytes(&mut iv); - - // 14 = log_2(16384), 32 bytes = 256 bits - let scrypt_params = scrypt::Params::new(14, 8, 1, 32).unwrap(); - let pbes2_params = - pbes2::Parameters::scrypt_aes256cbc(scrypt_params, &salt, &iv).unwrap(); - - let encrypted_data = pbes2_params.encrypt(password, bytes.as_bytes())?; - - EncryptedPrivateKeyInfo { - encryption_algorithm: pbes2_params.into(), - encrypted_data: &encrypted_data, - } - .try_into()? - }; - - Ok(doc.to_bytes()) - } - - /// Decode the private key from a `PKCS#8 PrivateKeyInfo` ASN.1 DER - pub fn try_from_pkcs8(bytes: &[u8]) -> Result { - let secret_key: SecretKey = pkcs8::DecodePrivateKey::from_pkcs8_der(bytes)?; - let mut bytes = [0_u8; LENGTH]; - bytes.copy_from_slice(&secret_key.to_bytes()); - Ok(Self { bytes, secret_key }) - } - - /// Decode the private key as a `PKCS#8 EncryptedPrivateKeyInfo` ASN.1 DER - pub fn try_from_encrypted_pkcs8( - bytes: &[u8], - password: impl AsRef<[u8]>, - ) -> Result { - let secret_key: SecretKey = - pkcs8::DecodePrivateKey::from_pkcs8_encrypted_der(bytes, password)?; - let mut bytes = [0_u8; LENGTH]; - bytes.copy_from_slice(&secret_key.to_bytes()); - Ok(Self { bytes, secret_key }) - } -} - impl Drop for NistPrivateKey { fn drop(&mut self) { self.bytes.zeroize(); @@ -219,6 +137,16 @@ impl NistPrivateKey } } +impl crate::PrivateKey + for NistPrivateKey +{ + type PublicKey = NistPublicKey; + + fn public_key(&self) -> Self::PublicKey { + Self::PublicKey::from(self) + } +} + /// Key Serialization framework #[cfg(all(feature = "ser", feature = "aes"))] impl Serializable for NistPrivateKey { @@ -237,3 +165,134 @@ impl Serializable for NistPriva Self::try_from_bytes(bytes) } } + +impl pkcs8::EncodePrivateKey for NistPrivateKey +where + C: Curve + CurveArithmetic + pkcs8::AssociatedOid, + ::FieldBytesSize: sec1::ModulusSize, + ::AffinePoint: sec1::ToEncodedPoint, + ::AffinePoint: sec1::FromEncodedPoint, +{ + fn to_pkcs8_der(&self) -> pkcs8::Result { + self.secret_key.to_pkcs8_der() + } + + fn to_pkcs8_encrypted_der( + &self, + rng: impl rand_core::CryptoRng + RngCore, + password: impl AsRef<[u8]>, + ) -> pkcs8::Result { + pkcs8_fix::to_pkcs8_encrypted_der(&self.to_pkcs8_der()?, rng, password) + } +} + +impl pkcs8::DecodePrivateKey for NistPrivateKey +where + C: Curve + CurveArithmetic + pkcs8::AssociatedOid, + ::FieldBytesSize: sec1::ModulusSize, + ::AffinePoint: sec1::ToEncodedPoint, + ::AffinePoint: sec1::FromEncodedPoint, +{ + fn from_pkcs8_der(bytes: &[u8]) -> pkcs8::Result { + let key = SecretKey::::from_pkcs8_der(bytes)?; + let mut bytes = [0_u8; LENGTH]; + bytes.copy_from_slice(&key.to_bytes()); + Ok(Self { + bytes, + secret_key: key, + }) + } +} + +/// PKCS#8 support (deprecated) +#[deprecated = "use the methods on the `pkcs8::EncodePrivateKey` and `pkcs8::DecodePrivateKey` \ + traits instead"] +impl NistPrivateKey +where + C: Curve + CurveArithmetic + pkcs8::AssociatedOid, + ::FieldBytesSize: sec1::ModulusSize, + ::AffinePoint: sec1::ToEncodedPoint, + ::AffinePoint: sec1::FromEncodedPoint, +{ + pub const LENGTH: usize = LENGTH; + + /// Encode the private key as a `PKCS#8 PrivateKeyInfo` ASN.1 DER + #[deprecated = "use the methods on the `pkcs8::EncodePrivateKey` and `pkcs8::DecodePrivateKey` \ + traits instead"] + pub fn try_to_pkcs8(&self) -> Result>, CryptoCoreError> { + let bytes = + pkcs8::EncodePrivateKey::to_pkcs8_der(&self.secret_key).map(|d| d.to_bytes())?; + Ok(bytes) + } + + /// Encode the private key as a `PKCS#8 EncryptedPrivateKeyInfo` ASN.1 DER + /// The encryption algorithm used is Scrypt AES-256 CBC + #[deprecated = "use the methods on the `pkcs8::EncodePrivateKey` and `pkcs8::DecodePrivateKey` \ + traits instead"] + pub fn try_to_encrypted_pkcs8( + &self, + password: impl AsRef<[u8]>, + ) -> Result>, CryptoCoreError> { + let mut rng = CsRng::from_entropy(); + + // Due to in compatibility issues with the openssl library, we use the + // modified parameters for Scrypt and cannot use the default implemented with + + // ```Rust + // let bytes = + // pkcs8::EncodePrivateKey::to_pkcs8_encrypted_der(&self.secret_key, &mut rng, password) + // .map(|d| d.to_bytes())?; + // ``` + + // see this issue for more details and the PR progress that will fix it: + // https://github.com/RustCrypto/formats/issues/1205 + + let doc: SecretDocument = { + let bytes = self.secret_key.to_pkcs8_der()?; + let mut salt = [0u8; 16]; + rng.fill_bytes(&mut salt); + + let mut iv = [0u8; 16]; + rng.fill_bytes(&mut iv); + + // 14 = log_2(16384), 32 bytes = 256 bits + let scrypt_params = scrypt::Params::new(14, 8, 1, 32).unwrap(); + let pbes2_params = + pbes2::Parameters::scrypt_aes256cbc(scrypt_params, &salt, &iv).unwrap(); + + let encrypted_data = pbes2_params.encrypt(password, bytes.as_bytes())?; + + EncryptedPrivateKeyInfo { + encryption_algorithm: pbes2_params.into(), + encrypted_data: &encrypted_data, + } + .try_into()? + }; + + Ok(doc.to_bytes()) + } + + /// Decode the private key from a `PKCS#8 PrivateKeyInfo` ASN.1 DER + #[deprecated = "use the methods on the `pkcs8::EncodePrivateKey` and `pkcs8::DecodePrivateKey` \ + traits instead"] + pub fn try_from_pkcs8(bytes: &[u8]) -> Result { + let secret_key: SecretKey = pkcs8::DecodePrivateKey::from_pkcs8_der(bytes)?; + let mut bytes = [0_u8; LENGTH]; + bytes.copy_from_slice(&secret_key.to_bytes()); + Ok(Self { bytes, secret_key }) + } + + /// Decode the private key as a `PKCS#8 EncryptedPrivateKeyInfo` ASN.1 DER + #[deprecated = "use the methods on the `pkcs8::EncodePrivateKey` and `pkcs8::DecodePrivateKey` \ + traits instead"] + pub fn try_from_encrypted_pkcs8( + bytes: &[u8], + password: impl AsRef<[u8]>, + ) -> Result { + let secret_key: SecretKey = + pkcs8::DecodePrivateKey::from_pkcs8_encrypted_der(bytes, password)?; + let mut bytes = [0_u8; LENGTH]; + bytes.copy_from_slice(&secret_key.to_bytes()); + Ok(Self { bytes, secret_key }) + } +} diff --git a/src/asymmetric_crypto/nist/public_key.rs b/src/asymmetric_crypto/curves/nist/public_key.rs similarity index 87% rename from src/asymmetric_crypto/nist/public_key.rs rename to src/asymmetric_crypto/curves/nist/public_key.rs index 7512099..a5774df 100644 --- a/src/asymmetric_crypto/nist/public_key.rs +++ b/src/asymmetric_crypto/curves/nist/public_key.rs @@ -62,6 +62,7 @@ where /// Encode the public key as a `X.509 SubjectPublicKeyInfo` (SPKI) ASN.1 DER /// byte array. + #[deprecated = "use the methods on the `Pkcs8PublicKey` trait instead"] pub fn try_to_pkcs8(&self) -> Result, CryptoCoreError> { let bytes = pkcs8::EncodePublicKey::to_public_key_der(&self.0).map(pkcs8::Document::into_vec)?; @@ -70,6 +71,7 @@ where /// Decode the public key from a `X.509 SubjectPublicKeyInfo` (SPKI) ASN.1 /// DER byte array. + #[deprecated = "use the methods on the `Pkcs8PublicKey` trait instead"] pub fn try_from_pkcs8(bytes: &[u8]) -> Result { let key = pkcs8::DecodePublicKey::from_public_key_der(bytes)?; Ok(Self(key)) @@ -87,11 +89,10 @@ where ::AffinePoint: sec1::ToEncodedPoint, ::AffinePoint: sec1::FromEncodedPoint, { - #[must_use] - /// Serialize the underlying SEC1 Encoded Point as an array of bytes /// /// This is a facade to `::to_bytes` + #[must_use] pub fn to_bytes(&self) -> [u8; LENGTH] { >::to_bytes(self) } @@ -105,6 +106,11 @@ where } } +impl crate::PublicKey + for NistPublicKey +{ +} + impl< C: Curve + CurveArithmetic, const PRIVATE_KEY_LENGTH: usize, @@ -159,6 +165,30 @@ where } } +impl pkcs8::EncodePublicKey for NistPublicKey +where + C: Curve + CurveArithmetic + pkcs8::AssociatedOid, + ::FieldBytesSize: sec1::ModulusSize, + ::AffinePoint: sec1::ToEncodedPoint, + ::AffinePoint: sec1::FromEncodedPoint, +{ + fn to_public_key_der(&self) -> pkcs8::spki::Result { + self.0.to_public_key_der() + } +} + +impl pkcs8::DecodePublicKey for NistPublicKey +where + C: Curve + CurveArithmetic + pkcs8::AssociatedOid, + ::FieldBytesSize: sec1::ModulusSize, + ::AffinePoint: sec1::ToEncodedPoint, + ::AffinePoint: sec1::FromEncodedPoint, +{ + fn from_public_key_der(bytes: &[u8]) -> pkcs8::spki::Result { + Ok(Self(PublicKey::from_public_key_der(bytes)?)) + } +} + #[cfg(all(test, feature = "aes"))] mod tests { use elliptic_curve::{sec1, Curve, CurveArithmetic}; diff --git a/src/asymmetric_crypto/nist/test_pkcs8_openssl_compat.rs b/src/asymmetric_crypto/curves/nist/test_pkcs8_openssl_compat.rs similarity index 50% rename from src/asymmetric_crypto/nist/test_pkcs8_openssl_compat.rs rename to src/asymmetric_crypto/curves/nist/test_pkcs8_openssl_compat.rs index 5f8ffd4..646820e 100644 --- a/src/asymmetric_crypto/nist/test_pkcs8_openssl_compat.rs +++ b/src/asymmetric_crypto/curves/nist/test_pkcs8_openssl_compat.rs @@ -8,8 +8,11 @@ mod tests { pkey::{PKey, Public}, symm::Cipher, }; + use pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey}; - use crate::{CryptoCoreError, P256PrivateKey, P256PublicKey}; + use crate::{ + reexport::rand_core::SeedableRng, CryptoCoreError, CsRng, P256PrivateKey, P256PublicKey, + }; #[test] fn test_pkcs8_openssl_key_compat() -> Result<(), CryptoCoreError> { @@ -24,6 +27,83 @@ mod tests { let private_key_pkcs8 = private_key.private_key_to_pkcs8()?; let public_key_pkcs8 = private_key.public_key_to_der()?; + // convert to Nist private Key + let nist_public_key = P256PublicKey::from_public_key_der(&public_key_pkcs8)?; + let nist_private_key = P256PrivateKey::from_pkcs8_der(&private_key_pkcs8)?; + assert_eq!(P256PublicKey::from(&nist_private_key), nist_public_key); + + // convert back to openssl + let openssl_private_key = + PKey::private_key_from_pkcs8(&nist_private_key.to_pkcs8_der().map(|d| d.to_bytes())?)?; + let openssl_ec_key = openssl_private_key.ec_key()?; + // check same private key + assert_eq!(openssl_ec_key.private_key().to_owned()?, ec_ky_big_num); + let openssl_public_key = PKey::::public_key_from_der( + &nist_public_key.to_public_key_der().map(|d| d.to_vec())?, + )?; + // check that the recovered public key matches the original one + assert!(private_key.public_eq(&openssl_public_key)); + + Ok(()) + } + + #[test] + fn test_encrypted_pkcs8_openssl_key_compat() -> Result<(), CryptoCoreError> { + let mut rng = CsRng::from_entropy(); + let nid = Nid::X9_62_PRIME256V1; // NIST P-256 curve + let group = EcGroup::from_curve_name(nid)?; + let ec_key = EcKey::generate(&group)?; + // used or later assert + let ec_ky_big_num = ec_key.private_key().to_owned()?; + + // convert to Openssl generic key + let private_key = PKey::from_ec_key(ec_key)?; + let private_key_pkcs8 = + private_key.private_key_to_pkcs8_passphrase(Cipher::aes_256_cbc(), b"blah")?; + let public_key_pkcs8 = private_key.public_key_to_der()?; + + // convert to Nist private Key + let nist_public_key = P256PublicKey::from_public_key_der(&public_key_pkcs8)?; + let nist_private_key = + P256PrivateKey::from_pkcs8_encrypted_der(&private_key_pkcs8, b"blah")?; + assert_eq!(P256PublicKey::from(&nist_private_key), nist_public_key); + + // convert back to openssl + let private_key_pkcs8 = nist_private_key.to_pkcs8_encrypted_der(&mut rng, b"blah")?; + { + // check we can decrypt with this library + let nist_private_key_ = + P256PrivateKey::from_pkcs8_encrypted_der(private_key_pkcs8.as_bytes(), b"blah")?; + assert_eq!(nist_private_key_, nist_private_key); + } + + let openssl_private_key = + PKey::private_key_from_pkcs8_passphrase(private_key_pkcs8.as_bytes(), b"blah")?; + let openssl_ec_key = openssl_private_key.ec_key()?; + // check same private key + assert_eq!(openssl_ec_key.private_key().to_owned()?, ec_ky_big_num); + let openssl_public_key = + PKey::::public_key_from_der(nist_public_key.to_public_key_der()?.as_bytes())?; + // check that the recovered public key matches the original one + assert!(private_key.public_eq(&openssl_public_key)); + + Ok(()) + } + + #[allow(deprecated)] + #[test] + fn test_pkcs8_openssl_key_deprecated_compat() -> Result<(), CryptoCoreError> { + let nid = Nid::X9_62_PRIME256V1; // NIST P-256 curve + let group = EcGroup::from_curve_name(nid)?; + let ec_key = EcKey::generate(&group)?; + // used or later assert + let ec_ky_big_num = ec_key.private_key().to_owned()?; + + // convert to Openssl generic key + let private_key = PKey::from_ec_key(ec_key)?; + let private_key_pkcs8 = private_key.private_key_to_pkcs8()?; + let public_key_pkcs8 = private_key.public_key_to_der()?; + // convert to Nist private Key let nist_public_key = P256PublicKey::try_from_pkcs8(&public_key_pkcs8)?; let nist_private_key = P256PrivateKey::try_from_pkcs8(&private_key_pkcs8)?; @@ -42,8 +122,9 @@ mod tests { Ok(()) } + #[allow(deprecated)] #[test] - fn test_encrypted_pkcs8_openssl_key_compat() -> Result<(), CryptoCoreError> { + fn test_encrypted_pkcs8_openssl_key_deprecated_compat() -> Result<(), CryptoCoreError> { let nid = Nid::X9_62_PRIME256V1; // NIST P-256 curve let group = EcGroup::from_curve_name(nid)?; let ec_key = EcKey::generate(&group)?; diff --git a/src/asymmetric_crypto/mod.rs b/src/asymmetric_crypto/mod.rs index 972458a..d3e4b5b 100644 --- a/src/asymmetric_crypto/mod.rs +++ b/src/asymmetric_crypto/mod.rs @@ -1,9 +1,25 @@ -#[cfg(feature = "curve25519")] -mod curve_25519; -#[cfg(feature = "nist_curves")] -mod nist; - -#[cfg(feature = "curve25519")] -pub use curve_25519::*; -#[cfg(feature = "nist_curves")] -pub use nist::*; +mod curves; +pub use curves::*; + +#[cfg(feature = "rsa")] +mod rsa; +#[cfg(feature = "rsa")] +pub use self::rsa::*; + +pub trait PublicKey {} + +pub trait PrivateKey { + type PublicKey: PublicKey; + + fn public_key(&self) -> Self::PublicKey; +} + +impl From<&T> for Box +where + T: PrivateKey, + T::PublicKey: 'static, +{ + fn from(private_key: &T) -> Self { + Box::new(private_key.public_key()) + } +} diff --git a/src/asymmetric_crypto/rsa/mod.rs b/src/asymmetric_crypto/rsa/mod.rs new file mode 100644 index 0000000..53d867c --- /dev/null +++ b/src/asymmetric_crypto/rsa/mod.rs @@ -0,0 +1,88 @@ +mod private_key; +pub use private_key::RsaPrivateKey; + +mod public_key; +pub use public_key::RsaPublicKey; + +#[cfg(test)] +mod tests; + +/// Supported RSA key length (length of the modulus) +/// +/// To be compliant with FIPS 186-5 (Digital Signature standards), +/// the length of the modulus must be greater than 2048 bits. +/// [https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum RsaKeyLength { + Modulus2048 = 2048, + Modulus3072 = 3072, + Modulus4096 = 4096, +} + +/// Supported PKCS#11 compatible key wrapping algorithms for RSA +/// +/// If in doubt, use the `Aes256Sha256` algorithm with a 3072 bits RSA key. +/// +/// Check the PKCS#11 OASIS specification for more details +/// [https://docs.oasis-open.org/pkcs11/pkcs11-curr/v3.0/pkcs11-curr-v3.0.html] +/// +/// For Google Cloud KMS compatibility, check: +/// [https://cloud.google.com/kms/docs/key-wrapping?hl=en] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RsaKeyWrappingAlgorithm { + /// PKCS #1 v1.5 RS following PKCS#11 CKM_RSA_PKCS + /// The maximum possible plaintext length is m = k - 11, + /// where k is the size of the RSA modulus. + Pkcs1v1_5, + /// PKCS #1 RSA with OAEP block format following PKCS#11 CKM_RSA_PKCS_OAEP + /// The hash function used is SHA256 + /// The maximum possible plaintext length is m = k - 2 * h_len - 2, + /// where k is the size of the RSA modulus + /// and h_len is the size of the hash of the optional label. + OaepSha256, + /// PKCS #1 RSA with OAEP block format following PKCS#11 CKM_RSA_PKCS_OAEP + /// The hash function used is SHA1. For that reason this algorithm is not + /// recommended and is only kept here for compatibility with legacy + /// systems. The maximum possible plaintext length is m = k - 2 * h_len + /// - 2, where k is the size of the RSA modulus + /// and h_len is the size of the hash of the optional label. + /// This algorithm is compatible with Google Cloud KMS + /// - RSA_OAEP_3072_SHA256 with RSA 3072 bits key + /// - RSA_OAEP_4096_SHA256 with RSA 4096 bits key + OaepSha1, + /// PKCS #1 RSA with OAEP block format following PKCS#11 CKM_RSA_PKCS_OAEP + /// The hash function used is SHA3. + /// and is only kept here for compatibility with legacy systems. + /// The maximum possible plaintext length is m = k - 2 * h_len - 2, + /// where k is the size of the RSA modulus + /// and h_len is the size of the hash of the optional label. + OaepSha3, + /// Key wrap with AES following PKCS#11 CKM_RSA_AES_KEY_WRAP + /// using an AES key of 256 bits. The hash function used is SHA256. + /// The AES wrapping follows the RFC 5649 which is compatible with PKCS#11 + /// CKM_AES_KEY_WRAP_KWP since there is no limitation on the size of the + /// plaintext; the recommended plaintext format for an EC Private key is + /// PKCS#8. This is the recommended key wrapping algorithm. + /// This algorithm is compatible with Google Cloud KMS + /// - RSA_OAEP_3072_SHA256_AES_256 for RSA 3072 bits key + /// - RSA_OAEP_4096_SHA256_AES_256 for RSA 4096 bits key + Aes256Sha256, + /// Key wrap with AES following PKCS#11 CKM_RSA_AES_KEY_WRAP + /// using an AES key of 256 bits. The hash function used is SHA1. + /// For that reason this algorithm is not recommended + /// and is only kept here for compatibility with legacy systems. + /// The AES wrapping follows the RFC 5649 which is compatible with PKCS#11 + /// CKM_AES_KEY_WRAP_KWP since there is no limitation on the size of the + /// plaintext; the recommended plaintext format for an EC Private key is + /// PKCS#8. This algorithm is compatible with Google Cloud KMS + /// - RSA_OAEP_3072_SHA1_AES_256 for RSA 3072 bits key + /// - RSA_OAEP_4096_SHA1_AES_256 for RSA 4096 bits key + Aes256Sha1, + /// Key wrap with AES following PKCS#11 CKM_RSA_AES_KEY_WRAP + /// using an AES key of 256 bits. The hash function used is SHA3-256 + /// (defined in FIPS 202). The AES wrapping follows the RFC 5649 which + /// is compatible with PKCS#11 CKM_AES_KEY_WRAP_KWP since there is no + /// limitation on the size of the plaintext; the recommended + /// plaintext format for an EC Private key is PKCS#8. + Aes256Sha3, +} diff --git a/src/asymmetric_crypto/rsa/private_key.rs b/src/asymmetric_crypto/rsa/private_key.rs new file mode 100644 index 0000000..a5b1a2e --- /dev/null +++ b/src/asymmetric_crypto/rsa/private_key.rs @@ -0,0 +1,150 @@ +use digest::{Digest, DynDigest}; +use pkcs8::SecretDocument; +use rsa::traits::PublicKeyParts; +use zeroize::{ZeroizeOnDrop, Zeroizing}; + +use crate::{ + asymmetric_crypto::{PrivateKey, RsaKeyLength, RsaKeyWrappingAlgorithm, RsaPublicKey}, + key_unwrap, pkcs8_fix, + reexport::rand_core::{CryptoRng, CryptoRngCore, RngCore}, + CryptoCoreError, +}; + +#[derive(Hash, Clone, Debug, PartialEq, Eq)] +pub struct RsaPrivateKey(rsa::RsaPrivateKey); + +impl RsaPrivateKey { + /// Generate a random private key + pub fn new( + rng: &mut R, + key_length: RsaKeyLength, + ) -> Result { + Ok(Self(rsa::RsaPrivateKey::new( + rng, + (key_length as i32) as usize, + )?)) + } + + /// Get the key length which is the modulus size in bits + #[must_use] + pub fn key_length(&self) -> RsaKeyLength { + match self.0.n().bits() { + 2048 => RsaKeyLength::Modulus2048, + 3072 => RsaKeyLength::Modulus3072, + 4096 => RsaKeyLength::Modulus4096, + _ => panic!("Invalid RSA key length; this should never happen"), + } + } +} + +/// Key wrapping support +impl RsaPrivateKey { + /// Unwrap a key + pub fn unwrap_key( + &self, + wrapping_algorithm: RsaKeyWrappingAlgorithm, + encrypted_key_material: &[u8], + ) -> Result>, CryptoCoreError> { + Ok(match wrapping_algorithm { + RsaKeyWrappingAlgorithm::Pkcs1v1_5 => Zeroizing::from( + self.0 + .decrypt(rsa::Pkcs1v15Encrypt, encrypted_key_material)?, + ), + RsaKeyWrappingAlgorithm::OaepSha256 => { + ckm_rsa_pkcs_oaep::(self, encrypted_key_material)? + } + RsaKeyWrappingAlgorithm::OaepSha1 => { + ckm_rsa_pkcs_oaep::(self, encrypted_key_material)? + } + RsaKeyWrappingAlgorithm::OaepSha3 => { + ckm_rsa_pkcs_oaep::(self, encrypted_key_material)? + } + RsaKeyWrappingAlgorithm::Aes256Sha256 => { + ckm_rsa_aes_key_unwrap::(self, encrypted_key_material)? + } + RsaKeyWrappingAlgorithm::Aes256Sha1 => { + ckm_rsa_aes_key_unwrap::(self, encrypted_key_material)? + } + RsaKeyWrappingAlgorithm::Aes256Sha3 => { + ckm_rsa_aes_key_unwrap::(self, encrypted_key_material)? + } + }) + } +} + +impl PrivateKey for RsaPrivateKey { + type PublicKey = RsaPublicKey; + + fn public_key(&self) -> Self::PublicKey { + self.0.to_public_key().into() + } +} + +/// Facades +impl RsaPrivateKey { + /// Get the public key + /// + /// This is a facade for [`PrivateKey::public_key`] + #[must_use] + pub fn public_key(&self) -> ::PublicKey { + PrivateKey::public_key(self) + } +} + +impl ZeroizeOnDrop for RsaPrivateKey {} + +/// Implementation of PKCS#1 RSA OAEP (`CKM_RSA_PKCS_OAEP`) +/// [https://docs.oasis-open.org/pkcs11/pkcs11-curr/v3.0/os/pkcs11-curr-v3.0-os.html#_Toc30061137] +fn ckm_rsa_pkcs_oaep( + rsa_private_key: &RsaPrivateKey, + encrypted_key_material: &[u8], +) -> Result>, CryptoCoreError> { + let padding = rsa::Oaep::new::(); + Ok(Zeroizing::from( + rsa_private_key.0.decrypt(padding, encrypted_key_material)?, + )) +} + +/// Implementation of PKCS#11 RSA AES KEY WRAP (`CKM_RSA_AES_KEY_WRAP`) +/// [https://docs.oasis-open.org/pkcs11/pkcs11-curr/v3.0/os/pkcs11-curr-v3.0-os.html#_Toc30061152] +/// +/// Also check [https://cloud.google.com/kms/docs/key-wrapping?hl=fr] +fn ckm_rsa_aes_key_unwrap( + rsa_private_key: &RsaPrivateKey, + ciphertext: &[u8], +) -> Result>, CryptoCoreError> { + // Splits the input into two parts. The first is the wrapped AES key, and the + // second is the wrapped target key. The length of the first part is equal to + // the length of the unwrapping RSA key. + let encapsulation_bytes_len = ((rsa_private_key.key_length() as i32) / 8) as usize; + // Un-wraps the temporary AES key from the first part with the private RSA key + // using CKM_RSA_PKCS_OAEP with parameters of OAEPParams. + let aes_key = ckm_rsa_pkcs_oaep::(rsa_private_key, &ciphertext[0..encapsulation_bytes_len])?; + // Un-wraps the target key from the second part with the temporary AES key using + // CKM_AES_KEY_WRAP_KWP ([AES KEYWRAP] section 6.3). Zeroizes the temporary + // AES key. Returns the handle to the newly unwrapped target key. + Ok(Zeroizing::from(key_unwrap( + &ciphertext[encapsulation_bytes_len..], + &aes_key, + )?)) +} + +impl pkcs8::EncodePrivateKey for RsaPrivateKey { + fn to_pkcs8_der(&self) -> pkcs8::Result { + self.0.to_pkcs8_der() + } + + fn to_pkcs8_encrypted_der( + &self, + rng: impl CryptoRng + RngCore, + password: impl AsRef<[u8]>, + ) -> pkcs8::Result { + pkcs8_fix::to_pkcs8_encrypted_der(&self.to_pkcs8_der()?, rng, password) + } +} + +impl pkcs8::DecodePrivateKey for RsaPrivateKey { + fn from_pkcs8_der(bytes: &[u8]) -> pkcs8::Result { + Ok(Self(rsa::RsaPrivateKey::from_pkcs8_der(bytes)?)) + } +} diff --git a/src/asymmetric_crypto/rsa/public_key.rs b/src/asymmetric_crypto/rsa/public_key.rs new file mode 100644 index 0000000..bf9c118 --- /dev/null +++ b/src/asymmetric_crypto/rsa/public_key.rs @@ -0,0 +1,120 @@ +use digest::{Digest, DynDigest}; +use rand_chacha::rand_core::CryptoRngCore; +use rsa::traits::PublicKeyParts; +use zeroize::Zeroizing; + +use crate::{ + asymmetric_crypto::{PublicKey, RsaKeyLength, RsaKeyWrappingAlgorithm}, + key_wrap, CryptoCoreError, RandomFixedSizeCBytes, SymmetricKey, +}; + +#[derive(Debug, PartialEq)] +pub struct RsaPublicKey(pub(super) rsa::RsaPublicKey); + +impl RsaPublicKey { + /// Get the key length which is the modulus size in bits + #[must_use] + pub fn key_length(&self) -> RsaKeyLength { + match self.0.n().bits() { + 2048 => RsaKeyLength::Modulus2048, + 3072 => RsaKeyLength::Modulus3072, + 4096 => RsaKeyLength::Modulus4096, + _ => panic!("Invalid RSA key length; this should never happen"), + } + } + + /// Wrap a key using PKCS #1 v1.5 RS (also denoted `CKM_RSA_PKCS`) + pub fn wrap_key( + &self, + rng: &mut R, + wrapping_algorithm: RsaKeyWrappingAlgorithm, + key_material: &Zeroizing>, + ) -> Result, CryptoCoreError> { + match wrapping_algorithm { + RsaKeyWrappingAlgorithm::Pkcs1v1_5 => self + .0 + .encrypt(&mut *rng, rsa::Pkcs1v15Encrypt, key_material) + .map_err(CryptoCoreError::from), + RsaKeyWrappingAlgorithm::OaepSha256 => { + ckm_rsa_pkcs_oaep::(rng, self, key_material) + } + RsaKeyWrappingAlgorithm::OaepSha1 => { + ckm_rsa_pkcs_oaep::(rng, self, key_material) + } + RsaKeyWrappingAlgorithm::OaepSha3 => { + ckm_rsa_pkcs_oaep::(rng, self, key_material) + } + RsaKeyWrappingAlgorithm::Aes256Sha256 => { + ckm_rsa_aes_key_wrap::(rng, self, key_material) + } + RsaKeyWrappingAlgorithm::Aes256Sha1 => { + ckm_rsa_aes_key_wrap::(rng, self, key_material) + } + RsaKeyWrappingAlgorithm::Aes256Sha3 => { + ckm_rsa_aes_key_wrap::(rng, self, key_material) + } + } + } +} + +impl PublicKey for RsaPublicKey {} + +impl From for RsaPublicKey { + fn from(key: rsa::RsaPublicKey) -> Self { + Self(key) + } +} + +/// Implementation of PKCS#1 RSA OAEP (`CKM_RSA_PKCS_OAEP`) +/// [https://docs.oasis-open.org/pkcs11/pkcs11-curr/v3.0/os/pkcs11-curr-v3.0-os.html#_Toc30061137] +fn ckm_rsa_pkcs_oaep( + rng: &mut R, + rsa_public_key: &RsaPublicKey, + key_material: &Zeroizing>, +) -> Result, CryptoCoreError> { + let padding = rsa::Oaep::new::(); + Ok(rsa_public_key.0.encrypt(&mut *rng, padding, key_material)?) +} + +/// Implementation of PKCS#11 RSA AES KEY WRAP (`CKM_RSA_AES_KEY_WRAP`) +/// [https://docs.oasis-open.org/pkcs11/pkcs11-curr/v3.0/os/pkcs11-curr-v3.0-os.html#_Toc30061152] +/// +/// The `AES_KEY_LENGTH` is the length of the AES key in bytes. +fn ckm_rsa_aes_key_wrap< + H: 'static + Digest + DynDigest + Send + Sync, + const AES_KEY_LENGTH: usize, + R: CryptoRngCore, +>( + rng: &mut R, + rsa_public_key: &RsaPublicKey, + key_material: &Zeroizing>, +) -> Result, CryptoCoreError> { + // Generates a temporary random AES key of ulAESKeyBits length. This key is not + // accessible to the user - no handle is returned. + let key_encryption_key = SymmetricKey::::new(rng); + // Wraps the target key with the temporary AES key using CKM_AES_KEY_WRAP_KWP + // ([AES KEYWRAP] section 6.3). PKCS#11 CKM_AES_KEY_WRAP_KWP is identical to + // tRFC 5649 + let mut ciphertext = key_wrap(key_material, &key_encryption_key)?; + //Wraps the AES key with the wrapping RSA key using CKM_RSA_PKCS_OAEP with + // parameters of OAEPParams. Zeroizes the temporary AES key (automatically + // done by the conversion into()) + let mut wrapped_kwk = + ckm_rsa_pkcs_oaep::(rng, rsa_public_key, &key_encryption_key.into())?; + // Concatenates two wrapped keys and outputs the concatenated blob. + // The first is the wrapped AES key, and the second is the wrapped target key. + wrapped_kwk.append(&mut ciphertext); + Ok(wrapped_kwk) +} + +impl pkcs8::EncodePublicKey for RsaPublicKey { + fn to_public_key_der(&self) -> pkcs8::spki::Result { + self.0.to_public_key_der() + } +} + +impl pkcs8::DecodePublicKey for RsaPublicKey { + fn from_public_key_der(bytes: &[u8]) -> pkcs8::spki::Result { + Ok(Self(rsa::RsaPublicKey::from_public_key_der(bytes)?)) + } +} diff --git a/src/asymmetric_crypto/rsa/tests/key_wrapping.rs b/src/asymmetric_crypto/rsa/tests/key_wrapping.rs new file mode 100644 index 0000000..1216990 --- /dev/null +++ b/src/asymmetric_crypto/rsa/tests/key_wrapping.rs @@ -0,0 +1,191 @@ +use openssl::{ + encrypt::{Decrypter, Encrypter}, + hash::MessageDigest, + pkey::PKey, + rsa::Padding, +}; +use pkcs8::{DecodePrivateKey, DecodePublicKey}; +use zeroize::Zeroizing; + +use crate::{ + reexport::rand_core::{RngCore, SeedableRng}, + CsRng, RsaKeyLength, RsaKeyWrappingAlgorithm, RsaPrivateKey, RsaPublicKey, +}; + +fn wrap_unwrap( + rng: &mut CsRng, + rsa_private_key: &RsaPrivateKey, + wrapping_algorithm: RsaKeyWrappingAlgorithm, +) { + let mut msg = [0_u8; 32]; + rng.fill_bytes(&mut msg); + + let mut key_to_wrap = [0_u8; 189]; + rng.fill_bytes(&mut key_to_wrap); + let key_to_wrap = Zeroizing::from(key_to_wrap.to_vec()); + + let rsa_public_key = rsa_private_key.public_key(); + + let wrapped_key = rsa_public_key + .wrap_key(rng, wrapping_algorithm, &key_to_wrap) + .unwrap(); + assert_eq!( + rsa_private_key + .unwrap_key(wrapping_algorithm, &wrapped_key) + .unwrap(), + key_to_wrap + ); +} + +fn test_all_algos(rng: &mut CsRng, rsa_private_key: &RsaPrivateKey) { + print!("Pkcs1v1_5..."); + wrap_unwrap(rng, rsa_private_key, RsaKeyWrappingAlgorithm::Pkcs1v1_5); + println!("ok."); + + print!("OaepSha256..."); + wrap_unwrap(rng, rsa_private_key, RsaKeyWrappingAlgorithm::OaepSha256); + println!("ok."); + + print!("OaepSha1..."); + wrap_unwrap(rng, rsa_private_key, RsaKeyWrappingAlgorithm::OaepSha1); + println!("ok."); + + print!("OaepSha3..."); + wrap_unwrap(rng, rsa_private_key, RsaKeyWrappingAlgorithm::OaepSha3); + println!("ok."); + + print!("Aes256Sha256..."); + wrap_unwrap(rng, rsa_private_key, RsaKeyWrappingAlgorithm::Aes256Sha256); + println!("ok."); + + print!("Aes256Sha1..."); + wrap_unwrap(rng, rsa_private_key, RsaKeyWrappingAlgorithm::Aes256Sha1); + println!("ok."); + + print!("Aes256Sha3..."); + wrap_unwrap(rng, rsa_private_key, RsaKeyWrappingAlgorithm::Aes256Sha3); + println!("ok."); +} + +#[test] +fn test_wrap_unwrap() { + let mut rng = CsRng::from_entropy(); + println!("Generating 3072 bit RSA key..."); + let rsa_private_key = RsaPrivateKey::new(&mut rng, RsaKeyLength::Modulus3072).unwrap(); + test_all_algos(&mut rng, &rsa_private_key); + println!("Generating 4096 bit RSA key..."); + let rsa_private_key = RsaPrivateKey::new(&mut rng, RsaKeyLength::Modulus4096).unwrap(); + test_all_algos(&mut rng, &rsa_private_key); + println!("Generating 2048 bit RSA key..."); + let rsa_private_key = RsaPrivateKey::new(&mut rng, RsaKeyLength::Modulus2048).unwrap(); + test_all_algos(&mut rng, &rsa_private_key); +} + +#[test] +fn test_openssl_wrap_oaep_sha256_compat() { + let mut rng = CsRng::from_entropy(); + + // load the secret from the disk + let secret = Zeroizing::from( + std::fs::read("src/asymmetric_crypto/rsa/tests/openssl/32_byte.key").unwrap(), + ); + + // load the binary file from the disk + let rsa_public_key_pem = String::from_utf8( + std::fs::read("src/asymmetric_crypto/rsa/tests/openssl/rsa_3072_bit.pub.pkcs8.pem") + .unwrap(), + ) + .unwrap(); + // load the RSA public key from the PEM file + let rsa_public_key = RsaPublicKey::from_public_key_pem(&rsa_public_key_pem).unwrap(); + + let encrypted = rsa_public_key + .wrap_key(&mut rng, RsaKeyWrappingAlgorithm::OaepSha256, &secret) + .unwrap(); + + // OpenSSL + + // load the binary file from the disk + let rsa_private_key_pem = + std::fs::read("src/asymmetric_crypto/rsa/tests/openssl/rsa_3072_bit.key.pkcs8.pem") + .unwrap(); + + let rsa_private_key = PKey::private_key_from_pem(&rsa_private_key_pem).unwrap(); + let mut decrypter = Decrypter::new(&rsa_private_key).unwrap(); + decrypter.set_rsa_mgf1_md(MessageDigest::sha256()).unwrap(); + decrypter.set_rsa_oaep_md(MessageDigest::sha256()).unwrap(); + decrypter.set_rsa_padding(Padding::PKCS1_OAEP).unwrap(); + + // Get the length of the output buffer + let buffer_len = decrypter.decrypt_len(&encrypted).unwrap(); + let mut decoded = vec![0u8; buffer_len]; + + // Decrypt the data and get its length + let decoded_len = decrypter.decrypt(&encrypted, &mut decoded).unwrap(); + + // Use only the part of the buffer with the decrypted data + let decoded = &decoded[..decoded_len]; + + assert_eq!(decoded, &secret[..]); +} + +#[test] +fn test_openssl_unwrap_oaep_sha256_compat() { + // load the secret from the disk + let secret = Zeroizing::from( + std::fs::read("src/asymmetric_crypto/rsa/tests/openssl/32_byte.key").unwrap(), + ); + + // openssl + + // load the binary file from the disk + let rsa_public_key_pem = + std::fs::read("src/asymmetric_crypto/rsa/tests/openssl/rsa_3072_bit.pub.pkcs8.pem") + .unwrap(); + let rsa_public_key = PKey::public_key_from_pem(&rsa_public_key_pem).unwrap(); + + let mut encrypter = Encrypter::new(&rsa_public_key).unwrap(); + encrypter.set_rsa_mgf1_md(MessageDigest::sha256()).unwrap(); + encrypter.set_rsa_oaep_md(MessageDigest::sha256()).unwrap(); + encrypter.set_rsa_padding(Padding::PKCS1_OAEP).unwrap(); + + let buffer_len = encrypter.encrypt_len(&secret).unwrap(); + let mut wrapped_secret = vec![0u8; buffer_len]; + + // Encode the data and get its length + let encoded_len = encrypter.encrypt(&secret, &mut wrapped_secret).unwrap(); + + // Use only the part of the buffer with the encoded data + let wrapped_secret = &wrapped_secret[..encoded_len]; + + // crypto-core + + // load the binary file from the disk + let rsa_private_key_pem = String::from_utf8( + std::fs::read("src/asymmetric_crypto/rsa/tests/openssl/rsa_3072_bit.key.pkcs8.pem") + .unwrap(), + ) + .unwrap(); + // load the RSA private key from the PEM file + let rsa_private_key = RsaPrivateKey::from_pkcs8_pem(&rsa_private_key_pem).unwrap(); + + // use the wrapped_secret generated by openssl using the crate + + let recovered = rsa_private_key + .unwrap_key(RsaKeyWrappingAlgorithm::OaepSha256, wrapped_secret) + .unwrap(); + + assert_eq!(recovered, secret); + + // use the ile generated by openssl using the CLI + let wrapped_secret = Zeroizing::from( + std::fs::read("src/asymmetric_crypto/rsa/tests/openssl/32_byte.key.oaep_sha256.enc") + .unwrap(), + ); + + let recovered = rsa_private_key + .unwrap_key(RsaKeyWrappingAlgorithm::OaepSha256, &wrapped_secret) + .unwrap(); + + assert_eq!(recovered, secret); +} diff --git a/src/asymmetric_crypto/rsa/tests/mod.rs b/src/asymmetric_crypto/rsa/tests/mod.rs new file mode 100644 index 0000000..15ac10a --- /dev/null +++ b/src/asymmetric_crypto/rsa/tests/mod.rs @@ -0,0 +1,2 @@ +mod key_wrapping; +mod pkcs8_tests; diff --git a/src/asymmetric_crypto/rsa/tests/openssl/32_byte.key b/src/asymmetric_crypto/rsa/tests/openssl/32_byte.key new file mode 100644 index 0000000..1754db9 --- /dev/null +++ b/src/asymmetric_crypto/rsa/tests/openssl/32_byte.key @@ -0,0 +1 @@ +G¼Ù2ô‚C*Vrž²üܽŒ´é)»¢5çÙãANŠîÁ \ No newline at end of file diff --git a/src/asymmetric_crypto/rsa/tests/openssl/32_byte.key.oaep_sha256.enc b/src/asymmetric_crypto/rsa/tests/openssl/32_byte.key.oaep_sha256.enc new file mode 100644 index 0000000..ddd762e Binary files /dev/null and b/src/asymmetric_crypto/rsa/tests/openssl/32_byte.key.oaep_sha256.enc differ diff --git a/src/asymmetric_crypto/rsa/tests/openssl/README.md b/src/asymmetric_crypto/rsa/tests/openssl/README.md new file mode 100644 index 0000000..eb9e14c --- /dev/null +++ b/src/asymmetric_crypto/rsa/tests/openssl/README.md @@ -0,0 +1,37 @@ +Files generated using command line `openssl` to evaluate the compatibility with RSA implementation. + +## Random RSA 3072 bit PEM private key + +```sh +openssl genrsa -out rsa_3072_bit.key.pkcs8.pem 3072 + +``` + +## RSA 3072 bit PEM public key + +```sh +openssl rsa -in rsa_3072_bit.key.pkcs8.pem -pubout -out rsa_3072_bit.pub + +``` + +## Random 32 byte secret key + +```sh +openssl rand -out 32_byte.key 32 + +``` + +## Wrap the secret with CKM_RSA_PKCS_OAEP mechanism, sha2:sha256 + +```sh +openssl pkeyutl \ + -encrypt \ + -pubin \ + -inkey rsa_3072_bit.pub.pkcs8.pem \ + -in 32_byte.key \ + -out 32_byte.key.oaep_sha256.enc \ + -pkeyopt rsa_padding_mode:oaep \ + -pkeyopt rsa_oaep_md:sha256 \ + -pkeyopt rsa_mgf1_md:sha256 + +``` diff --git a/src/asymmetric_crypto/rsa/tests/openssl/rsa_3072_bit.key.pkcs8.pem b/src/asymmetric_crypto/rsa/tests/openssl/rsa_3072_bit.key.pkcs8.pem new file mode 100644 index 0000000..4e0d722 --- /dev/null +++ b/src/asymmetric_crypto/rsa/tests/openssl/rsa_3072_bit.key.pkcs8.pem @@ -0,0 +1,40 @@ +-----BEGIN PRIVATE KEY----- +MIIG/AIBADANBgkqhkiG9w0BAQEFAASCBuYwggbiAgEAAoIBgQDVVLSMjD4/J5aJ +BnRRfIvCdhYkKhqj4Yth8RH67on0YA8NtZG69/shwQDxqC38LlN/W/kj2uISF05s +E2Zed7ASsLJz8izc9Ijo9jwGgWsWPUStQ0EyeN9Uu/CAQW6Lb9Shi287Q0egUVBG +o8/0vvX5JAgEgbLM2KW7ZCazwEVrrRIZmN3sjqL1Tvp/N0AISsn4MAQtZT6n8XPh +wwE4quPs05TnJV4TXaRgaHjX3kdyda1Y+3IlqA4FztdcQbJ7QZuQ+tSpLPqwwgC1 +0I3DFcIHB/4n/DUTmHXh7O/6LBPMDt9Vg3UKS8SiNGkfk0cvdrB9qi7lv36pNXSy +jDMtL+7jr+pUwnLwOcZN9ynUB0pW1hpspABPPBc7CO8KT2+ti4+RQpDqZIw5lI+w +dqdJx2byYCR9nDqOtvu845fKQ6V8Jf6evlXjkB5I3dWH0EVsoZoq2mguod/BSbho +5LgcVc2ktSUBkY8e87Ahi0bEQE+uown606EsGlAZRe2oV/GLJtUCAwEAAQKCAYAF +ZdoW34YZjWQVURygFeDwwxpJ62/hZOBCp+aAb1XK0D2NAPxUdumXa5YdVAsjskFz +8P8obxi8xC68eM11ov9A3hTF3b77hq2KTeT4ywj+f3aj7lqEomupkAMI7NIdxveM +5/BLpECR+xqaW8Y4N0fz5xUYDW57kLJ1HafB14hIA3fetavmzEnx9O7Vr+K8Fz7D +KqG7GA52eJtMB8PM0DQDZ8nNrd5fYgExj9VMzuA6VIerR8oFE8U/mKPW6aEMPI6y +n5tKJbXz0Xcecj4OZmJ2syrCI8SbakSPeVPs2CH/4A7fFqaEI3J1WFXVyxWWTD3Q ++3Lu5BdX+2z4cAfxJF4qT/uu+susP8jFpVU6WhCHPlt6/LClQUOYbg4Z2tYC8hVi +Qw+NdL9g4btzOsei/Gocrq/+dhokoeFMAEjnWVVGzJUOWDcXHQI1dmmKlVdr084F +4lMPcbOBCcAlfl6XCoE3701/oNanjnUOg3CCjws0Nf0asecuJqTR1p3ZJciukskC +gcEA+MqOVi9il8BrptMTt62yFQDFbWEOTrbPYFU92aK2Aery6PACL8VtGSeu8S7V +Cf8ukAHb5q4RiKxnvtA3H+nvIrupZ3ars4tR/ch4GBFLKEJpoiHR/XW/0EeqDFCc +XDermRWmXhoM+o4qrNoQRtLeDvxriWcfP41sRQsFWO40OZFQtuiyd3nBH8nhifV/ +/Fc3PqDO4dXYi2bIotdaPH48wNEtjDsd4GpsRQ782Kdf5o1TK8rV+L2OnAyc4hQG +djkvAoHBANuDHfoUPc7J7Fjm6/Vv9OVIOfKNwneT81gSYqW0VqVT5FKHbVF74Cae +hmSsgJau7ANniiZEG7+Mq5yyG4dOytSxDF9Bh5229mM0xpKNgFlLqML9Xp4NLtT6 +17TXGVDHIRIFkXGMLKjhX/y+DH+ff5mt4lsR9ZGaZA4HHcTTdmK2lR+wE/RLmF5w +IwQO3jSnzEb3W8+XjtNX+miaziWTT7dJkrjM4PP5lAd4V+WdW4yzrTGJZk+2dDnG +kYFtpbNXOwKBwB5Xzf5S7ypk7Hw/OCNpytNm8a1CbhVJlIdCPjXh82LOfevNAedD +o+eygFKoDJGYjtw/bct3pWnO5SRPjrmUtXa9/o5A2fYe4tQZ9BOzYEA7wVhl2Y+Z +IdjMNMAlwLw6ifI0wsKtWLHiTvAf9GY+55haTt9GNXhxziepjXBoNPRkdbypKsMJ +lXVEEP7bc02acYFw6Wp/w1ZVJdk+fKHuH4jAulZlC2LIeAYAKvRdjYO37tHjfi9F +JzR/EU89dABNPwKBwGFxHsl4WseE0ynTuELHZAqzuSV20DCMuKUL5EZye1QAnYYV +H+3f82yxSi8+dAZvt8z7FVRPP5IUO8nfNmEmFNu9jCRszmqwYUdAkjVUIvmAoHWO +S4YVjAh3IXHhWheFjaSWXC3vb5O4GAf1xU6R2u65RZXvrvbndirT9d79j7vaGPBs +FS+Iryac2K5gezsckY2fGF5ahMhEcaAluATuE6DlRABM0j77Nz7pqPsAkzcuO1+W +Api4AXf0MXsm1Tt7KQKBwAQoo2SBHk+/qEb/rWKQ92HtPMbgpODYkgvyxnl2eUQX +5geqlhgaEG/ulUI7ee2IaA+jGPg3p/Fo1Z4radRkKKoWE4bMGPhDCjST96SwafAk +JSSPJFPMxJfNFxQgcmCa875aNgZ90EWq0okUolMl+1geZBRGitOqXb0nBbD96Ji3 +e6R5oBaU8UPBil/LmUkQdk5OQUD1Zj0SamHv8in3jTH2Ev271Ko2qCb+0YQiwvfv +z1NhRjAxguRD4kCPnsaX1w== +-----END PRIVATE KEY----- diff --git a/src/asymmetric_crypto/rsa/tests/openssl/rsa_3072_bit.pub.pkcs8.pem b/src/asymmetric_crypto/rsa/tests/openssl/rsa_3072_bit.pub.pkcs8.pem new file mode 100644 index 0000000..443ab0d --- /dev/null +++ b/src/asymmetric_crypto/rsa/tests/openssl/rsa_3072_bit.pub.pkcs8.pem @@ -0,0 +1,11 @@ +-----BEGIN PUBLIC KEY----- +MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA1VS0jIw+PyeWiQZ0UXyL +wnYWJCoao+GLYfER+u6J9GAPDbWRuvf7IcEA8agt/C5Tf1v5I9riEhdObBNmXnew +ErCyc/Is3PSI6PY8BoFrFj1ErUNBMnjfVLvwgEFui2/UoYtvO0NHoFFQRqPP9L71 ++SQIBIGyzNilu2Qms8BFa60SGZjd7I6i9U76fzdACErJ+DAELWU+p/Fz4cMBOKrj +7NOU5yVeE12kYGh4195HcnWtWPtyJagOBc7XXEGye0GbkPrUqSz6sMIAtdCNwxXC +Bwf+J/w1E5h14ezv+iwTzA7fVYN1CkvEojRpH5NHL3awfaou5b9+qTV0sowzLS/u +46/qVMJy8DnGTfcp1AdKVtYabKQATzwXOwjvCk9vrYuPkUKQ6mSMOZSPsHanScdm +8mAkfZw6jrb7vOOXykOlfCX+nr5V45AeSN3Vh9BFbKGaKtpoLqHfwUm4aOS4HFXN +pLUlAZGPHvOwIYtGxEBPrqMJ+tOhLBpQGUXtqFfxiybVAgMBAAE= +-----END PUBLIC KEY----- diff --git a/src/asymmetric_crypto/rsa/tests/pkcs8_tests.rs b/src/asymmetric_crypto/rsa/tests/pkcs8_tests.rs new file mode 100644 index 0000000..d8f6487 --- /dev/null +++ b/src/asymmetric_crypto/rsa/tests/pkcs8_tests.rs @@ -0,0 +1,82 @@ +use openssl::{ + pkey::{PKey, Public}, + rsa::Rsa, + symm::Cipher, +}; +use pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey}; + +use crate::{ + reexport::rand_core::SeedableRng, CryptoCoreError, CsRng, RsaPrivateKey, RsaPublicKey, +}; + +#[test] +fn test_pkcs8_openssl_key_compat() -> Result<(), CryptoCoreError> { + let private_key_rsa = Rsa::generate(3072)?; + + // convert to Openssl generic key + let private_key = PKey::from_rsa(private_key_rsa)?; + let private_key_pkcs8 = private_key.private_key_to_pkcs8()?; + let public_key_pkcs8 = private_key.public_key_to_der()?; + + // convert to RSA private Key + let rsa_public_key = RsaPublicKey::from_public_key_der(&public_key_pkcs8)?; + let rsa_private_key = RsaPrivateKey::from_pkcs8_der(&private_key_pkcs8)?; + assert_eq!(rsa_private_key.public_key(), rsa_public_key); + + // convert back to openssl + let openssl_private_key = + PKey::private_key_from_pkcs8(&rsa_private_key.to_pkcs8_der().map(|d| d.to_bytes())?)?; + // check same private key + assert_eq!( + openssl_private_key.private_key_to_der()?, + private_key.private_key_to_der()? + ); + let openssl_public_key = + Rsa::public_key_from_der(&rsa_public_key.to_public_key_der().map(|d| d.to_vec())?)?; + // check that the recovered public key matches the original one + assert_eq!( + private_key.public_key_to_pem()?, + openssl_public_key.public_key_to_pem()? + ); + + Ok(()) +} + +#[test] +fn test_encrypted_pkcs8_openssl_key_compat() -> Result<(), CryptoCoreError> { + let mut rng = CsRng::from_entropy(); + let private_key_rsa = Rsa::generate(3072)?; + let private_key_pkcs1 = private_key_rsa.private_key_to_pem()?; + + // convert to Openssl generic key + let private_key = PKey::from_rsa(private_key_rsa)?; + let private_key_pkcs8 = + private_key.private_key_to_pkcs8_passphrase(Cipher::aes_256_cbc(), b"blah")?; + let public_key_pkcs8 = private_key.public_key_to_der()?; + + // convert to Nist private Key + let rsa_public_key = RsaPublicKey::from_public_key_der(&public_key_pkcs8)?; + let rsa_private_key = RsaPrivateKey::from_pkcs8_encrypted_der(&private_key_pkcs8, b"blah")?; + assert_eq!(rsa_private_key.public_key(), rsa_public_key); + + // convert back to openssl + let private_key_pkcs8 = rsa_private_key.to_pkcs8_encrypted_der(&mut rng, b"blah")?; + { + // check we can decrypt with this library + let rsa_private_key_ = + RsaPrivateKey::from_pkcs8_encrypted_der(private_key_pkcs8.as_bytes(), b"blah")?; + assert_eq!(rsa_private_key_, rsa_private_key); + } + + let openssl_private_key = + PKey::private_key_from_pkcs8_passphrase(private_key_pkcs8.as_bytes(), b"blah")?; + let openssl_rsa_key = openssl_private_key.rsa()?; + // check same private key + assert_eq!(openssl_rsa_key.private_key_to_pem()?, private_key_pkcs1); + let openssl_public_key = + PKey::::public_key_from_der(rsa_public_key.to_public_key_der()?.as_bytes())?; + // check that the recovered public key matches the original one + assert!(private_key.public_eq(&openssl_public_key)); + + Ok(()) +} diff --git a/src/ecies/curve_25519/ecies_x25519_xchacha20.rs b/src/ecies/curve_25519/ecies_x25519_xchacha20.rs index ec77116..6f99a47 100644 --- a/src/ecies/curve_25519/ecies_x25519_xchacha20.rs +++ b/src/ecies/curve_25519/ecies_x25519_xchacha20.rs @@ -297,8 +297,6 @@ mod tests { Some(&b"Corrupted authenticated data"[..]), ); - println!("{not_decrypted:?}"); - assert!(matches!( not_decrypted, Err(CryptoCoreError::DecryptionError) diff --git a/src/error.rs b/src/error.rs index 88444f7..8c9bed9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -30,10 +30,12 @@ pub enum CryptoCoreError { }, #[cfg(any(feature = "certificate", feature = "nist_curves"))] Certificate(String), - #[cfg(any(feature = "certificate", feature = "nist_curves"))] + #[cfg(any(feature = "certificate", feature = "nist_curves", feature = "rsa"))] Pkcs8Error(String), #[cfg(feature = "ser")] ReadLeb128Error(leb128::read::Error), + #[cfg(feature = "rsa")] + RsaError(String), SerializationIoError { bytes_len: usize, error: std::io::Error, @@ -50,34 +52,9 @@ pub enum CryptoCoreError { impl Display for CryptoCoreError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::DeserializationEmptyError => { - write!(f, "empty input when parsing bytes") - } - Self::DeserializationSizeError { given, expected } => write!( - f, - "wrong size when parsing bytes: {given} given should be {expected}" - ), - #[cfg(feature = "ser")] - Self::ReadLeb128Error(err) => write!(f, "when reading LEB128, {err}"), - Self::GenericDeserializationError(err) => { - write!(f, "deserialization error: {err}") - } - Self::WriteLeb128Error { value, error } => { - write!(f, "when writing {value} as LEB128 size, IO error {error}") - } - Self::SerializationIoError { bytes_len, error } => { - write!(f, "when writing {bytes_len} bytes, {error}") - } - Self::PlaintextTooBigError { plaintext_len, max } => write!( - f, - "when encrypting, plaintext of {plaintext_len} bytes is too big, max is {max} \ - bytes" - ), - #[cfg(any(feature = "certificate", feature = "nist_curves"))] - Self::Pkcs8Error(err) => write!(f, "when converting to PKCS8, {err}"), #[cfg(any(feature = "certificate", feature = "nist_curves"))] - Self::Certificate(err) => write!(f, "when building certificate, {err}"), - Self::CiphertextTooSmallError { + CryptoCoreError::Certificate(err) => write!(f, "when building certificate, {err}"), + CryptoCoreError::CiphertextTooSmallError { ciphertext_len, min, } => write!( @@ -85,7 +62,7 @@ impl Display for CryptoCoreError { "when decrypting, ciphertext of {ciphertext_len} bytes is too small, min is {min} \ bytes" ), - Self::CiphertextTooBigError { + CryptoCoreError::CiphertextTooBigError { ciphertext_len, max, } => write!( @@ -93,10 +70,22 @@ impl Display for CryptoCoreError { "when decrypting, ciphertext of {ciphertext_len} bytes is too big, max is {max} \ bytes" ), - Self::ConversionError(err) => write!(f, "failed to convert: {err}"), - Self::EncryptionError => write!(f, "error during encryption"), - Self::DecryptionError => write!(f, "error during decryption"), - Self::InvalidBytesLength(message, given, expected) => match expected { + CryptoCoreError::ConversionError(err) => write!(f, "failed to convert: {err}"), + CryptoCoreError::DecryptionError => write!(f, "error during decryption"), + CryptoCoreError::DeserializationEmptyError => { + write!(f, "empty input when parsing bytes") + } + CryptoCoreError::DeserializationSizeError { given, expected } => write!( + f, + "wrong size when parsing bytes: {given} given should be {expected}" + ), + #[cfg(feature = "nist_curves")] + CryptoCoreError::EllipticCurveError(e) => write!(f, "NIST elliptic curve error: {e}"), + CryptoCoreError::EncryptionError => write!(f, "error during encryption"), + CryptoCoreError::GenericDeserializationError(err) => { + write!(f, "deserialization error: {err}") + } + CryptoCoreError::InvalidBytesLength(message, given, expected) => match expected { Some(expected_length) => write!( f, "{message}: invalid key length: got {given}, expected: {expected_length}", @@ -105,11 +94,26 @@ impl Display for CryptoCoreError { write!(f, "{message}: invalid key length: got {given}") } }, - Self::SignatureError(e) => write!(f, "error during signature: {e}"), - Self::StreamCipherError(e) => write!(f, "stream cipher error: {e}"), - Self::TryFromSliceError(e) => write!(f, "try from slice error: {e}"), - #[cfg(feature = "nist_curves")] - Self::EllipticCurveError(e) => write!(f, "NIST elliptic curve error: {e}"), + CryptoCoreError::PlaintextTooBigError { plaintext_len, max } => write!( + f, + "when encrypting, plaintext of {plaintext_len} bytes is too big, max is {max} \ + bytes" + ), + #[cfg(any(feature = "certificate", feature = "nist_curves", feature = "rsa"))] + CryptoCoreError::Pkcs8Error(err) => write!(f, "when converting to PKCS8, {err}"), + #[cfg(feature = "ser")] + CryptoCoreError::ReadLeb128Error(err) => write!(f, "when reading LEB128, {err}"), + #[cfg(feature = "rsa")] + CryptoCoreError::RsaError(e) => write!(f, "RSA error: {e}"), + CryptoCoreError::SerializationIoError { bytes_len, error } => { + write!(f, "when writing {bytes_len} bytes, {error}") + } + CryptoCoreError::SignatureError(e) => write!(f, "error during signature: {e}"), + CryptoCoreError::StreamCipherError(e) => write!(f, "stream cipher error: {e}"), + CryptoCoreError::TryFromSliceError(e) => write!(f, "try from slice error: {e}"), + CryptoCoreError::WriteLeb128Error { value, error } => { + write!(f, "when writing {value} as LEB128 size, IO error {error}") + } } } } @@ -169,3 +173,10 @@ impl From for CryptoCoreError { Self::Certificate(e.to_string()) } } + +#[cfg(feature = "rsa")] +impl From for CryptoCoreError { + fn from(e: rsa::errors::Error) -> Self { + CryptoCoreError::RsaError(e.to_string()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 854731c..3147872 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,8 @@ pub mod bytes_ser_de; mod ecies; #[cfg(feature = "sha3")] pub mod kdf; +#[cfg(any(feature = "certificate", feature = "rsa", feature = "nist_curves"))] +mod pkcs8_fix; #[cfg(any(feature = "aes", feature = "chacha", feature = "rfc5649"))] mod symmetric_crypto; diff --git a/src/pkcs8_fix.rs b/src/pkcs8_fix.rs new file mode 100644 index 0000000..9340840 --- /dev/null +++ b/src/pkcs8_fix.rs @@ -0,0 +1,38 @@ +use crate::reexport::rand_core::{CryptoRng, RngCore}; + +/// Create an [`SecretDocument`] containing the ciphertext of +/// a PKCS#8 encoded private key encrypted under the given `password`. +/// +/// Due to in compatibility issues with the openssl library, we use the +/// modified parameters for Scrypt and cannot use the default implemented with +/// ```Rust +/// let bytes = +/// pkcs8::EncodePrivateKey::to_pkcs8_encrypted_der(&self.secret_key, &mut rng, password) +/// .map(|d| d.to_bytes())?; +/// ``` +/// see this issue for more details and the PR progress that will fix it: +/// https://github.com/RustCrypto/formats/issues/1205 +pub(crate) fn to_pkcs8_encrypted_der( + secret_document: &pkcs8::SecretDocument, + mut rng: impl CryptoRng + RngCore, + password: impl AsRef<[u8]>, +) -> pkcs8::Result { + let mut salt = [0u8; 16]; + rng.fill_bytes(&mut salt); + + let mut iv = [0u8; 16]; + rng.fill_bytes(&mut iv); + + // 14 = log_2(16384), 32 bytes = 256 bits + let scrypt_params = pkcs8::pkcs5::scrypt::Params::new(14, 8, 1, 32).unwrap(); + let pbes2_params = + pkcs8::pkcs5::pbes2::Parameters::scrypt_aes256cbc(scrypt_params, &salt, &iv)?; + + let encrypted_data = pbes2_params.encrypt(password, secret_document.as_bytes())?; + + pkcs8::EncryptedPrivateKeyInfo { + encryption_algorithm: pbes2_params.into(), + encrypted_data: &encrypted_data, + } + .try_into() +} diff --git a/src/symmetric_crypto/key.rs b/src/symmetric_crypto/key.rs index 5a21c8b..dbc4cdd 100644 --- a/src/symmetric_crypto/key.rs +++ b/src/symmetric_crypto/key.rs @@ -3,7 +3,7 @@ use core::{hash::Hash, ops::Deref}; use std::ops::DerefMut; -use zeroize::{Zeroize, ZeroizeOnDrop}; +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use crate::{ reexport::rand_core::CryptoRngCore, CBytes, CryptoCoreError, FixedSizeCBytes, @@ -63,6 +63,12 @@ impl Default for SymmetricKey { } } +impl From> for Zeroizing> { + fn from(value: SymmetricKey) -> Self { + Zeroizing::new(value.0.to_vec()) + } +} + #[cfg(test)] mod tests {