diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 393cb3c9..b00b6c34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,19 @@ jobs: sudo apt-get install -y libudev-dev openssl BUILD_OPTIONS: --features crypto_openssl --no-default-features + # Linux RustCrypto + - OS: ubuntu-latest + TARGET: x86_64-unknown-linux-gnu + NATIVE_BUILD: true + # XXX: This version downgrade is required because 1.8.x has too high of an MSRV + # and the lockfile isn't checked in for tests to use, so the latest dependency version + # is always used instead. + ADD_INSTALL: | + sudo apt-get update + sudo apt-get install -y libudev-dev + cargo update -p base64ct --precise 1.6.0 + BUILD_OPTIONS: --features crypto_rust --no-default-features + # Mac dummy crypto - OS: macos-latest TARGET: x86_64-apple-darwin diff --git a/Cargo.toml b/Cargo.toml index 38da8a43..936cc5b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ binding-recompile = ["bindgen"] crypto_dummy = [] crypto_openssl = ["openssl", "openssl-sys"] crypto_nss = ["nss-gk-api", "pkcs11-bindings"] +crypto_rust = ["aes", "cbc", "rand_core", "p256", "hmac"] gecko = ["nss-gk-api/gecko"] [target.'cfg(target_os = "linux")'.dependencies] @@ -69,6 +70,11 @@ openssl-sys = { version = "0.9", optional = true} openssl = { version = "0.10", optional = true} nss-gk-api = { version = "0.3.0", optional = true } pkcs11-bindings = { version = "0.1.4", optional = true } +aes = { version = "0.8", optional = true } +cbc = { version = "0.1", default-features = false, features = ["std"], optional = true } +rand_core = { version = "0.6", features = ["getrandom"], optional = true } +hmac = { version = "0.12", optional = true } +p256 = { version = "0.13", default-features = false, features = ["arithmetic", "ecdsa", "ecdh", "std"], optional = true } [dev-dependencies] env_logger = "^0.6" diff --git a/src/crypto/dummy.rs b/src/crypto/dummy.rs index 9d168569..0622d267 100644 --- a/src/crypto/dummy.rs +++ b/src/crypto/dummy.rs @@ -6,7 +6,7 @@ This is a dummy implementation for CI, to avoid having to install NSS or openSSL pub type Result = std::result::Result; -pub fn ecdhe_p256_raw(_peer_spki: &[u8]) -> Result<(Vec, Vec)> { +pub fn ecdhe_p256_raw(_peer: &super::COSEEC2Key) -> Result<(Vec, Vec)> { unimplemented!() } diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 1ad99742..91eb70c1 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -24,6 +24,11 @@ mod openssl; #[cfg(feature = "crypto_openssl")] use self::openssl as backend; +#[cfg(feature = "crypto_rust")] +mod rustcrypto; +#[cfg(feature = "crypto_rust")] +use rustcrypto as backend; + #[cfg(feature = "crypto_dummy")] mod dummy; #[cfg(feature = "crypto_dummy")] @@ -107,9 +112,7 @@ trait PinProtocolImpl: ClonablePinProtocolImpl { _ => return Err(CryptoError::UnsupportedKeyType), }; - let peer_spki = peer_cose_ec2_key.der_spki()?; - - let (shared_point, client_public_sec1) = ecdhe_p256_raw(&peer_spki)?; + let (shared_point, client_public_sec1) = ecdhe_p256_raw(peer_cose_ec2_key)?; let client_cose_ec2_key = COSEEC2Key::from_sec1_uncompressed(Curve::SECP256R1, &client_public_sec1)?; @@ -1452,8 +1455,8 @@ mod test { // We are using `test_cose_ec2_p256_ecdh_sha256()` here, because we need a way to hand in // the private key which would be generated on the fly otherwise (ephemeral keys), // to predict the outputs - let peer_spki = peer_ec2_key.der_spki().unwrap(); - let shared_point = test_ecdh_p256_raw(&peer_spki, &EC_PUB_X, &EC_PUB_Y, &EC_PRIV).unwrap(); + let shared_point = + test_ecdh_p256_raw(&peer_ec2_key, &EC_PUB_X, &EC_PUB_Y, &EC_PRIV).unwrap(); let shared_secret = SharedSecret { pin_protocol: PinUvAuthProtocol(Box::new(PinUvAuth1 {})), key: sha256(&shared_point).unwrap(), diff --git a/src/crypto/nss.rs b/src/crypto/nss.rs index 56c23db5..7f540601 100644 --- a/src/crypto/nss.rs +++ b/src/crypto/nss.rs @@ -158,14 +158,15 @@ pub fn ecdsa_p256_sha256_sign_raw(private: &[u8], data: &[u8]) -> Result der::sequence(&[&der::integer(r)?, &der::integer(s)?]) } -/// Ephemeral ECDH over P256. Takes a DER SubjectPublicKeyInfo that encodes a public key. Generates -/// an ephemeral P256 key pair. Returns +/// Ephemeral ECDH over P256. Generates an ephemeral P256 key pair. Returns /// 1) the x coordinate of the shared point, and /// 2) the uncompressed SEC 1 encoding of the ephemeral public key. -pub fn ecdhe_p256_raw(peer_spki: &[u8]) -> Result<(Vec, Vec)> { +pub fn ecdhe_p256_raw(peer: &super::COSEEC2Key) -> Result<(Vec, Vec)> { + let peer_spki = peer.der_spki()?; + nss_gk_api::init(); - let peer_public = nss_public_key_from_der_spki(peer_spki)?; + let peer_public = nss_public_key_from_der_spki(&peer_spki)?; let (client_private, client_public) = generate_p256_nss()?; @@ -374,14 +375,15 @@ pub fn random_bytes(count: usize) -> Result> { #[cfg(test)] pub fn test_ecdh_p256_raw( - peer_spki: &[u8], + peer: &super::COSEEC2Key, client_public_x: &[u8], client_public_y: &[u8], client_private: &[u8], ) -> Result> { nss_gk_api::init(); - let peer_public = nss_public_key_from_der_spki(peer_spki)?; + let peer_spki = peer.der_spki()?; + let peer_public = nss_public_key_from_der_spki(&peer_spki)?; // NSS has no mechanism to import a raw elliptic curve coordinate as a private key. // We need to encode it in an RFC 5208 PrivateKeyInfo: diff --git a/src/crypto/openssl.rs b/src/crypto/openssl.rs index 065ea201..a440ad9d 100644 --- a/src/crypto/openssl.rs +++ b/src/crypto/openssl.rs @@ -44,12 +44,12 @@ fn ecdh_openssl_raw(client_private: EcKey, peer_public: EcKey) Ok(shared_point) } -/// Ephemeral ECDH over P256. Takes a DER SubjectPublicKeyInfo that encodes a public key. Generates -/// an ephemeral P256 key pair. Returns +/// Ephemeral ECDH over P256. Generates an ephemeral P256 key pair. Returns /// 1) the x coordinate of the shared point, and /// 2) the uncompressed SEC 1 encoding of the ephemeral public key. -pub fn ecdhe_p256_raw(peer_spki: &[u8]) -> Result<(Vec, Vec)> { - let peer_public = EcKey::public_key_from_der(peer_spki)?; +pub fn ecdhe_p256_raw(peer: &super::COSEEC2Key) -> Result<(Vec, Vec)> { + let peer_spki = peer.der_spki()?; + let peer_public = EcKey::public_key_from_der(&peer_spki)?; // Hard-coding the P256 group here is easier than extracting a group name from peer_public and // comparing it with P256. We'll fail in key derivation if peer_public is on the wrong curve. @@ -140,12 +140,13 @@ pub fn random_bytes(count: usize) -> Result> { #[cfg(test)] pub fn test_ecdh_p256_raw( - peer_spki: &[u8], + peer: &super::COSEEC2Key, client_public_x: &[u8], client_public_y: &[u8], client_private: &[u8], ) -> Result> { - let peer_public = EcKey::public_key_from_der(peer_spki)?; + let peer_spki = peer.der_spki()?; + let peer_public = EcKey::public_key_from_der(&peer_spki)?; let group = peer_public.group(); let mut client_pub_sec1 = vec![]; diff --git a/src/crypto/rustcrypto.rs b/src/crypto/rustcrypto.rs new file mode 100644 index 00000000..2145eb1f --- /dev/null +++ b/src/crypto/rustcrypto.rs @@ -0,0 +1,176 @@ +use super::CryptoError; +use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; +use hmac::Mac; +use p256::elliptic_curve::sec1::FromEncodedPoint; +use rand_core::RngCore; +use sha2::Digest; +use std::convert::TryInto; + +pub type Result = std::result::Result; + +fn cose_key_to_public(peer: &super::COSEEC2Key) -> Result { + // SEC 1 encoded uncompressed point + let peer = p256::EncodedPoint::from_affine_coordinates( + peer.x + .as_slice() + .try_into() + .map_err(|_| CryptoError::MalformedInput)?, + peer.y + .as_slice() + .try_into() + .map_err(|_| CryptoError::MalformedInput)?, + false, + ); + p256::PublicKey::from_encoded_point(&peer) + .into_option() + .ok_or(CryptoError::LibraryFailure) +} + +/// Ephemeral ECDH over P256. Generates an ephemeral P256 key pair. Returns +/// 1) the x coordinate of the shared point, and +/// 2) the uncompressed SEC 1 encoding of the ephemeral public key. +pub fn ecdhe_p256_raw(peer: &super::COSEEC2Key) -> Result<(Vec, Vec)> { + let peer_public = cose_key_to_public(peer)?; + + let internal_private = p256::ecdh::EphemeralSecret::random(&mut rand_core::OsRng); + let internal_public = internal_private.public_key().to_sec1_bytes().into_vec(); + + let shared_point = internal_private.diffie_hellman(&peer_public); + + Ok((shared_point.raw_secret_bytes().to_vec(), internal_public)) +} + +type Aes256CbcEnc = cbc::Encryptor; +type Aes256CbcDec = cbc::Decryptor; + +const AES_BLOCK_SIZE: usize = 16; + +pub fn encrypt_aes_256_cbc_no_pad(key: &[u8], iv: Option<&[u8]>, data: &[u8]) -> Result> { + let key: [u8; 32] = match key.try_into() { + Ok(key) => key, + Err(_) => return Err(CryptoError::LibraryFailure), + }; + + let iv = iv.unwrap_or(&[0u8; AES_BLOCK_SIZE]); + let iv = match iv.try_into() { + Ok(iv) => iv, + Err(_) => return Err(CryptoError::LibraryFailure), + }; + + // Validate that the data is an exact multiple of the block size since we have no + // padding available. + let blocks = data.chunks_exact(AES_BLOCK_SIZE); + if !blocks.remainder().is_empty() { + return Err(CryptoError::LibraryFailure); + } + + let mut encryptor = Aes256CbcEnc::new(&key.into(), iv); + + // Since we now know that `data` is a multiple of `AES_BLOCK_SIZE`, so this will always have the + // same number of blocks as it. + let mut ciphertext = vec![0u8; data.len()]; + // XXX: `slice::as_chunks` would be better but it requires an MSRV of 1.88. + for (input_block, output_block) in blocks + .into_iter() + .zip(ciphertext.chunks_exact_mut(AES_BLOCK_SIZE)) + { + let input: &[u8; AES_BLOCK_SIZE] = input_block.try_into().unwrap(); + let output: &mut [u8; AES_BLOCK_SIZE] = output_block.try_into().unwrap(); + + encryptor.encrypt_block_b2b_mut(input.into(), output.into()); + debug_assert_ne!(output, &[0u8; AES_BLOCK_SIZE]); + } + + Ok(ciphertext) +} + +pub fn decrypt_aes_256_cbc_no_pad(key: &[u8], iv: Option<&[u8]>, data: &[u8]) -> Result> { + let key: [u8; 32] = match key.try_into() { + Ok(key) => key, + Err(_) => return Err(CryptoError::LibraryFailure), + }; + + let iv = iv.unwrap_or(&[0u8; AES_BLOCK_SIZE]); + let iv = match iv.try_into() { + Ok(iv) => iv, + Err(_) => return Err(CryptoError::LibraryFailure), + }; + + // See comments in `encrypt_aes_256_cbc_no_pad` for rationale. + let blocks = data.chunks_exact(AES_BLOCK_SIZE); + if !blocks.remainder().is_empty() { + return Err(CryptoError::LibraryFailure); + } + + let mut decryptor = Aes256CbcDec::new(&key.into(), iv); + let mut plaintext = vec![0u8; data.len()]; + for (input_block, output_block) in blocks + .into_iter() + .zip(plaintext.chunks_exact_mut(AES_BLOCK_SIZE)) + { + let input: &[u8; AES_BLOCK_SIZE] = input_block.try_into().unwrap(); + let output: &mut [u8; AES_BLOCK_SIZE] = output_block.try_into().unwrap(); + + decryptor.decrypt_block_b2b_mut(input.into(), output.into()); + debug_assert_ne!(output, &[0u8; AES_BLOCK_SIZE]); + } + + Ok(plaintext) +} + +type HmacSha256 = hmac::Hmac; + +pub fn hmac_sha256(key: &[u8], data: &[u8]) -> Result> { + let mut key = HmacSha256::new_from_slice(key) + .map_err(|_| CryptoError::Backend(String::from("InvalidLength")))?; + + key.update(data); + Ok(key.finalize().into_bytes().to_vec()) +} + +pub fn sha256(data: &[u8]) -> Result> { + let digest = sha2::Sha256::digest(data); + Ok(digest.to_vec()) +} + +pub fn random_bytes(count: usize) -> Result> { + let mut rng = rand_core::OsRng; + let mut out = vec![0u8; count]; + rng.try_fill_bytes(out.as_mut_slice()) + .map_err(|_| CryptoError::LibraryFailure)?; + Ok(out) +} + +#[cfg(test)] +pub fn test_ecdh_p256_raw( + peer: &super::COSEEC2Key, + _client_public_x: &[u8], + _client_public_y: &[u8], + client_private: &[u8], +) -> Result> { + let peer_public = cose_key_to_public(peer)?; + + let client_private = p256::SecretKey::from_slice(client_private).unwrap(); + let shared_point = + p256::ecdh::diffie_hellman(client_private.to_nonzero_scalar(), peer_public.as_affine()); + + Ok(shared_point.raw_secret_bytes().to_vec()) +} + +pub fn gen_p256() -> Result<(Vec, Vec)> { + unimplemented!() +} + +pub fn ecdsa_p256_sha256_sign_raw(_private: &[u8], _data: &[u8]) -> Result> { + unimplemented!() +} + +#[allow(dead_code)] +#[cfg(test)] +pub fn test_ecdsa_p256_sha256_verify_raw( + _public: &[u8], + _signature: &[u8], + _data: &[u8], +) -> Result<()> { + unimplemented!() +}