Add support for ed25519 keys (#335)
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
|
||||
use crate::ctap::data_formats::{
|
||||
extract_array, extract_byte_string, CoseKey, PublicKeyCredentialSource,
|
||||
PublicKeyCredentialType, SignatureAlgorithm,
|
||||
PublicKeyCredentialType, SignatureAlgorithm, ES256_ALGORITHM, EDDSA_ALGORITHM,
|
||||
};
|
||||
use crate::ctap::status_code::Ctap2StatusCode;
|
||||
use crate::ctap::storage;
|
||||
@@ -30,6 +30,8 @@ use crypto::sha256::Sha256;
|
||||
use rng256::Rng256;
|
||||
use sk_cbor as cbor;
|
||||
use sk_cbor::{cbor_array, cbor_bytes, cbor_int};
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
use ed25519_dalek::Signer;
|
||||
|
||||
// Legacy credential IDs consist of
|
||||
// - 16 bytes: initialization vector for AES-256,
|
||||
@@ -42,6 +44,16 @@ pub const ECDSA_CREDENTIAL_ID_SIZE: usize = 113;
|
||||
// See encrypt_key_handle v1 documentation.
|
||||
pub const MAX_CREDENTIAL_ID_SIZE: usize = 113;
|
||||
|
||||
const ECDSA_CREDENTIAL_ID_VERSION: u8 = 0x01;
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
const ED25519_CREDENTIAL_ID_VERSION: u8 = 0x02;
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x03;
|
||||
#[cfg(test)]
|
||||
#[cfg(not(feature = "with_ed25519"))]
|
||||
const UNSUPPORTED_CREDENTIAL_ID_VERSION: u8 = 0x02;
|
||||
|
||||
/// Wraps the AES256-CBC encryption to match what we need in CTAP.
|
||||
pub fn aes256_cbc_encrypt(
|
||||
rng: &mut dyn Rng256,
|
||||
@@ -89,11 +101,37 @@ pub fn aes256_cbc_decrypt(
|
||||
}
|
||||
|
||||
/// An asymmetric private key that can sign messages.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
pub enum PrivateKey {
|
||||
Ecdsa(ecdsa::SecKey),
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
Ed25519(ed25519_dalek::Keypair),
|
||||
}
|
||||
|
||||
impl Clone for PrivateKey {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Ecdsa(sk) => Self::Ecdsa (sk.clone ()),
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
Self::Ed25519(keypair) => Self::Ed25519 (ed25519_dalek::Keypair::from_bytes (&keypair.to_bytes()).unwrap()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PrivateKey {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(&Self::Ecdsa(ref a), &Self::Ecdsa(ref b)) => a == b,
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
(&Self::Ed25519(ref a), &Self::Ed25519(ref b)) => a.to_bytes() == b.to_bytes(),
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PrivateKey {}
|
||||
|
||||
impl PrivateKey {
|
||||
/// Creates a new private key for the given algorithm.
|
||||
///
|
||||
@@ -103,6 +141,11 @@ impl PrivateKey {
|
||||
pub fn new(rng: &mut impl Rng256, alg: SignatureAlgorithm) -> Self {
|
||||
match alg {
|
||||
SignatureAlgorithm::ES256 => PrivateKey::Ecdsa(crypto::ecdsa::SecKey::gensk(rng)),
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
SignatureAlgorithm::EDDSA => {
|
||||
let bytes = rng.gen_uniform_u8x32();
|
||||
Self::new_ed25519_from_bytes(&bytes).unwrap()
|
||||
},
|
||||
SignatureAlgorithm::Unknown => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -117,10 +160,25 @@ impl PrivateKey {
|
||||
ecdsa::SecKey::from_bytes(array_ref!(bytes, 0, 32)).map(PrivateKey::from)
|
||||
}
|
||||
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
pub fn new_ed25519_from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
if bytes.len() != 32 {
|
||||
return None;
|
||||
}
|
||||
if let Ok(secret) = ed25519_dalek::SecretKey::from_bytes(bytes) {
|
||||
let public = ed25519_dalek::PublicKey::from (&secret);
|
||||
Some(Self::Ed25519(ed25519_dalek::Keypair{secret, public}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the corresponding public key.
|
||||
pub fn get_pub_key(&self) -> CoseKey {
|
||||
match self {
|
||||
PrivateKey::Ecdsa(ecdsa_key) => CoseKey::from(ecdsa_key.genpk()),
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
PrivateKey::Ed25519(ed25519_keypair) => CoseKey::from(ed25519_keypair.public),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +186,8 @@ impl PrivateKey {
|
||||
pub fn sign_and_encode(&self, message: &[u8]) -> Vec<u8> {
|
||||
match self {
|
||||
PrivateKey::Ecdsa(ecdsa_key) => ecdsa_key.sign_rfc6979::<Sha256>(message).to_asn1_der(),
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
PrivateKey::Ed25519(ed25519_keypair) => ed25519_keypair.try_sign(message).unwrap().to_bytes().to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +195,8 @@ impl PrivateKey {
|
||||
pub fn signature_algorithm(&self) -> SignatureAlgorithm {
|
||||
match self {
|
||||
PrivateKey::Ecdsa(_) => SignatureAlgorithm::ES256,
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
PrivateKey::Ed25519(_) => SignatureAlgorithm::EDDSA,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +208,8 @@ impl PrivateKey {
|
||||
ecdsa_key.to_bytes(array_mut_ref!(key_bytes, 0, 32));
|
||||
key_bytes
|
||||
}
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
PrivateKey::Ed25519(ed25519_keypair) => ed25519_keypair.secret.to_bytes().to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,6 +235,9 @@ impl TryFrom<cbor::Value> for PrivateKey {
|
||||
match SignatureAlgorithm::try_from(array.pop().unwrap())? {
|
||||
SignatureAlgorithm::ES256 => PrivateKey::new_ecdsa_from_bytes(&key_bytes)
|
||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
SignatureAlgorithm::EDDSA => PrivateKey::new_ed25519_from_bytes(&key_bytes)
|
||||
.ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
|
||||
_ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
|
||||
}
|
||||
}
|
||||
@@ -190,12 +257,19 @@ impl From<ecdsa::SecKey> for PrivateKey {
|
||||
/// Also, by limiting ourselves to private key and RP ID hash, we are compatible with U2F for
|
||||
/// ECDSA private keys.
|
||||
///
|
||||
/// This is v1, and we write the following data for ECDSA (algorithm -7):
|
||||
/// For v1 we write the following data for ECDSA (algorithm -7):
|
||||
/// - 1 byte : version number
|
||||
/// - 16 bytes: initialization vector for AES-256,
|
||||
/// - 32 bytes: ECDSA private key for the credential,
|
||||
/// - 32 bytes: relying party ID hashed with SHA256,
|
||||
/// - 32 bytes: HMAC-SHA256 over everything else.
|
||||
///
|
||||
/// For v2 we write the following data for EdDSA over curve Ed25519 (algorithm -8, curve 6):
|
||||
/// - 1 byte : version number
|
||||
/// - 16 bytes: initialization vector for AES-256,
|
||||
/// - 32 bytes: Ed25519 private key for the credential,
|
||||
/// - 32 bytes: relying party ID hashed with SHA256,
|
||||
/// - 32 bytes: HMAC-SHA256 over everything else.
|
||||
pub fn encrypt_key_handle(
|
||||
env: &mut impl Env,
|
||||
private_key: &PrivateKey,
|
||||
@@ -204,17 +278,22 @@ pub fn encrypt_key_handle(
|
||||
let master_keys = storage::master_keys(env)?;
|
||||
let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption);
|
||||
|
||||
let mut encrypted_id = match private_key {
|
||||
let mut plaintext = [0; 64];
|
||||
let version;
|
||||
match private_key {
|
||||
PrivateKey::Ecdsa(ecdsa_key) => {
|
||||
let mut plaintext = [0; 64];
|
||||
ecdsa_key.to_bytes(array_mut_ref!(plaintext, 0, 32));
|
||||
plaintext[32..64].copy_from_slice(application);
|
||||
let mut encrypted_id = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true)?;
|
||||
// Version number
|
||||
encrypted_id.insert(0, 0x01);
|
||||
encrypted_id
|
||||
version = ECDSA_CREDENTIAL_ID_VERSION;
|
||||
}
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
PrivateKey::Ed25519(keypair) => {
|
||||
plaintext[0..32].copy_from_slice(&keypair.secret.to_bytes());
|
||||
version = ED25519_CREDENTIAL_ID_VERSION;
|
||||
}
|
||||
};
|
||||
plaintext[32..64].copy_from_slice(application);
|
||||
let mut encrypted_id = aes256_cbc_encrypt(env.rng(), &aes_enc_key, &plaintext, true)?;
|
||||
encrypted_id.insert(0, version);
|
||||
|
||||
let id_hmac = hmac_256::<Sha256>(&master_keys.hmac, &encrypted_id[..]);
|
||||
encrypted_id.extend(&id_hmac);
|
||||
@@ -231,6 +310,7 @@ pub fn encrypt_key_handle(
|
||||
/// This functions reads:
|
||||
/// - legacy credentials (no version number),
|
||||
/// - v1 (ECDSA)
|
||||
/// - v2 (EdDSA over curve Ed25519)
|
||||
pub fn decrypt_credential_source(
|
||||
env: &mut impl Env,
|
||||
credential_id: Vec<u8>,
|
||||
@@ -249,12 +329,17 @@ pub fn decrypt_credential_source(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let algorithm;
|
||||
let payload = if credential_id.len() == LEGACY_CREDENTIAL_ID_SIZE {
|
||||
algorithm = ES256_ALGORITHM;
|
||||
&credential_id[..hmac_message_size]
|
||||
} else {
|
||||
// Version number check
|
||||
if credential_id[0] != 1 {
|
||||
return Ok(None);
|
||||
match credential_id[0] {
|
||||
ECDSA_CREDENTIAL_ID_VERSION => algorithm = ES256_ALGORITHM,
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
ED25519_CREDENTIAL_ID_VERSION => algorithm = EDDSA_ALGORITHM,
|
||||
_ => return Ok(None),
|
||||
}
|
||||
&credential_id[1..hmac_message_size]
|
||||
};
|
||||
@@ -269,7 +354,15 @@ pub fn decrypt_credential_source(
|
||||
if rp_id_hash != &decrypted_id[32..] {
|
||||
return Ok(None);
|
||||
}
|
||||
let sk_option = PrivateKey::new_ecdsa_from_bytes(&decrypted_id[..32]);
|
||||
let sk_option;
|
||||
match algorithm {
|
||||
ES256_ALGORITHM => sk_option = PrivateKey::new_ecdsa_from_bytes(&decrypted_id[..32]),
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
EDDSA_ALGORITHM => sk_option = PrivateKey::new_ed25519_from_bytes(&decrypted_id[..32]),
|
||||
#[cfg(not(feature = "with_ed25519"))]
|
||||
EDDSA_ALGORITHM => return Ok(None),
|
||||
_ => return Ok(None),
|
||||
}
|
||||
|
||||
Ok(sk_option.map(|sk| PublicKeyCredentialSource {
|
||||
key_type: PublicKeyCredentialType::PublicKey,
|
||||
@@ -368,6 +461,18 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
fn test_new_ed25519_from_bytes() {
|
||||
let mut env = TestEnv::new();
|
||||
let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::EDDSA);
|
||||
let key_bytes = private_key.to_bytes();
|
||||
assert_eq!(
|
||||
PrivateKey::new_ed25519_from_bytes(&key_bytes),
|
||||
Some(private_key)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_new_ecdsa_from_bytes_wrong_length() {
|
||||
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 16]), None);
|
||||
@@ -376,6 +481,15 @@ mod test {
|
||||
assert_eq!(PrivateKey::new_ecdsa_from_bytes(&[0x55; 64]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
fn test_new_ed25519_from_bytes_wrong_length() {
|
||||
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 16]), None);
|
||||
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 31]), None);
|
||||
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 33]), None);
|
||||
assert_eq!(PrivateKey::new_ed25519_from_bytes(&[0x55; 64]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_private_key_get_pub_key() {
|
||||
let mut env = TestEnv::new();
|
||||
@@ -395,22 +509,41 @@ mod test {
|
||||
assert_eq!(private_key.sign_and_encode(&message), signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_private_key_signature_algorithm() {
|
||||
fn test_private_key_signature_algorithm(signature_algorithm: SignatureAlgorithm) {
|
||||
let mut env = TestEnv::new();
|
||||
let algorithm = SignatureAlgorithm::ES256;
|
||||
let private_key = PrivateKey::new(env.rng(), algorithm);
|
||||
assert_eq!(private_key.signature_algorithm(), algorithm);
|
||||
let private_key = PrivateKey::new(env.rng(), signature_algorithm);
|
||||
assert_eq!(private_key.signature_algorithm(), signature_algorithm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_private_key_from_to_cbor() {
|
||||
fn test_ecdsa_private_key_signature_algorithm() {
|
||||
test_private_key_signature_algorithm(SignatureAlgorithm::ES256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
fn test_ed25519_private_key_signature_algorithm() {
|
||||
test_private_key_signature_algorithm(SignatureAlgorithm::EDDSA);
|
||||
}
|
||||
|
||||
fn test_private_key_from_to_cbor(signature_algorithm: SignatureAlgorithm) {
|
||||
let mut env = TestEnv::new();
|
||||
let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::ES256);
|
||||
let private_key = PrivateKey::new(env.rng(), signature_algorithm);
|
||||
let cbor = cbor::Value::from(private_key.clone());
|
||||
assert_eq!(PrivateKey::try_from(cbor), Ok(private_key),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ecdsa_private_key_from_to_cbor() {
|
||||
test_private_key_from_to_cbor(SignatureAlgorithm::ES256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
fn test_ed25519_private_key_from_to_cbor() {
|
||||
test_private_key_from_to_cbor(SignatureAlgorithm::EDDSA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_private_key_from_bad_cbor() {
|
||||
let cbor = cbor_array![
|
||||
@@ -424,6 +557,20 @@ mod test {
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
|
||||
);
|
||||
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
{
|
||||
let cbor = cbor_array![
|
||||
cbor_int!(SignatureAlgorithm::EDDSA as i64),
|
||||
cbor_bytes!(vec![0x88; 32]),
|
||||
// The array is too long.
|
||||
cbor_int!(0),
|
||||
];
|
||||
assert_eq!(
|
||||
PrivateKey::try_from(cbor),
|
||||
Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR),
|
||||
);
|
||||
}
|
||||
|
||||
let cbor = cbor_array![
|
||||
// This algorithms doesn't exist.
|
||||
cbor_int!(-1),
|
||||
@@ -435,11 +582,10 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_credential() {
|
||||
fn test_encrypt_decrypt_credential(signature_algorithm: SignatureAlgorithm) {
|
||||
let mut env = TestEnv::new();
|
||||
storage::init(&mut env).ok().unwrap();
|
||||
let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::ES256);
|
||||
let private_key = PrivateKey::new(env.rng(), signature_algorithm);
|
||||
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap();
|
||||
@@ -450,6 +596,17 @@ mod test {
|
||||
assert_eq!(private_key, decrypted_source.private_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_ecdsa_credential() {
|
||||
test_encrypt_decrypt_credential(SignatureAlgorithm::ES256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
fn test_encrypt_decrypt_ed25519_credential() {
|
||||
test_encrypt_decrypt_credential(SignatureAlgorithm::EDDSA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_bad_version() {
|
||||
let mut env = TestEnv::new();
|
||||
@@ -459,7 +616,7 @@ mod test {
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let mut encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap();
|
||||
// Version 2 does not exist yet.
|
||||
encrypted_id[0] = 0x02;
|
||||
encrypted_id[0] = UNSUPPORTED_CREDENTIAL_ID_VERSION;
|
||||
// Override the HMAC to pass the check.
|
||||
encrypted_id.truncate(&encrypted_id.len() - 32);
|
||||
let master_keys = storage::master_keys(&mut env).unwrap();
|
||||
@@ -472,11 +629,10 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_bad_hmac() {
|
||||
fn test_encrypt_decrypt_bad_hmac(signature_algorithm: SignatureAlgorithm) {
|
||||
let mut env = TestEnv::new();
|
||||
storage::init(&mut env).ok().unwrap();
|
||||
let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::ES256);
|
||||
let private_key = PrivateKey::new(env.rng(), signature_algorithm);
|
||||
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap();
|
||||
@@ -491,10 +647,20 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_credential_missing_blocks() {
|
||||
fn test_ecdsa_encrypt_decrypt_bad_hmac() {
|
||||
test_encrypt_decrypt_bad_hmac(SignatureAlgorithm::ES256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
fn test_ed25519_encrypt_decrypt_bad_hmac() {
|
||||
test_encrypt_decrypt_bad_hmac(SignatureAlgorithm::EDDSA);
|
||||
}
|
||||
|
||||
fn test_decrypt_credential_missing_blocks(signature_algorithm: SignatureAlgorithm) {
|
||||
let mut env = TestEnv::new();
|
||||
storage::init(&mut env).ok().unwrap();
|
||||
let private_key = PrivateKey::new(env.rng(), SignatureAlgorithm::ES256);
|
||||
let private_key = PrivateKey::new(env.rng(), signature_algorithm);
|
||||
|
||||
let rp_id_hash = [0x55; 32];
|
||||
let encrypted_id = encrypt_key_handle(&mut env, &private_key, &rp_id_hash).unwrap();
|
||||
@@ -507,6 +673,17 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ecdsa_decrypt_credential_missing_blocks() {
|
||||
test_decrypt_credential_missing_blocks(SignatureAlgorithm::ES256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "with_ed25519")]
|
||||
fn test_ed25519_decrypt_credential_missing_blocks() {
|
||||
test_decrypt_credential_missing_blocks(SignatureAlgorithm::EDDSA);
|
||||
}
|
||||
|
||||
/// This is a copy of the function that genereated deprecated key handles.
|
||||
fn legacy_encrypt_key_handle(
|
||||
env: &mut impl Env,
|
||||
|
||||
Reference in New Issue
Block a user