blob: b127a6dab554c960096b25c780e6d403f89517b4 [file] [log] [blame] [edit]
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
//! Defines the Crypto trait and related types.
use crate::byte_array_wrapper;
use crate::constants::*;
use crate::error::DpeResult;
use crate::memory::{Message, SizedMessage};
use zeroize::ZeroizeOnDrop;
byte_array_wrapper!(MacKey, HASH_SIZE, "MAC key");
byte_array_wrapper!(EncryptionKey, ENCRYPTION_KEY_SIZE, "encryption key");
byte_array_wrapper!(DhPublicKey, DH_PUBLIC_KEY_SIZE, "DH public key");
byte_array_wrapper!(DhPrivateKey, DH_PRIVATE_KEY_SIZE, "DH private key");
byte_array_wrapper!(Hash, HASH_SIZE, "hash");
byte_array_wrapper!(
SigningPublicKey,
SIGNING_PUBLIC_KEY_SIZE,
"signing public key"
);
byte_array_wrapper!(
SigningPrivateKey,
SIGNING_PRIVATE_KEY_SIZE,
"signing private key"
);
byte_array_wrapper!(
SealingPublicKey,
SEALING_PUBLIC_KEY_SIZE,
"sealing public key"
);
byte_array_wrapper!(
SealingPrivateKey,
SEALING_PRIVATE_KEY_SIZE,
"sealing private key"
);
/// A session handshake message.
pub(crate) type HandshakeMessage = SizedMessage<MAX_HANDSHAKE_MESSAGE_SIZE>;
/// A session handshake payload.
pub(crate) type HandshakePayload = SizedMessage<MAX_HANDSHAKE_PAYLOAD_SIZE>;
/// A signature.
pub(crate) type Signature = SizedMessage<MAX_SIGNATURE_SIZE>;
/// A trait for committing previously staged changes.
pub(crate) trait Commit {
/// Commits a previously staged changes. When used with session cipher
/// state, the staged changes are typically counter increments that result
/// from encrypt or decrypt operations.
fn commit(&mut self);
}
/// A trait for maintaining a counter.
pub(crate) trait Counter {
/// Returns the current counter value.
fn n(&self) -> u64;
/// Sets the counter value to `n`.
fn set_n(&mut self, n: u64);
}
/// Provides cryptographic operations for encrypted sessions.
pub(crate) trait SessionCrypto {
/// A type to represent session cipher states. These are owned by and opaque
/// to the caller in `new_session_handshake` and `derive_session_handshake`.
type SessionCipherState: Commit + Counter;
/// Performs a session responder handshake for a new session.
///
/// # Parameters
///
/// * `static_dh_key`: The DPE session identity, which the client is
/// expected to already know.
/// * `initiator_handshake`: The handshake message received from the client.
/// * `payload`: The payload to include in the `responder_handshake`.
/// * `responder_handshake`: Receives the handshake message to be sent back
/// to the client.
/// * `decrypt_cipher_state`: Receives cipher state for decrypting incoming
/// session messages. This is intended to be passed to
/// [`SessionCrypto::session_decrypt`].
/// * `encrypt_cipher_state`: Receives cipher state for encrypting outgoing
/// session messages. This is intended to be passed to
/// [`SessionCrypto::session_encrypt`].
/// * `psk_seed`: Receives a PSK seed that can be used to construct a PSK to
/// be used when deriving a session (see
/// [`SessionCrypto::derive_session_handshake`]).
///
/// # Errors
///
/// This method allows implementers to return an error but it is expected to
/// be infallible.
#[allow(clippy::too_many_arguments)]
fn new_session_handshake(
static_dh_key: &DhPrivateKey,
initiator_handshake: &HandshakeMessage,
payload: &HandshakePayload,
responder_handshake: &mut HandshakeMessage,
decrypt_cipher_state: &mut Self::SessionCipherState,
encrypt_cipher_state: &mut Self::SessionCipherState,
psk_seed: &mut Hash,
) -> DpeResult<()>;
/// Performs a session responder handshake for a derived session. In
/// contrast to a new session handshake, a derived session does not use a
/// static key, but a pre-shared key (PSK) derived from an existing session.
///
/// # Parameters
///
/// * `psk`: A PSK derived from an existing session.
/// * `initiator_handshake`: The handshake message received from the client.
/// * `payload`: The payload to include in the `responder_handshake`.
/// * `responder_handshake`: Receives the handshake message to be sent back
/// to the client.
/// * `decrypt_cipher_state`: Receives cipher state for decrypting incoming
/// session messages. This is intended to be passed to
/// [`SessionCrypto::session_decrypt`].
/// * `encrypt_cipher_state`: Receives cipher state for encrypting outgoing
/// session messages. This is intended to be passed to
/// [`SessionCrypto::session_encrypt`].
/// * `psk_seed`: Receives a PSK seed that can be used to construct a PSK to
/// be used when deriving another session.
///
/// # Errors
///
/// This method allows implementers to return an error but it is expected to
/// be infallible.
#[allow(clippy::too_many_arguments)]
fn derive_session_handshake(
psk: &Hash,
initiator_handshake: &HandshakeMessage,
payload: &HandshakePayload,
responder_handshake: &mut HandshakeMessage,
decrypt_cipher_state: &mut Self::SessionCipherState,
encrypt_cipher_state: &mut Self::SessionCipherState,
psk_seed: &mut Hash,
) -> DpeResult<()>;
/// Derives a PSK from session state: `psk_seed`, `decrypt_cipher_state`,
/// and `encrypt_cipher_state`. The returned PSK is appropriate as an
/// argument to [`derive_session_handshake`].
///
/// # Errors
///
/// This method allows implementers to return an error but it is expected to
/// be infallible.
///
/// [`derive_session_handshake`]: #method.derive_session_handshake
fn derive_psk_from_session(
psk_seed: &Hash,
decrypt_cipher_state: &Self::SessionCipherState,
encrypt_cipher_state: &Self::SessionCipherState,
) -> DpeResult<Hash>;
/// Encrypts an outgoing session message with the given `cipher_state`. The
/// `in_place_buffer` both provides the plaintext message and receives the
/// corresponding ciphertext.
///
/// # Errors
///
/// This method fails with an OutOfMemory error if the encryption overhead
/// does not fit in the buffer.
fn session_encrypt(
cipher_state: &mut Self::SessionCipherState,
in_place_buffer: &mut Message,
) -> DpeResult<()>;
/// Decrypts an incoming session message with the given `cipher_state`. The
/// `in_place_buffer` both provides the ciphertext message and receives the
/// corresponding plaintext.
///
/// # Errors
///
/// This method fails with an InvalidArgument error if the ciphertext cannot
/// be decrypted (e.g. if tag authentication fails).
fn session_decrypt(
cipher_state: &mut Self::SessionCipherState,
in_place_buffer: &mut Message,
) -> DpeResult<()>;
}
/// Provides cryptographic operations. These operations are specifically for DPE
/// concepts, defined by a DPE profile, and to be invoked by a DPE instance.
pub(crate) trait Crypto {
/// An associated [`SessionCrypto`] type.
type S: SessionCrypto;
/// Returns a hash of `input`.
///
/// # Errors
///
/// This method is infallible.
fn hash(input: &[u8]) -> Hash;
/// Returns a hash over all items in `iter`, in order.
///
/// # Errors
///
/// This method is infallible.
fn hash_iter<'a>(iter: impl Iterator<Item = &'a [u8]>) -> Hash;
/// Runs a key derivation function (KDF) to derive a key the length of the
/// `derived_key` buffer. The inputs are interpreted as documented by the
/// [HKDF](<https://datatracker.ietf.org/doc/html/rfc5869>) scheme. The
/// implementation doesn't need to be HKDF specifically but needs to work
/// with HKDF-style inputs.
///
/// # Parameters
///
/// * `kdf_ikm`: input keying material
/// * `kdf_info`: HKDF-style info (optional)
/// * `kdf_salt`: HKDF-style salt (optional)
/// * `derived_key`: Receives the derived key
///
/// # Errors
///
/// Fails with an `InternalError` if `derived_key` is too large.
fn kdf(
kdf_ikm: &[u8],
kdf_info: &[u8],
kdf_salt: &[u8],
derived_key: &mut [u8],
) -> DpeResult<()>;
/// Derives an asymmetric key pair for signing from a given `seed`.
///
/// # Errors
///
/// This method allows implementers to return an error but it is expected to
/// be infallible.
fn signing_keypair_from_seed(
seed: &Hash,
) -> DpeResult<(SigningPublicKey, SigningPrivateKey)>;
/// Derives an asymmetric key pair for sealing from a given `seed`.
///
/// # Errors
///
/// This method allows implementers to return an error but it is expected to
/// be infallible.
fn sealing_keypair_from_seed(
seed: &Hash,
) -> DpeResult<(SealingPublicKey, SealingPrivateKey)>;
/// Computes a MAC over `data` using the given `key`.
///
/// # Errors
///
/// This method allows implementers to return an error but it is expected to
/// be infallible.
fn mac(key: &MacKey, data: &[u8]) -> DpeResult<Hash>;
/// Generates a signature over `tbs` using the given `key`.
///
/// # Errors
///
/// This method allows implementers to return an error but it is expected to
/// be infallible.
fn sign(key: &SigningPrivateKey, tbs: &[u8]) -> DpeResult<Signature>;
/// Encrypts data using the given `key` in a way that it can be decrypted by
/// the `unseal` method with the same `key`. The `in_place_buffer` both
/// provides the plaintext input and receives the ciphertext output.
///
/// # Errors
///
/// Fails with OutOfMemory if the ciphertext, including overhead, does not
/// fit in the buffer.
fn seal(
key: &EncryptionKey,
in_place_buffer: &mut Message,
) -> DpeResult<()>;
/// Decrypts and authenticates data previously generated by the `seal`
/// method using the given 'key'. The `in_place_buffer` both provides the
/// ciphertext input and receives the plaintext output.
///
/// # Errors
///
/// Fails with InvalidArgument if authenticated decryption fails.
fn unseal(
key: &EncryptionKey,
in_place_buffer: &mut Message,
) -> DpeResult<()>;
/// Encrypts data using an asymmetric scheme and the given `public_key` in
/// a way that it can be decrypted by the `unseal_asymmetric` method given
/// the corresponding private key. While this method is useful for testing,
/// a DPE does not use this during normal operation. The `in_place_buffer`
/// both provides the plaintext input and receives the ciphertext output.
///
/// # Errors
///
/// Fails with OutOfMemory if the ciphertext, including overhead, does not
/// fit in the buffer.
fn seal_asymmetric(
public_key: &SealingPublicKey,
in_place_buffer: &mut Message,
) -> DpeResult<()>;
/// Decrypts data using an asymmetric scheme and the give `key`. The
/// `in_place_buffer` both provides the ciphertext input and receives the
/// plaintext output.
///
/// # Errors
///
/// Fails with InvalidArgument if the ciphertext cannot be decrypted.
fn unseal_asymmetric(
key: &SealingPrivateKey,
in_place_buffer: &mut Message,
) -> DpeResult<()>;
}
#[cfg(test)]
pub(crate) mod test {
use super::*;
use crate::cbor::{
cbor_decoder_from_message, cbor_encoder_from_message,
encode_bytes_prefix, DecoderExt,
};
use crate::error::ErrCode;
use crate::memory::SmallMessage;
use crate::noise::test::SessionCryptoForTesting;
use aes_gcm_siv::{AeadInPlace, Aes256GcmSiv, KeyInit};
use ed25519_dalek::Signer;
use hkdf::Hkdf;
use hmac::{Hmac, Mac};
use hpke::{
aead::AeadTag, aead::AesGcm256, kdf::HkdfSha512, kem::Kem,
kem::X25519HkdfSha256, Deserializable, OpModeR, OpModeS, Serializable,
};
use log::{debug, error};
use rand_chacha::ChaCha12Rng;
use rand_core::SeedableRng;
use sha2::{Digest, Sha512};
pub(crate) type HmacSha512 = Hmac<Sha512>;
impl From<aes_gcm_siv::Error> for ErrCode {
fn from(err: aes_gcm_siv::Error) -> Self {
error!("Decrypt error: {:?}", err);
ErrCode::InvalidArgument
}
}
impl From<hpke::HpkeError> for ErrCode {
fn from(err: hpke::HpkeError) -> Self {
error!("Hpke error: {:?}", err);
ErrCode::InvalidArgument
}
}
#[derive(Clone, Default, Debug, Eq, PartialEq, Hash)]
pub(crate) struct CryptoForTesting {
pub(crate) noise: SessionCryptoForTesting,
}
impl Crypto for CryptoForTesting {
type S = SessionCryptoForTesting;
fn hash(input: &[u8]) -> Hash {
Hash::from_slice(Sha512::digest(input).as_slice()).unwrap()
}
fn hash_iter<'a>(iter: impl Iterator<Item = &'a [u8]>) -> Hash {
let mut hasher = Sha512::new();
for input in iter {
hasher.update(input);
}
Hash::from_slice(hasher.finalize().as_slice()).unwrap()
}
fn kdf(
kdf_ikm: &[u8],
kdf_info: &[u8],
kdf_salt: &[u8],
derived_key: &mut [u8],
) -> DpeResult<()> {
Hkdf::<Sha512>::new(Some(kdf_salt), kdf_ikm)
.expand(kdf_info, derived_key)
.unwrap();
Ok(())
}
fn signing_keypair_from_seed(
seed: &Hash,
) -> DpeResult<(SigningPublicKey, SigningPrivateKey)> {
let mut key_bytes: ed25519_dalek::SecretKey = Default::default();
key_bytes.copy_from_slice(&seed.as_slice()[..32]);
let sk = ed25519_dalek::SigningKey::from_bytes(&key_bytes);
Ok((
SigningPublicKey::from_array(sk.verifying_key().as_bytes()),
SigningPrivateKey::from_array(sk.as_bytes()),
))
}
fn sealing_keypair_from_seed(
seed: &Hash,
) -> DpeResult<(SealingPublicKey, SealingPrivateKey)> {
let (private_key, public_key) =
<X25519HkdfSha256 as Kem>::derive_keypair(seed.as_slice());
Ok((
SealingPublicKey::from_slice(public_key.to_bytes().as_slice())?,
SealingPrivateKey::from_slice(
private_key.to_bytes().as_slice(),
)?,
))
}
fn mac(key: &MacKey, data: &[u8]) -> DpeResult<Hash> {
let mut hmac =
<HmacSha512 as Mac>::new_from_slice(key.as_slice()).unwrap();
hmac.update(data);
Ok(Hash::from_slice(hmac.finalize().into_bytes().as_slice())
.unwrap())
}
fn sign(key: &SigningPrivateKey, tbs: &[u8]) -> DpeResult<Signature> {
let sk = ed25519_dalek::SigningKey::from_bytes(key.as_array());
let sig = sk.sign(tbs).to_bytes();
Ok(Signature::from_slice(&sig).unwrap())
}
fn seal(
key: &EncryptionKey,
in_place_buffer: &mut Message,
) -> DpeResult<()> {
const TAG_LENGTH: usize = 16;
let cipher = Aes256GcmSiv::new(key.as_slice().into());
let nonce = Default::default();
// Make space to append the tag.
let required_buffer_size = in_place_buffer.len() + TAG_LENGTH;
in_place_buffer
.vec
.resize_default(required_buffer_size)
.map_err(|_| ErrCode::OutOfMemory)
.unwrap();
cipher
.encrypt_in_place(&nonce, &[], &mut in_place_buffer.vec)
.unwrap();
Ok(())
}
fn unseal(
key: &EncryptionKey,
in_place_buffer: &mut Message,
) -> DpeResult<()> {
const TAG_LENGTH: usize = 16;
let cipher = Aes256GcmSiv::new(key.as_slice().into());
let nonce = Default::default();
cipher.decrypt_in_place(&nonce, &[], &mut in_place_buffer.vec)?;
let plaintext_len = in_place_buffer.len() - TAG_LENGTH;
in_place_buffer.vec.truncate(plaintext_len);
Ok(())
}
fn seal_asymmetric(
public_key: &SealingPublicKey,
in_place_buffer: &mut Message,
) -> DpeResult<()> {
let mut rng = <ChaCha12Rng as SeedableRng>::from_seed([0xFF; 32]);
let kem_public_key =
<X25519HkdfSha256 as Kem>::PublicKey::from_bytes(
public_key.as_slice(),
)?;
let (encapped_key, aead_tag) =
hpke::single_shot_seal_in_place_detached::<
AesGcm256,
HkdfSha512,
X25519HkdfSha256,
_,
>(
&OpModeS::Base,
&kem_public_key,
&[],
in_place_buffer.vec.as_mut(),
&[],
&mut rng,
)?;
let mut prefix = SmallMessage::new();
let mut encoder = cbor_encoder_from_message(&mut prefix);
let _ = encoder.bytes(encapped_key.to_bytes().as_slice())?;
let _ = encoder.bytes(aead_tag.to_bytes().as_slice())?;
encode_bytes_prefix(&mut prefix, in_place_buffer.len())?;
debug!(
"seal_asymmetric: h={}, d={}",
prefix.len(),
in_place_buffer.len()
);
in_place_buffer.insert_prefix(prefix.as_slice())?;
Ok(())
}
fn unseal_asymmetric(
private_key: &SealingPrivateKey,
in_place_buffer: &mut Message,
) -> DpeResult<()> {
let mut decoder = cbor_decoder_from_message(in_place_buffer);
let encapped_key =
<X25519HkdfSha256 as Kem>::EncappedKey::from_bytes(
decoder.bytes()?,
)?;
let tag = AeadTag::from_bytes(decoder.bytes()?)?;
// Leave only the ciphertext in in_place_buffer.
let sealed_data_position = decoder.decode_bytes_prefix()?;
debug!(
"unseal_asymmetric: h={}, d={}",
sealed_data_position,
in_place_buffer.len() - sealed_data_position
);
in_place_buffer.remove_prefix(sealed_data_position)?;
let kem_private_key =
<X25519HkdfSha256 as Kem>::PrivateKey::from_bytes(
private_key.as_slice(),
)?;
hpke::single_shot_open_in_place_detached::<
AesGcm256,
HkdfSha512,
X25519HkdfSha256,
>(
&OpModeR::Base,
&kem_private_key,
&encapped_key,
&[],
in_place_buffer.vec.as_mut(),
&[],
&tag,
)?;
Ok(())
}
}
}