cli/key/
tpm_key.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#![allow(clippy::no_effect_underscore_binding)]
6
7use super::{Alg, ExternalKey, KeyError, Tpm2shAlgId};
8use crate::{
9    auth::Auth,
10    crypto::{
11        crypto_hash_size, crypto_hmac, crypto_kdfa, crypto_make_name, derive_seed_with_ecc,
12        protect_seed_with_rsa, KDF_LABEL_INTEGRITY, KDF_LABEL_STORAGE,
13    },
14    device::{Device, DeviceError},
15    job::{Job, JobError},
16    template,
17    vtpm::VtpmError,
18    write_object,
19};
20
21use aes::Aes128;
22use cfb_mode::Encryptor;
23use cipher::{AsyncStreamCipher, KeyIvInit};
24use pem::Pem;
25use rand::{CryptoRng, RngCore};
26use rasn::{
27    prelude::ObjectIdentifier,
28    types::{OctetString, Utf8String},
29    AsnType, Decode, Decoder, Encode, Encoder,
30};
31use tpm2_protocol::data::{TpmAlgId, TpmRcBase, TpmtPublic};
32use tpm2_protocol::{
33    constant::TPM_MAX_COMMAND_SIZE,
34    data::{
35        Tpm2bAuth, Tpm2bData, Tpm2bDigest, Tpm2bEccParameter, Tpm2bEncryptedSecret, Tpm2bName,
36        Tpm2bPrivate, Tpm2bPrivateKeyRsa, Tpm2bPublic, Tpm2bSensitive, Tpm2bSensitiveCreate,
37        Tpm2bSensitiveData, Tpm2bSymKey, TpmCc, TpmaObject, TpmlPcrSelection, TpmsSensitiveCreate,
38        TpmtSensitive, TpmtSymDefObject, TpmuSensitiveComposite,
39    },
40    message::{TpmCreateCommand, TpmImportCommand},
41    TpmBuild, TpmError, TpmHandle, TpmParse, TpmWriter,
42};
43
44pub const OID_LOADABLE_KEY: ObjectIdentifier =
45    ObjectIdentifier::new_unchecked(std::borrow::Cow::Borrowed(&[2, 23, 133, 10, 1, 3]));
46pub const OID_IMPORTABLE_KEY: ObjectIdentifier =
47    ObjectIdentifier::new_unchecked(std::borrow::Cow::Borrowed(&[2, 23, 133, 10, 1, 4]));
48pub const OID_SEALED_DATA: ObjectIdentifier =
49    ObjectIdentifier::new_unchecked(std::borrow::Cow::Borrowed(&[2, 23, 133, 10, 1, 5]));
50
51/// A template for creating a new TPM key object.
52pub struct TpmKeyTemplate<'a> {
53    pub alg_desc: &'a Alg,
54    pub sensitive_data: Tpm2bSensitiveData,
55    pub key_type_oid: ObjectIdentifier,
56}
57
58/// A TPM policy struct that is directly compatible with ASN.1 DER encoding.
59#[derive(AsnType, Decode, Encode, Clone, Debug, Eq, PartialEq)]
60pub struct TpmPolicy {
61    #[rasn(tag(explicit(context, 0)))]
62    pub command_code: u32,
63    #[rasn(tag(explicit(context, 1)))]
64    pub command_policy: OctetString,
65}
66
67/// A TPM authorization policy struct that is directly compatible with ASN.1 DER encoding.
68#[derive(AsnType, Decode, Encode, Clone, Debug, Eq, PartialEq)]
69pub struct TpmAuthPolicy {
70    #[rasn(tag(explicit(context, 0)))]
71    pub name: Option<Utf8String>,
72    #[rasn(tag(explicit(context, 1)))]
73    pub policy: Vec<TpmPolicy>,
74}
75
76/// A TPM key struct that is directly compatible with ASN.1 DER encoding.
77#[derive(AsnType, Decode, Encode, Clone, Debug, Eq, PartialEq)]
78pub struct TpmKey {
79    pub key_type: ObjectIdentifier,
80    #[rasn(tag(explicit(context, 0)))]
81    pub empty_auth: Option<bool>,
82    #[rasn(tag(explicit(context, 1)))]
83    pub policy: Option<Vec<TpmPolicy>>,
84    #[rasn(tag(explicit(context, 2)))]
85    pub secret: Option<OctetString>,
86    #[rasn(tag(explicit(context, 3)))]
87    pub auth_policy: Option<Vec<TpmAuthPolicy>>,
88    #[rasn(tag(explicit(context, 4)))]
89    pub description: Option<Utf8String>,
90    #[rasn(tag(explicit(context, 5)))]
91    pub rsa_parent: Option<bool>,
92    #[rasn(tag(explicit(context, 6)))]
93    pub parent_pub_key: Option<OctetString>,
94    pub parent: u32,
95    pub pub_key: OctetString,
96    pub priv_key: OctetString,
97}
98
99impl TpmKey {
100    /// Creates a new `TpmKey` by executing a `TPM2_Create` command.
101    ///
102    /// # Errors
103    ///
104    /// Returns a `KeyError` if any of the TPM structures cannot be serialized or the command fails.
105    #[allow(clippy::too_many_arguments)]
106    pub fn new(
107        job: &mut Job,
108        device: &mut Device,
109        auth_list: &[Auth],
110        user_auth: Tpm2bAuth,
111        auth_policy: Tpm2bDigest,
112        object_attributes: TpmaObject,
113        parent_handle: TpmHandle,
114        template: &TpmKeyTemplate,
115    ) -> Result<Self, KeyError> {
116        let public_template =
117            template::build_public(template.alg_desc, auth_policy, object_attributes);
118
119        let create_cmd = TpmCreateCommand {
120            parent_handle: parent_handle.0.into(),
121            in_sensitive: Tpm2bSensitiveCreate {
122                inner: TpmsSensitiveCreate {
123                    user_auth,
124                    data: template.sensitive_data,
125                },
126            },
127            in_public: Tpm2bPublic {
128                inner: public_template,
129            },
130            outside_info: Tpm2bData::default(),
131            creation_pcr: TpmlPcrSelection::default(),
132        };
133
134        let handles = [parent_handle.0];
135        let (resp, _) = job
136            .execute(device, &create_cmd, &handles, auth_list)
137            .map_err(|e| {
138                if let JobError::Device(DeviceError::TpmRc(rc)) = &e {
139                    if rc.base() == TpmRcBase::Type {
140                        return KeyError::InvalidParent(parent_handle.0);
141                    }
142                }
143                match e {
144                    JobError::Device(d) => KeyError::Device(d),
145                    JobError::Vtpm(
146                        VtpmError::Auth(_)
147                        | VtpmError::HandleNotFound(_, _)
148                        | VtpmError::TrailingAuthorizations,
149                    ) => KeyError::Device(DeviceError::TpmProtocol(TpmError::MalformedData)),
150                    _ => KeyError::ValueConversionFailed(e.to_string()),
151                }
152            })?;
153
154        let create_resp = resp
155            .Create()
156            .map_err(|_| DeviceError::ResponseMismatch(TpmCc::Create))?;
157
158        let (parent_public, _) = device.read_public(parent_handle)?;
159        let parent_public_2b = Tpm2bPublic {
160            inner: parent_public,
161        };
162
163        Self::from_creation_data(
164            user_auth.is_empty(),
165            parent_handle,
166            &create_resp.out_public,
167            &create_resp.out_private,
168            &auth_policy,
169            template.key_type_oid.clone(),
170            &parent_public_2b,
171        )
172    }
173
174    /// Creates a new `TpmKey` from the raw TPM creation response data.
175    #[allow(clippy::too_many_arguments)]
176    fn from_creation_data(
177        empty_auth: bool,
178        parent_handle: TpmHandle,
179        out_public: &Tpm2bPublic,
180        out_private: &Tpm2bPrivate,
181        policy_digest: &Tpm2bDigest,
182        key_type: ObjectIdentifier,
183        parent_public: &Tpm2bPublic,
184    ) -> Result<Self, KeyError> {
185        let policy = if policy_digest.is_empty() {
186            None
187        } else {
188            Some(vec![TpmPolicy {
189                command_code: 0,
190                command_policy: OctetString::copy_from_slice(policy_digest.as_ref()),
191            }])
192        };
193        Ok(Self {
194            key_type,
195            empty_auth: empty_auth.then_some(true),
196            policy,
197            secret: None,
198            auth_policy: None,
199            description: None,
200            rsa_parent: None,
201            parent_pub_key: Some(OctetString::copy_from_slice(
202                &write_object(parent_public).map_err(DeviceError::TpmProtocol)?,
203            )),
204            parent: parent_handle.0,
205            pub_key: OctetString::copy_from_slice(
206                &write_object(out_public).map_err(DeviceError::TpmProtocol)?,
207            ),
208            priv_key: OctetString::copy_from_slice(
209                &write_object(out_private).map_err(DeviceError::TpmProtocol)?,
210            ),
211        })
212    }
213
214    /// Imports an external key under a TPM parent, creating a new `TpmKey`.
215    ///
216    /// # Errors
217    ///
218    /// Returns an error if the TPM import operation fails.
219    ///
220    /// # Remarks on `symmetricAlg`
221    ///
222    /// In `TPM2_Import` the `symmetricAlg` parameter defines the cipher for the
223    /// inner wrapper of the `duplicate` blob.
224    ///
225    /// The key import process differences for ECC and RSA parents:
226    ///
227    /// - **ECC**: the import uses ECDH with AES-CFB as the symmetric algorithm.
228    /// - **RSA**: the import uses RSA-OAEP to encrypt a seed, which passed in
229    ///   the `inSymSeed` command parameter, `encryptionKey` is zero-length
230    ///   vector and `symmetricAlg` must be set to `TPM_ALG_NULL`.
231    #[allow(clippy::too_many_arguments)]
232    pub fn from_external_key(
233        device: &mut Device,
234        job: &mut Job,
235        parent_handle: TpmHandle,
236        external_key: &ExternalKey,
237        rng: &mut (impl RngCore + CryptoRng),
238        handles: &[u32],
239        auth_list: &[Auth],
240    ) -> Result<Self, KeyError> {
241        let (parent_public, parent_name) = match device.read_public(parent_handle) {
242            Ok(result) => result,
243            Err(DeviceError::Io(e)) if e.kind() == std::io::ErrorKind::InvalidInput => {
244                return Err(KeyError::InvalidParent(parent_handle.0));
245            }
246            Err(e) => return Err(e.into()),
247        };
248        let parent_name_alg = parent_public.name_alg;
249
250        let public = external_key.to_public(parent_name_alg)?;
251        let object_name = crypto_make_name(&public)?;
252        let sensitive_blob = external_key.sensitive_blob();
253
254        let (duplicate, in_sym_seed, encryption_key) = create_import_blob(
255            &parent_public,
256            &public,
257            &sensitive_blob,
258            &parent_name,
259            &object_name,
260            rng,
261        )?;
262
263        let import_cmd = TpmImportCommand {
264            parent_handle: parent_handle.0.into(),
265            encryption_key,
266            object_public: Tpm2bPublic {
267                inner: public.clone(),
268            },
269            duplicate,
270            in_sym_seed,
271            symmetric_alg: TpmtSymDefObject::default(),
272        };
273
274        let (resp, _) = job
275            .execute(device, &import_cmd, handles, auth_list)
276            .map_err(|e| match e {
277                JobError::Device(d) => KeyError::Device(d),
278                JobError::Vtpm(
279                    VtpmError::Auth(_)
280                    | VtpmError::HandleNotFound(_, _)
281                    | VtpmError::TrailingAuthorizations,
282                ) => KeyError::Device(DeviceError::TpmProtocol(TpmError::MalformedData)),
283                _ => KeyError::ValueConversionFailed(e.to_string()),
284            })?;
285
286        let import_resp = resp
287            .Import()
288            .map_err(|_| DeviceError::ResponseMismatch(TpmCc::Import))?;
289        let out_private = import_resp.out_private;
290
291        let parent_public_2b = Tpm2bPublic {
292            inner: parent_public,
293        };
294        let tpm_key = Self::from_creation_data(
295            true,
296            parent_handle,
297            &Tpm2bPublic { inner: public },
298            &out_private,
299            &Tpm2bDigest::default(),
300            OID_IMPORTABLE_KEY,
301            &parent_public_2b,
302        )?;
303
304        Ok(tpm_key)
305    }
306
307    /// Parses and returns the public area of the key.
308    ///
309    /// # Errors
310    ///
311    /// Returns a `KeyError` if the public key bytes cannot be parsed.
312    pub fn public(&self) -> Result<Tpm2bPublic, KeyError> {
313        let (public, _) = Tpm2bPublic::parse(&self.pub_key).map_err(DeviceError::TpmProtocol)?;
314        Ok(public)
315    }
316
317    /// Serialize TPM key to PEM.
318    ///
319    /// # Errors
320    ///
321    /// Returns `CliError` if the key's OID or other fields cannot be encoded to DER.
322    pub fn to_pem(&self) -> Result<String, KeyError> {
323        Ok(pem::encode(&Pem::new("TSS2 PRIVATE KEY", self.to_der()?)))
324    }
325
326    /// Serialize TPM key to DER bytes.
327    ///
328    /// # Errors
329    ///
330    /// Returns `CliError` if the key's OID or other fields cannot be encoded to DER.
331    pub fn to_der(&self) -> Result<Vec<u8>, KeyError> {
332        rasn::der::encode(self).map_err(Into::into)
333    }
334
335    /// Parse TPM key from PEM bytes.
336    ///
337    /// # Errors
338    ///
339    /// Returns `CliError` if the PEM bytes cannot be parsed.
340    pub fn from_pem(pem_bytes: &[u8]) -> Result<Self, KeyError> {
341        let pem = pem::parse(pem_bytes)?;
342        if pem.tag() == "TSS2 PRIVATE KEY" {
343            Self::from_der(pem.contents())
344        } else {
345            Err(KeyError::UnsupportedPemTag(pem.tag().to_string()))
346        }
347    }
348
349    /// Parse TPM key from DER bytes.
350    ///
351    /// # Errors
352    ///
353    /// Returns `CliError` if the DER bytes cannot be parsed into a valid `TpmKeyAsn1` data.
354    pub fn from_der(der_bytes: &[u8]) -> Result<Self, KeyError> {
355        rasn::der::decode(der_bytes).map_err(Into::into)
356    }
357}
358
359/// Create import blob. As per the TCG TPM 2.0 specification, the duplication
360/// blob for import requires a zero IV for its symmetric encryption in CFB mode.
361///
362/// # Errors
363///
364/// Returns a `CryptoError` if any underlying cryptographic operations fail, if
365/// the provided parent key type is unsupported for import, or if TPM data
366/// structures cannot be serialized.
367#[allow(clippy::too_many_arguments)]
368fn create_import_blob(
369    parent_public: &TpmtPublic,
370    object_public: &TpmtPublic,
371    private_bytes: &[u8],
372    _parent_name: &Tpm2bName,
373    object_name: &Tpm2bName,
374    rng: &mut (impl RngCore + CryptoRng),
375) -> Result<(Tpm2bPrivate, Tpm2bEncryptedSecret, Tpm2bData), KeyError> {
376    let parent_name_alg = parent_public.name_alg;
377    let parent_key_type = parent_public.object_type;
378
379    let (seed, in_sym_seed) = match parent_key_type {
380        TpmAlgId::Rsa => {
381            let seed_size = crypto_hash_size(parent_name_alg).ok_or(
382                KeyError::UnsupportedNameAlgorithm(Tpm2shAlgId(parent_name_alg)),
383            )? as usize;
384            let mut seed = vec![0u8; seed_size];
385            rng.fill_bytes(&mut seed);
386            let encrypted_seed = protect_seed_with_rsa(parent_public, &seed, rng)?;
387            (seed, encrypted_seed)
388        }
389        TpmAlgId::Ecc => {
390            let (derived_seed, ephemeral_point) = derive_seed_with_ecc(parent_public, rng)?;
391            let point_bytes = write_object(&ephemeral_point)?;
392            let secret = Tpm2bEncryptedSecret::try_from(point_bytes.as_slice())?;
393            (derived_seed, secret)
394        }
395        _ => {
396            return Err(KeyError::UnsupportedKeyAlgorithm(Tpm2shAlgId(
397                parent_key_type,
398            )))
399        }
400    };
401
402    let sym_key = crypto_kdfa(
403        parent_name_alg,
404        &seed,
405        KDF_LABEL_STORAGE,
406        object_name.as_ref(),
407        &[],
408        128,
409    )?;
410
411    let key_bits = crypto_hash_size(parent_name_alg).ok_or(KeyError::UnsupportedNameAlgorithm(
412        Tpm2shAlgId(parent_name_alg),
413    ))? * 8;
414    let key_bits =
415        u16::try_from(key_bits).map_err(|_| KeyError::InvalidRsaKeyBits(key_bits.to_string()))?;
416
417    let hmac_key = crypto_kdfa(
418        parent_name_alg,
419        &seed,
420        KDF_LABEL_INTEGRITY,
421        &[],
422        &[],
423        key_bits,
424    )?;
425
426    let object_key_type = object_public.object_type;
427    let sensitive_composite = match object_key_type {
428        TpmAlgId::Rsa => TpmuSensitiveComposite::Rsa(Tpm2bPrivateKeyRsa::try_from(private_bytes)?),
429        TpmAlgId::Ecc => TpmuSensitiveComposite::Ecc(Tpm2bEccParameter::try_from(private_bytes)?),
430        TpmAlgId::KeyedHash => {
431            TpmuSensitiveComposite::Bits(Tpm2bSensitiveData::try_from(private_bytes)?)
432        }
433        TpmAlgId::SymCipher => TpmuSensitiveComposite::Sym(Tpm2bSymKey::try_from(private_bytes)?),
434        _ => {
435            return Err(KeyError::UnsupportedKeyAlgorithm(Tpm2shAlgId(
436                object_key_type,
437            )))
438        }
439    };
440    let sensitive = TpmtSensitive {
441        sensitive_type: object_key_type,
442        auth_value: Tpm2bAuth::default(),
443        seed_value: Tpm2bDigest::default(),
444        sensitive: sensitive_composite,
445    };
446    let sensitive_tpm2b = Tpm2bSensitive::from(sensitive);
447    let mut enc_data = write_object(&sensitive_tpm2b)?;
448
449    let iv = [0u8; 16];
450    let cipher = Encryptor::<Aes128>::new(sym_key.as_slice().into(), &iv.into());
451    cipher.encrypt(&mut enc_data);
452
453    let final_mac = crypto_hmac(
454        parent_name_alg,
455        &hmac_key,
456        &[&enc_data, object_name.as_ref()],
457    )?;
458
459    let duplicate_blob = {
460        let mut duplicate_blob_buf = [0u8; TPM_MAX_COMMAND_SIZE];
461        let len = {
462            let mut writer = TpmWriter::new(&mut duplicate_blob_buf);
463            Tpm2bDigest::try_from(final_mac.as_slice())?.build(&mut writer)?;
464            writer.write_bytes(&enc_data)?;
465            writer.len()
466        };
467        duplicate_blob_buf[..len].to_vec()
468    };
469
470    Ok((
471        Tpm2bPrivate::try_from(duplicate_blob.as_slice())?,
472        in_sym_seed,
473        Tpm2bData::default(),
474    ))
475}