lexe-tls 0.1.17

Lexe TLS configs, certs, and utilities
Documentation
//! Manage self-signed x509 certificate containing enclave remote attestation
//! endorsements.

use std::{borrow::Cow, fmt};

use anyhow::{Context, anyhow, ensure};
use asn1_rs::FromDer;
use lexe_crypto::ed25519;
use lexe_enclave::enclave;
use lexe_hex::hex;
use x509_parser::certificate::X509Certificate;
use yasna::{ASN1Error, ASN1ErrorKind, models::ObjectIdentifier};

use crate::attest_client::quote::ReportData;

/// Packages a remote attestation quote generated by
/// `lexe_tls_attest_server::quote::quote_enclave` into a x509 cert extension
/// with all the evidence a client needs to verify it.
///
/// ```asn.1
/// SgxAttestationExtension ::= SEQUENCE {
///     QUOTE      OCTET STRING
/// }
/// ```
#[derive(Eq, PartialEq)]
pub struct SgxAttestationExtension<'quote> {
    pub quote: Cow<'quote, [u8]>,
    // We don't verify the QE_REPORT because it requires calling an Intel JSON
    // API for marginal security against old SGX versions. Also, the MIN CPUSVN
    // already handles the case where the platform enclaves are too old.
    // pub qe_report: Cow<'b, [u8]>,
}

// -- impl SgxAttestationExtension -- //

impl SgxAttestationExtension<'_> {
    /// This is the Intel SGX OID prefix + 1337.7
    /// gramine uses the same but 1337.6 to embed the quote.
    pub const OID: &'static [u64] = &[1, 2, 840, 113741, 1337, 7];

    /// DER-encoded OID
    pub const OID_DER: &'static [u8] = &[
        0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF8, 0x4D, 0x8A, 0x39, 0x07,
    ];

    pub fn oid_yasna() -> ObjectIdentifier {
        ObjectIdentifier::from_slice(Self::OID)
    }

    #[rustfmt::skip]
    pub const fn oid_asn1_rs() -> asn1_rs::Oid<'static> {
        // TODO(phlip9): won't parse OID_DER...
        asn1_rs::oid!(1.2.840.113741.1337.7)
    }

    /// Clients that don't understand a critical extension will immediately
    /// reject the cert. Unfortunately, setting this to true seems to break
    /// clients...
    pub const fn is_critical() -> bool {
        false
    }

    /// Serialize the attestation to DER.
    pub fn to_der_bytes(&self) -> Vec<u8> {
        yasna::construct_der(|writer| {
            writer.write_sequence(|writer| {
                writer.next().write_bytes(&self.quote);
            })
        })
    }

    pub fn to_cert_extension(&self) -> rcgen::CustomExtension {
        let mut ext = rcgen::CustomExtension::from_oid_content(
            Self::OID,
            self.to_der_bytes(),
        );
        ext.set_criticality(Self::is_critical());
        ext
    }
}

impl SgxAttestationExtension<'static> {
    /// Build a dummy attestation for testing on non-SGX platforms.
    pub fn dummy(cert_pk: &ed25519::PublicKey) -> Self {
        // Use a dummy report as the 'quote'.
        let mut report = enclave::report().clone();

        // Insert the cert pk into the first 32 bytes of the `reportdata` field,
        // See `ReportData::from_cert_pk` in `attestation::quote::quote_enclave`
        let report_data = ReportData::from_cert_pk(cert_pk);
        report.reportdata = report_data.into_inner();

        Self {
            quote: Cow::Owned(AsRef::<[u8]>::as_ref(&report).to_owned()),
        }
    }

    /// Deserialize the attestation from DER bytes.
    pub fn from_der_bytes(buf: &[u8]) -> yasna::ASN1Result<Self> {
        yasna::parse_der(buf, |reader| {
            reader.read_sequence(|reader| {
                let quote = reader.next().read_bytes()?;
                Ok(Self {
                    quote: Cow::Owned(quote),
                })
            })
        })
    }
}

impl fmt::Debug for SgxAttestationExtension<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("SgxAttestationExtension")
            .field("quote", &hex::display(&self.quote))
            .finish()
    }
}

/// Additional X509 cert extensions that Intel includes in the PCK leaf cert.
//
// See: [Intel SGX PCK Certificate Specification - 1.3.5 p.11](https://download.01.org/intel-sgx/sgx-dcap/1.26/linux/docs/SGX_PCK_Certificate_CRL_Spec-1.4.pdf)
//
// ```
// X509v3 extensions:
//   X509v3 Authority Key Identifier:
//     keyid:<keyid of issuer public key>
//   X509v3 CRL Distribution Points:
//     Full Name:
//       URI: <URL>
//   X509v3 Subject Key Identifier:
//     <keyid of public key>
//   X509v3 Key Usage: critical
//     Digital Signature, Non Repudiation
//   X509v3 Basic Constraints: critical
//     CA:FALSE
//   <SGX Extensions OID>:
//     <PPID OID>: <PPID value>
//     <TCB OID>:
//       <SGX TCB Comp01 SVN OID>: <SGX TCB Comp01 SVN value>
//       <SGX TCB Comp02 SVN OID>: <SGX TCB Comp02 SVN value>
//       ...
//       <SGX TCB Comp16 SVN OID>: <SGX TCB Comp16 SVN value>
//     <PCESVN OID>: <PCESVN value>
//     <CPUSVN OID>: <CPUSVN value>
//     <PCE-ID OID>: <PCE-ID value>
//     <FMSPC OID>: <FMSPC value>
//     <SGX Type OID>: <SGX Type value>
//     <PlatformInstanceID OID>: <PlatformInstanceID value>
//     <Configuration OID>:
//       <Dynamic Platform OID>: <Dynamic Platform flag value>
//       <Cached Keys OID>: <Cached Keys flag value>
//       <SMT Enabled OID>: <SMT Enabled flag value>
// ```
//
// | name           | oid                        | type               | description                                |
// |----------------|----------------------------|--------------------|--------------------------------------------|
// | SGX Extensions | 1.2.840.113741.1.13.1      | ASN.1 Sequence     | Sequence of Intel SGX PCK cert extensions  |
// | TCB SVNs       | 1.2.840.113741.1.13.1.2    | ASN.1 Sequence     | Sequence of SGX TCB Component SVNs         |
// | TCB CompXX SVN | 1.2.840.113741.1.13.1.2.XX | ASN.1 Integer      | SGX TCB Component XX SVN                   |
// | PCESVN         | 1.2.840.113741.1.13.1.2.17 | ASN.1 Integer      | Platform Certification Enclave SVN         |
// | CPUSVN         | 1.2.840.113741.1.13.1.2.18 | ASN.1 Octet String | Intel CPU hardware SVN (16 bytes)          |
// | FMSPC          | 1.2.840.113741.1.13.1.4    | ASN.1 Octet String | Value of CPU FMSPC (6 bytes)               |
#[derive(Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct SgxPckExtensions {
    /// This field contains the Intel CPU family of the machine that made the
    /// attestation.
    pub(crate) cpu_fmspc: CpuFmspc,

    /// The Platform Certification Enclave (PCE) SVN.
    #[allow(dead_code)] // Printed in `sgx-test`
    pub(crate) pce_svn: u16,

    /// The 16 SGX TCB component SVNs.
    #[allow(dead_code)] // Printed in `sgx-test`
    pub(crate) sgx_tcb_comp_svns: [u8; 16],
}

impl SgxPckExtensions {
    /// The asn1 OID for the outer "SGX Extensions" sequence.
    #[rustfmt::skip]
    const OID: asn1_rs::Oid<'static> = asn1_rs::oid!(1.2.840.113741.1.13.1);

    /// The asn1 OID prefix for SGX TCB entries in the outer sequence.
    const OID_TCB: &[u64] = &[1, 2, 840, 113741, 1, 13, 1, 2];

    /// The asn1 OID for the inner FMSPC entry in the outer sequence.
    const OID_FMSPC: &[u64] = &[1, 2, 840, 113741, 1, 13, 1, 4];

    /// Dummy values for local dev with no SGX.
    pub(crate) const DUMMY: Self = Self {
        cpu_fmspc: CpuFmspc(hex::decode_const(b"00606a000000")),
        pce_svn: 123,
        sgx_tcb_comp_svns: [42u8; 16],
    };

    /// Try to parse from a whole DER-encoded x509 cert.
    pub(crate) fn from_cert_der(cert_der: &[u8]) -> anyhow::Result<Self> {
        let (unparsed_data, cert) =
            X509Certificate::from_der(cert_der).context("Invalid PCK cert")?;
        ensure!(unparsed_data.is_empty(), "leftover unparsed PCK cert data");

        let cert_ext = cert
            .get_extension_unique(&Self::OID)
            .context("Invalid SGX PCK cert extension")?
            .context("Missing SGX PCK cert extension")?;

        Self::from_der_bytes(cert_ext.value)
            .map_err(|err| anyhow!("Invalid SGX PCK cert extension: {err:#}"))
    }

    /// Try to parse from the x509 cert extension contents.
    ///
    /// These are the bytes contained in the `"SGX Extensions"
    /// (oid=1.2.840.113741.1.13.1)` x509 cert extension.
    fn from_der_bytes(buf: &[u8]) -> yasna::ASN1Result<Self> {
        // Inside `buf` is an ASN1 sequence. We just want the entries for
        // the FMSPC and TCB component SVNs.

        let mut fmspc = None;
        let mut tcb = None;

        yasna::parse_der(buf, |reader| {
            reader.read_sequence_of(|reader| {
                reader.read_sequence(|reader| {
                    let oid = reader.next().read_oid()?;

                    if oid.as_ref() == Self::OID_FMSPC {
                        let bytes = reader.next().read_bytes()?;
                        let bytes = <[u8; 6]>::try_from(bytes.as_slice())
                            .map_err(|_| Self::invalid_asn1())?;
                        if fmspc.replace(bytes).is_some() {
                            return Err(Self::invalid_asn1());
                        }
                    } else if oid.as_ref() == Self::OID_TCB {
                        let new_tcb = Self::read_tcb(reader)?;
                        if tcb.replace(new_tcb).is_some() {
                            return Err(Self::invalid_asn1());
                        }
                    } else {
                        // Skip other extensions we don't care about
                        let _ = reader.next().read_der()?;
                    }

                    Ok(())
                })
            })?;

            let cpu_fmspc = CpuFmspc(fmspc.ok_or_else(Self::invalid_asn1)?);
            let (sgx_tcb_comp_svns, pce_svn) =
                tcb.ok_or_else(Self::invalid_asn1)?;
            Ok(Self {
                cpu_fmspc,
                pce_svn,
                sgx_tcb_comp_svns,
            })
        })
    }

    /// Try to parse the TCB Component SVNs and PCESVN
    fn read_tcb(
        reader: &mut yasna::BERReaderSeq<'_, '_>,
    ) -> yasna::ASN1Result<([u8; 16], u16)> {
        let mut pce_svn = None;
        let mut sgx_tcb_comp_svns = [None; 16];

        reader.next().read_sequence_of(|reader| {
            reader.read_sequence(|reader| {
                let oid = reader.next().read_oid()?;
                let (idx, prefix) =
                    oid.as_ref().split_last().ok_or_else(Self::invalid_asn1)?;
                if prefix != Self::OID_TCB {
                    return Err(Self::invalid_asn1());
                }

                match *idx {
                    // SGX TCB Comp(01..=16)
                    1..=16 => {
                        let idx = (idx - 1) as usize;
                        let svn = reader.next().read_u8()?;
                        if sgx_tcb_comp_svns[idx].replace(svn).is_some() {
                            return Err(Self::invalid_asn1());
                        }
                    }
                    // PCESVN
                    17 => {
                        let svn = reader.next().read_u16()?;
                        if pce_svn.replace(svn).is_some() {
                            return Err(Self::invalid_asn1());
                        }
                    }
                    // Ignore other fields, like CPUSVN
                    _ => {
                        let _ = reader.next().read_der()?;
                    }
                }

                Ok(())
            })
        })?;

        let pce_svn = pce_svn.ok_or_else(Self::invalid_asn1)?;
        let mut svns = [0; 16];
        for (svn, parsed_svn) in svns.iter_mut().zip(sgx_tcb_comp_svns) {
            *svn = parsed_svn.ok_or_else(Self::invalid_asn1)?;
        }

        Ok((svns, pce_svn))
    }

    fn invalid_asn1() -> ASN1Error {
        ASN1Error::new(ASN1ErrorKind::Invalid)
    }
}

/// An Intel CPU FMSPC (Family-Model-Stepping-Platform-Config) code that
/// identifies a family of Intel CPUs
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub(crate) struct CpuFmspc(pub [u8; 6]);

lexe_byte_array::impl_byte_array!(CpuFmspc, 6);
lexe_byte_array::impl_debug_display_as_hex!(CpuFmspc);

#[cfg(test)]
mod test {
    use std::fs;

    use lexe_common::ByteArray;
    use rustls::pki_types::pem::PemObject;

    use super::*;

    #[test]
    fn test_sgx_attestation_ext_oid_der_bytes() {
        let oid = SgxAttestationExtension::oid_yasna();
        let oid_der = yasna::encode_der(&oid);
        assert_eq!(SgxAttestationExtension::OID_DER, &oid_der);
    }

    #[test]
    fn test_sgx_attestation_ext_serde() {
        let ext = SgxAttestationExtension {
            quote: b"test".as_slice().into(),
        };

        let der_bytes = ext.to_der_bytes();
        let ext2 = SgxAttestationExtension::from_der_bytes(&der_bytes).unwrap();
        assert_eq!(ext, ext2);

        let der_bytes2 = ext2.to_der_bytes();
        assert_eq!(der_bytes, der_bytes2);

        assert_eq!(b"test".as_slice(), ext2.quote.as_ref());
    }

    #[test]
    fn test_sgx_pck_extensions_parse() {
        let cert_pem = fs::read_to_string("test_data/pck_cert.pem").unwrap();
        let cert_der = rustls::pki_types::CertificateDer::from_pem_slice(
            cert_pem.as_bytes(),
        )
        .unwrap();

        let actual = SgxPckExtensions::from_cert_der(&cert_der).unwrap();
        let expected = SgxPckExtensions {
            cpu_fmspc: CpuFmspc::from_hex("00606a000000").unwrap(),
            pce_svn: 13,
            sgx_tcb_comp_svns: [
                14, 14, 3, 3, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            ],
        };
        assert_eq!(actual, expected)
    }
}