cli/
crypto.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright (c) 2025 Opinsys Oy
3// Copyright (c) 2024-2025 Jarkko Sakkinen
4
5//! This file contains cryptographic algorithms shared by tpm2sh and `MockTPM`.
6
7use crate::convert::from_tpm_object_to_vec;
8use hmac::{Hmac, Mac};
9use num_traits::FromPrimitive;
10use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
11use rand::{CryptoRng, RngCore};
12use rsa::Oaep;
13use sha1::Sha1;
14use sha2::{Digest, Sha256, Sha384, Sha512};
15use thiserror::Error;
16use tpm2_protocol::{
17    data::{
18        Tpm2bEccParameter, Tpm2bEncryptedSecret, Tpm2bName, TpmAlgId, TpmEccCurve, TpmsEccPoint,
19        TpmtPublic, TpmuPublicId, TpmuPublicParms,
20    },
21    tpm_hash_size, TpmErrorKind,
22};
23
24pub const UNCOMPRESSED_POINT_TAG: u8 = 0x04;
25
26pub const KDF_LABEL_DUPLICATE: &str = "DUPLICATE";
27pub const KDF_LABEL_INTEGRITY: &str = "INTEGRITY";
28pub const KDF_LABEL_STORAGE: &str = "STORAGE";
29
30#[derive(Debug, Error)]
31pub enum CryptoError {
32    #[error("unsupported or invalid hash algorithm")]
33    InvalidHashAlgorithm,
34    #[error("invalid cryptographic key")]
35    InvalidKey,
36    #[error("unsupported or invalid cryptographic scheme")]
37    InvalidScheme,
38    #[error("MAC verification failed")]
39    MacVerificationFailed,
40    #[error("RSA operation failed: {0}")]
41    RsaOperationFailed(String),
42    #[error("DER encoding failed: {0}")]
43    DerEncodingFailed(String),
44    #[error("unsupported elliptic curve")]
45    UnsupportedCurve,
46    #[error("invalid elliptic curve point")]
47    InvalidEccPoint,
48    #[error("value conversion failed: {0}")]
49    ValueConversionFailed(String),
50    #[error("TPM: {0}")]
51    Tpm(TpmErrorKind),
52}
53
54impl From<TpmErrorKind> for CryptoError {
55    fn from(err: TpmErrorKind) -> Self {
56        Self::Tpm(err)
57    }
58}
59
60/// Computes a cryptographic digest over a series of data chunks.
61///
62/// # Errors
63///
64/// Returns a `CryptoError` if the algorithm is unsupported.
65pub fn crypto_digest(alg: TpmAlgId, data_chunks: &[&[u8]]) -> Result<Vec<u8>, CryptoError> {
66    macro_rules! digest {
67        ($hasher:ty) => {{
68            let mut hasher = <$hasher>::new();
69            for chunk in data_chunks {
70                hasher.update(chunk);
71            }
72            Ok(hasher.finalize().to_vec())
73        }};
74    }
75
76    match alg {
77        TpmAlgId::Sha1 => digest!(Sha1),
78        TpmAlgId::Sha256 => digest!(Sha256),
79        TpmAlgId::Sha384 => digest!(Sha384),
80        TpmAlgId::Sha512 => digest!(Sha512),
81        _ => Err(CryptoError::InvalidHashAlgorithm),
82    }
83}
84
85/// Computes an HMAC digest over a series of data chunks.
86///
87/// # Errors
88///
89/// Returns a `CryptoError` if the key is invalid or the algorithm is unsupported.
90pub fn crypto_hmac(
91    alg: TpmAlgId,
92    key: &[u8],
93    data_chunks: &[&[u8]],
94) -> Result<Vec<u8>, CryptoError> {
95    macro_rules! hmac {
96        ($digest:ty) => {{
97            let mut mac =
98                <Hmac<$digest> as Mac>::new_from_slice(key).map_err(|_| CryptoError::InvalidKey)?;
99            for chunk in data_chunks {
100                mac.update(chunk);
101            }
102            Ok(mac.finalize().into_bytes().to_vec())
103        }};
104    }
105
106    match alg {
107        TpmAlgId::Sha256 => hmac!(Sha256),
108        TpmAlgId::Sha384 => hmac!(Sha384),
109        TpmAlgId::Sha512 => hmac!(Sha512),
110        _ => Err(CryptoError::InvalidHashAlgorithm),
111    }
112}
113
114/// Verifies an HMAC signature over a series of data chunks.
115///
116/// # Errors
117///
118/// Returns a `CryptoError` if the key is invalid, the algorithm is unsupported,
119/// or the signature does not match.
120pub fn crypto_hmac_verify(
121    alg: TpmAlgId,
122    key: &[u8],
123    data_chunks: &[&[u8]],
124    signature: &[u8],
125) -> Result<(), CryptoError> {
126    macro_rules! verify_hmac {
127        ($digest:ty) => {{
128            let mut mac =
129                <Hmac<$digest> as Mac>::new_from_slice(key).map_err(|_| CryptoError::InvalidKey)?;
130            for chunk in data_chunks {
131                mac.update(chunk);
132            }
133            mac.verify_slice(signature)
134                .map_err(|_| CryptoError::MacVerificationFailed)
135        }};
136    }
137
138    match alg {
139        TpmAlgId::Sha256 => verify_hmac!(Sha256),
140        TpmAlgId::Sha384 => verify_hmac!(Sha384),
141        TpmAlgId::Sha512 => verify_hmac!(Sha512),
142        _ => Err(CryptoError::InvalidHashAlgorithm),
143    }
144}
145
146/// Implements the `KDFa` key derivation function from the TPM specification.
147///
148/// # Errors
149///
150/// Returns a `CryptoError` on failure.
151pub fn crypto_kdfa(
152    auth_hash: TpmAlgId,
153    hmac_key: &[u8],
154    label: &str,
155    context_a: &[u8],
156    context_b: &[u8],
157    key_bits: u16,
158) -> Result<Vec<u8>, CryptoError> {
159    let mut key_stream = Vec::new();
160    let key_bytes = (key_bits as usize).div_ceil(8);
161    let label_bytes = {
162        let mut bytes = label.as_bytes().to_vec();
163        bytes.push(0);
164        bytes
165    };
166
167    let mut counter: u32 = 1;
168    while key_stream.len() < key_bytes {
169        let counter_bytes = counter.to_be_bytes();
170        let key_bits_bytes = u32::from(key_bits).to_be_bytes();
171        let hmac_payload = [
172            counter_bytes.as_slice(),
173            label_bytes.as_slice(),
174            context_a,
175            context_b,
176            key_bits_bytes.as_slice(),
177        ];
178
179        let result = crypto_hmac(auth_hash, hmac_key, &hmac_payload)?;
180        let remaining = key_bytes - key_stream.len();
181        let to_take = remaining.min(result.len());
182        key_stream.extend_from_slice(&result[..to_take]);
183
184        counter += 1;
185    }
186
187    Ok(key_stream)
188}
189
190/// Implements the `KDFe` key derivation function from SP 800-56A for ECDH.
191///
192/// # Errors
193///
194/// Returns a `CryptoError` on failure.
195pub fn crypto_kdfe(
196    hash_alg: TpmAlgId,
197    z: &[u8],
198    label: &str,
199    context_u: &[u8],
200    context_v: &[u8],
201    key_bits: u16,
202) -> Result<Vec<u8>, CryptoError> {
203    let mut key_stream = Vec::new();
204    let key_bytes = (key_bits as usize).div_ceil(8);
205    let mut label_bytes = label.as_bytes().to_vec();
206    if label_bytes.last() != Some(&0) {
207        label_bytes.push(0);
208    }
209
210    let other_info = [label_bytes.as_slice(), context_u, context_v].concat();
211
212    let mut counter: u32 = 1;
213    while key_stream.len() < key_bytes {
214        let counter_bytes = counter.to_be_bytes();
215        let digest_payload = [&counter_bytes, z, &other_info];
216
217        let result = crypto_digest(hash_alg, &digest_payload)?;
218        let remaining = key_bytes - key_stream.len();
219        let to_take = remaining.min(result.len());
220        key_stream.extend_from_slice(&result[..to_take]);
221
222        counter += 1;
223    }
224
225    Ok(key_stream)
226}
227
228/// Dispatches RSA OAEP encryption based on the `TpmAlgId`.
229fn dispatch_rsa_oaep_encrypt(
230    key: &rsa::RsaPublicKey,
231    rng: &mut (impl CryptoRng + RngCore),
232    name_alg: TpmAlgId,
233    label: &str,
234    data: &[u8],
235) -> Result<Vec<u8>, CryptoError> {
236    let result = match name_alg {
237        TpmAlgId::Sha1 => key.encrypt(rng, Oaep::new_with_label::<Sha1, _>(label), data),
238        TpmAlgId::Sha256 => key.encrypt(rng, Oaep::new_with_label::<Sha256, _>(label), data),
239        TpmAlgId::Sha384 => key.encrypt(rng, Oaep::new_with_label::<Sha384, _>(label), data),
240        TpmAlgId::Sha512 => key.encrypt(rng, Oaep::new_with_label::<Sha512, _>(label), data),
241        _ => return Err(CryptoError::InvalidScheme),
242    };
243    result.map_err(|e| CryptoError::RsaOperationFailed(e.to_string()))
244}
245
246/// Encrypts a seed using the parent's RSA public key for duplication.
247///
248/// See Table 27 in TCG TPM 2.0 Architectures specification for more information.
249///
250/// # Errors
251///
252/// Returns a `CryptoError` on failure.
253pub fn protect_seed_with_rsa(
254    parent_public: &TpmtPublic,
255    seed: &[u8],
256    rng: &mut (impl RngCore + CryptoRng),
257) -> Result<Tpm2bEncryptedSecret, CryptoError> {
258    let n = match &parent_public.unique {
259        TpmuPublicId::Rsa(data) => Ok(data.as_ref()),
260        _ => Err(CryptoError::InvalidKey),
261    }?;
262    let e_raw = match &parent_public.parameters {
263        TpmuPublicParms::Rsa(params) => Ok(params.exponent),
264        _ => Err(CryptoError::InvalidKey),
265    }?;
266    let e = if e_raw == 0 { 65537 } else { e_raw };
267    let rsa_pub_key = rsa::RsaPublicKey::new(
268        rsa::BigUint::from_bytes_be(n),
269        rsa::BigUint::from_u32(e).ok_or_else(|| {
270            CryptoError::ValueConversionFailed("invalid RSA exponent".to_string())
271        })?,
272    )
273    .map_err(|e| CryptoError::RsaOperationFailed(e.to_string()))?;
274
275    let label = "DUPLICATE\0";
276
277    let encrypted_seed =
278        dispatch_rsa_oaep_encrypt(&rsa_pub_key, rng, parent_public.name_alg, label, seed)?;
279
280    Tpm2bEncryptedSecret::try_from(encrypted_seed.as_slice())
281        .map_err(|e| CryptoError::ValueConversionFailed(e.to_string()))
282}
283
284/// Derives a `seed` and an ephemeral public key using ECDH with the parent's ECC public key.
285///
286/// # Errors
287///
288/// Returns a `CryptoError` on failure.
289pub fn derive_seed_with_ecc(
290    parent_public: &TpmtPublic,
291    rng: &mut (impl RngCore + CryptoRng),
292) -> Result<(Vec<u8>, TpmsEccPoint), CryptoError> {
293    let (parent_point, curve_id) = match (&parent_public.unique, &parent_public.parameters) {
294        (TpmuPublicId::Ecc(point), TpmuPublicParms::Ecc(params)) => Ok((point, params.curve_id)),
295        _ => Err(CryptoError::InvalidKey),
296    }?;
297
298    match curve_id {
299        TpmEccCurve::NistP256 => crypto_ecdh_p256(parent_point, parent_public.name_alg, rng),
300        TpmEccCurve::NistP384 => crypto_ecdh_p384(parent_point, parent_public.name_alg, rng),
301        TpmEccCurve::NistP521 => crypto_ecdh_p521(parent_point, parent_public.name_alg, rng),
302        _ => Err(CryptoError::UnsupportedCurve),
303    }
304}
305
306macro_rules! ecdh {
307    (
308        $vis:vis $fn_name:ident,
309        $pk_ty:ty, $sk_ty:ty, $affine_ty:ty, $dh_fn:path, $encoded_point_ty:ty
310    ) => {
311        #[allow(clippy::similar_names, clippy::missing_errors_doc)]
312        $vis fn $fn_name(
313            parent_point: &TpmsEccPoint,
314            name_alg: TpmAlgId,
315            rng: &mut (impl RngCore + CryptoRng),
316        ) -> Result<(Vec<u8>, TpmsEccPoint), CryptoError> {
317            let encoded_point = <$encoded_point_ty>::from_affine_coordinates(
318                parent_point.x.as_ref().into(),
319                parent_point.y.as_ref().into(),
320                false,
321            );
322            let affine_point_opt: Option<$affine_ty> =
323                <$affine_ty>::from_encoded_point(&encoded_point).into();
324            let affine_point = affine_point_opt.ok_or(CryptoError::InvalidEccPoint)?;
325
326            if affine_point.is_identity().into() {
327                return Err(CryptoError::InvalidEccPoint);
328            }
329
330            let parent_pk =
331                <$pk_ty>::from_affine(affine_point).map_err(|_| CryptoError::InvalidEccPoint)?;
332
333            let ephemeral_sk = <$sk_ty>::random(rng);
334            let ephemeral_pk_bytes_encoded = ephemeral_sk.public_key().to_encoded_point(false);
335            let ephemeral_pk_bytes = ephemeral_pk_bytes_encoded.as_bytes();
336            if ephemeral_pk_bytes.is_empty() || ephemeral_pk_bytes[0] != UNCOMPRESSED_POINT_TAG {
337                return Err(CryptoError::InvalidEccPoint);
338            }
339            let coord_len = (ephemeral_pk_bytes.len() - 1) / 2;
340            let x = &ephemeral_pk_bytes[1..=coord_len];
341            let y = &ephemeral_pk_bytes[1 + coord_len..];
342
343            let context_u = x;
344            let context_v = parent_point.x.as_ref();
345
346            let shared_secret = $dh_fn(ephemeral_sk.to_nonzero_scalar(), parent_pk.as_affine());
347            let z = shared_secret.raw_secret_bytes();
348            let seed_bits =
349                u16::try_from(tpm_hash_size(&name_alg).ok_or(CryptoError::InvalidHashAlgorithm)? * 8)
350                    .map_err(|_| CryptoError::InvalidKey)?;
351            let seed =
352                crypto_kdfe(name_alg, &z, KDF_LABEL_DUPLICATE, context_u, context_v, seed_bits)?;
353
354            let ephemeral_point = TpmsEccPoint {
355                x: Tpm2bEccParameter::try_from(x)
356                    .map_err(|e| CryptoError::ValueConversionFailed(e.to_string()))?,
357                y: Tpm2bEccParameter::try_from(y)
358                    .map_err(|e| CryptoError::ValueConversionFailed(e.to_string()))?,
359            };
360
361            Ok((seed, ephemeral_point))
362        }
363    };
364}
365
366ecdh!(
367    pub crypto_ecdh_p256,
368    p256::PublicKey,
369    p256::SecretKey,
370    p256::AffinePoint,
371    p256::ecdh::diffie_hellman,
372    p256::EncodedPoint
373);
374
375ecdh!(
376    pub crypto_ecdh_p384,
377    p384::PublicKey,
378    p384::SecretKey,
379    p384::AffinePoint,
380    p384::ecdh::diffie_hellman,
381    p384::EncodedPoint
382);
383
384ecdh!(
385    pub crypto_ecdh_p521,
386    p521::PublicKey,
387    p521::SecretKey,
388    p521::AffinePoint,
389    p521::ecdh::diffie_hellman,
390    p521::EncodedPoint
391);
392
393/// Calculates the TPM name of a public object.
394///
395/// # Errors
396///
397/// Returns a `CryptoError` on failure.
398pub fn crypto_make_name(public: &TpmtPublic) -> Result<Tpm2bName, CryptoError> {
399    let mut name_buf = Vec::new();
400    let name_alg = public.name_alg;
401    name_buf.extend_from_slice(&(name_alg as u16).to_be_bytes());
402    let public_area_bytes = from_tpm_object_to_vec(public)
403        .map_err(|e| CryptoError::ValueConversionFailed(e.to_string()))?;
404    let digest = crypto_digest(name_alg, &[&public_area_bytes])?;
405    name_buf.extend_from_slice(&digest);
406    Tpm2bName::try_from(name_buf.as_slice()).map_err(Into::into)
407}