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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/crypto/dummy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This is a dummy implementation for CI, to avoid having to install NSS or openSSL

pub type Result<T> = std::result::Result<T, CryptoError>;

pub fn ecdhe_p256_raw(_peer_spki: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
pub fn ecdhe_p256_raw(_peer: &super::COSEEC2Key) -> Result<(Vec<u8>, Vec<u8>)> {
unimplemented!()
}

Expand Down
13 changes: 8 additions & 5 deletions src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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(),
Expand Down
14 changes: 8 additions & 6 deletions src/crypto/nss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,15 @@ pub fn ecdsa_p256_sha256_sign_raw(private: &[u8], data: &[u8]) -> Result<Vec<u8>
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<u8>, Vec<u8>)> {
pub fn ecdhe_p256_raw(peer: &super::COSEEC2Key) -> Result<(Vec<u8>, Vec<u8>)> {
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()?;

Expand Down Expand Up @@ -374,14 +375,15 @@ pub fn random_bytes(count: usize) -> Result<Vec<u8>> {

#[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<Vec<u8>> {
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:
Expand Down
13 changes: 7 additions & 6 deletions src/crypto/openssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ fn ecdh_openssl_raw(client_private: EcKey<Private>, peer_public: EcKey<Public>)
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<u8>, Vec<u8>)> {
let peer_public = EcKey::public_key_from_der(peer_spki)?;
pub fn ecdhe_p256_raw(peer: &super::COSEEC2Key) -> Result<(Vec<u8>, Vec<u8>)> {
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.
Expand Down Expand Up @@ -140,12 +140,13 @@ pub fn random_bytes(count: usize) -> Result<Vec<u8>> {

#[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<Vec<u8>> {
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![];
Expand Down
176 changes: 176 additions & 0 deletions src/crypto/rustcrypto.rs
Original file line number Diff line number Diff line change
@@ -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<T> = std::result::Result<T, CryptoError>;

fn cose_key_to_public(peer: &super::COSEEC2Key) -> Result<p256::PublicKey> {
// 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<u8>, Vec<u8>)> {
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<aes::Aes256>;
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;

const AES_BLOCK_SIZE: usize = 16;

pub fn encrypt_aes_256_cbc_no_pad(key: &[u8], iv: Option<&[u8]>, data: &[u8]) -> Result<Vec<u8>> {
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<Vec<u8>> {
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<sha2::Sha256>;

pub fn hmac_sha256(key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
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<Vec<u8>> {
let digest = sha2::Sha256::digest(data);
Ok(digest.to_vec())
}

pub fn random_bytes(count: usize) -> Result<Vec<u8>> {
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<Vec<u8>> {
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<u8>, Vec<u8>)> {
unimplemented!()
}

pub fn ecdsa_p256_sha256_sign_raw(_private: &[u8], _data: &[u8]) -> Result<Vec<u8>> {
unimplemented!()
}

#[allow(dead_code)]
#[cfg(test)]
pub fn test_ecdsa_p256_sha256_verify_raw(
_public: &[u8],
_signature: &[u8],
_data: &[u8],
) -> Result<()> {
unimplemented!()
}