diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 45832de5..0568c2fc 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -10,7 +10,7 @@ ENV LIBTOOL_VER=2.4.7 ENV LIBXML2_VER=2.9.14 ENV LIBXSLT_VER=1.1.39 ENV XMLSEC_VER=1.3.9 -#ENV TPM2TSS_VER="4.1.3" +ENV TPM2TSS_VER=4.1.3 RUN apt-get update \ && apt-get install -qyy --no-install-recommends \ @@ -33,6 +33,7 @@ ENV SYSROOT="/x86_64-sysroot" ENV PKG_CONFIG_ALLOW_CROSS=1 ENV PKG_CONFIG_PATH="${SYSROOT}/lib/pkgconfig:${SYSROOT}/share/pkgconfig" ENV PKG_CONFIG_SYSROOT_DIR="$SYSROOT" +ENV LD_LIBRARY_PATH="${SYSROOT}/lib" RUN wget \ -O/tmp/openssl-${OPENSSL_VER}.tar.gz \ @@ -131,22 +132,35 @@ RUN \ /tmp/xmlsec1-${XMLSEC_VER} \ /tmp/xmlsec1-${XMLSEC_VER}.tar.gz -# RUN git clone \ -# https://github.com/tpm2-software/tpm2-tss.git \ -# -b ${TPM2TSS_VER} --depth 1 /tmp/tpm2-tss \ -# && cd /tmp/tpm2-tss \ -# && ./bootstrap \ -# && ./configure \ -# --disable-fapi \ -# --disable-policy \ -# --prefix="${SYSROOT}" \ -# --target=${TARGET_TRIPLE}gnu \ -# --host=${TARGET_TRIPLE}gnu \ -# CC=/usr/bin/${TARGET_TRIPLE}gnu-gcc \ -# CXX=/usr/bin/${TARGET_TRIPLE}gnu-cpp \ -# && make -j$(nproc) \ -# && make install \ -# && rm -rf /tmp/tpm2-tss +RUN \ + wget \ + https://github.com/tpm2-software/tpm2-tss/releases/download/${TPM2TSS_VER}/tpm2-tss-${TPM2TSS_VER}.tar.gz \ + -O /tmp/tpm2-tss-${TPM2TSS_VER}.tar.gz \ + && cd /tmp \ + && tar xvf tpm2-tss-${TPM2TSS_VER}.tar.gz \ + && cd tpm2-tss-${TPM2TSS_VER} \ + && CFLAGS=-fPIC ./configure \ + --prefix=${SYSROOT} \ + --host=${TARGET_TRIPLE}gnu \ + --with-crypto=ossl \ + --disable-fapi \ + --disable-policy \ + --disable-doxygen-doc \ + --enable-static \ + --enable-shared \ + CC=/usr/bin/${TARGET_TRIPLE}gnu-gcc \ + CXX=/usr/bin/${TARGET_TRIPLE}gnu-g++ \ + && make -j$(nproc) \ + && make install \ + && test -f ${SYSROOT}/lib/libtss2-esys.a \ + && test -f ${SYSROOT}/lib/pkgconfig/tss2-esys.pc \ + && test -f ${SYSROOT}/lib/libtss2-tcti-device.so \ + && test -f ${SYSROOT}/lib/libtss2-tcti-device.so.0 \ + && mv ${SYSROOT}/lib/libtss2-esys.a ${SYSROOT}/lib/libtss2-esys-real.a \ + && printf '%s\n' \ + 'GROUP ( libtss2-esys-real.a libtss2-sys.a libtss2-mu.a libtss2-tctildr.a libtss2-rc.a )' \ + > ${SYSROOT}/lib/libtss2-esys.a \ + && rm -rf /tmp/tpm2-tss-${TPM2TSS_VER} /tmp/tpm2-tss-${TPM2TSS_VER}.tar.gz RUN rustup target add \ ${RUST_TARGET} diff --git a/Cargo.lock b/Cargo.lock index 5c9e8fff..6a63c599 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,7 +180,8 @@ name = "attestation-server" version = "0.1.0" dependencies = [ "anyhow", - "base64 0.22.1", + "azure-attest", + "base64", "configfs-tsm", "env_logger", "hex", @@ -189,6 +190,7 @@ dependencies = [ "nvat", "nvidia-attest", "nvml-wrapper", + "reqwest 0.13.4", "rocket", "serde", "serde_json", @@ -226,6 +228,31 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "azure-attest" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "hex", + "jsonwebtoken", + "libattest", + "rsa 0.10.0-rc.18", + "serde", + "serde_json", + "sha2 0.11.0", + "snp-attest", + "tdx-attest", + "thiserror 2.0.18", + "tokio", + "tpm2-protocol", + "tss-esapi", + "wasm-bindgen", + "wasm-bindgen-futures", + "x509-cert 0.3.0-rc.4", + "zerocopy", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -233,10 +260,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] -name = "base64" -version = "0.21.7" +name = "base16ct" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "base64" @@ -276,12 +303,6 @@ dependencies = [ "syn", ] -[[package]] -name = "bitfield" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" - [[package]] name = "bitfield" version = "0.19.4" @@ -317,6 +338,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -465,6 +495,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "cobs" version = "0.3.0" @@ -555,6 +591,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -585,6 +627,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.2", + "hybrid-array", + "num-traits", + "rand_core 0.10.0", + "serdect", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -595,6 +654,37 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.2", + "hybrid-array", + "rand_core 0.10.0", +] + +[[package]] +name = "crypto-primes" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f" +dependencies = [ + "crypto-bigint 0.7.5", + "rand_core 0.10.0", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -604,7 +694,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -773,12 +863,24 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid 0.9.6", - "crypto-common", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "dirs" version = "6.0.0" @@ -824,21 +926,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der 0.7.10", - "digest", - "elliptic-curve", - "rfc6979", - "signature", + "digest 0.10.7", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", "spki 0.7.3", ] +[[package]] +name = "ecdsa" +version = "0.17.0-rc.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c72d1455753a703ad4b90ed2a759f2bc4562024a303176439cf6e593b5ade4" +dependencies = [ + "der 0.8.0", + "digest 0.11.3", + "elliptic-curve 0.14.0", + "rfc6979 0.6.0-pre.0", + "signature 3.0.0", + "spki 0.8.0", + "zeroize", +] + [[package]] name = "ed25519" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", ] [[package]] @@ -850,7 +967,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -867,17 +984,38 @@ version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", + "base16ct 0.2.0", + "crypto-bigint 0.5.5", + "digest 0.10.7", + "ff 0.13.1", "generic-array", - "group", + "group 0.13.0", "hkdf", "pem-rfc7468 0.7.0", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.3", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3273f1195b6f6253ebda493d6742c8baa9b26a291674cd96d92a0f09e90e9b46" +dependencies = [ + "base16ct 1.0.0", + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff 0.14.0", + "group 0.14.0", + "hybrid-array", + "pem-rfc7468 1.0.0", + "pkcs8 0.11.0", + "rand_core 0.10.0", + "sec1 0.8.1", "subtle", "zeroize", ] @@ -978,6 +1116,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.0", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1219,11 +1367,22 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.1", "rand_core 0.6.4", "subtle", ] +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff 0.14.0", + "rand_core 0.10.0", + "subtle", +] + [[package]] name = "h2" version = "0.3.27" @@ -1304,7 +1463,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -1313,7 +1472,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -1389,6 +1557,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + [[package]] name = "hyper" version = "0.14.32" @@ -1472,7 +1651,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-util", @@ -1798,20 +1977,20 @@ version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ - "base64 0.22.1", + "base64", "ed25519-dalek", "getrandom 0.2.16", - "hmac", + "hmac 0.12.1", "js-sys", - "p256", + "p256 0.13.2", "p384", "pem", "rand 0.8.5", - "rsa", + "rsa 0.9.10", "serde", "serde_json", - "sha2", - "signature", + "sha2 0.10.9", + "signature 2.2.0", "simple_asn1", ] @@ -1835,14 +2014,25 @@ name = "libattest" version = "0.1.0" dependencies = [ "anyhow", + "der 0.8.0", + "digest 0.11.3", "getrandom 0.4.2", "hex", "js-sys", + "p256 0.14.0-rc.14", "regorus", + "rsa 0.10.0-rc.18", + "sec1 0.8.1", "serde", "serde-value", + "sha2 0.11.0", + "signature 3.0.0", + "spki 0.8.0", + "thiserror 2.0.18", "wasm-bindgen", "wasm-bindgen-futures", + "web-time", + "x509-cert 0.3.0-rc.4", ] [[package]] @@ -2149,7 +2339,7 @@ dependencies = [ "reqwest 0.12.28", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "thiserror 2.0.18", "tokio", "wasm-bindgen", @@ -2274,10 +2464,23 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", + "sha2 0.10.9", +] + +[[package]] +name = "p256" +version = "0.14.0-rc.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c855a8d2ffd346aa03122626f22e96e3aa75e3bfe64e6bf6cb82f71821ed6ae7" +dependencies = [ + "ecdsa 0.17.0-rc.22", + "elliptic-curve 0.14.0", + "primefield", + "primeorder 0.14.0-rc.14", + "sha2 0.11.0", ] [[package]] @@ -2286,10 +2489,10 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", + "sha2 0.10.9", ] [[package]] @@ -2344,7 +2547,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64 0.22.1", + "base64", "serde_core", ] @@ -2374,9 +2577,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "picky-asn1" -version = "0.8.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295eea0f33c16be21e2a98b908fdd4d73c04dd48c8480991b76dbcf0cb58b212" +checksum = "2ff038f9360b934342fb3c0a1d6e82c438a2624b51c3c6e3e6d7cf252b6f3ee3" dependencies = [ "oid", "serde", @@ -2385,9 +2588,9 @@ dependencies = [ [[package]] name = "picky-asn1-der" -version = "0.4.1" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df7873a9e36d42dadb393bea5e211fe83d793c172afad5fb4ec846ec582793f" +checksum = "d413165e4bf7f808b9a27cbaba657657a2921f0965db833f488c4d4be96dcd2e" dependencies = [ "picky-asn1", "serde", @@ -2396,11 +2599,11 @@ dependencies = [ [[package]] name = "picky-asn1-x509" -version = "0.12.0" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5f20f71a68499ff32310f418a6fad8816eac1a2859ed3f0c5c741389dd6208" +checksum = "859d4117bd1b1dc5646359ee7243c50c5000c0920ea2d1fb120335a2f4c684b8" dependencies = [ - "base64 0.21.7", + "base64", "oid", "picky-asn1", "picky-asn1-der", @@ -2420,10 +2623,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ "der 0.7.10", - "pkcs8", + "pkcs8 0.10.2", "spki 0.7.3", ] +[[package]] +name = "pkcs1" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" +dependencies = [ + "der 0.8.0", + "spki 0.8.0", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -2434,6 +2647,16 @@ dependencies = [ "spki 0.7.3", ] +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.0", + "spki 0.8.0", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2501,13 +2724,39 @@ dependencies = [ "syn", ] +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint 0.7.5", + "crypto-common 0.2.2", + "ff 0.14.0", + "rand_core 0.10.0", + "subtle", + "zeroize", +] + [[package]] name = "primeorder" version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.13.8", +] + +[[package]] +name = "primeorder" +version = "0.14.0-rc.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e56e6d67fdf5744e9e245ae571450fe584b91f5af261d0e40163b618e53a1f6" +dependencies = [ + "elliptic-curve 0.14.0", + "once_cell", + "primefield", + "serdect", ] [[package]] @@ -2768,6 +3017,7 @@ dependencies = [ "parking_lot", "postcard", "rand 0.10.0", + "regex", "serde", "serde_json", "spin 0.10.0", @@ -2780,7 +3030,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", "futures-channel", @@ -2818,14 +3068,16 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", + "futures-util", "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", @@ -2859,9 +3111,10 @@ dependencies = [ [[package]] name = "reticle" -version = "0.4.8" +version = "0.5.0" dependencies = [ "async-once-cell", + "azure-attest", "bytes", "console_error_panic_hook", "futures", @@ -2885,10 +3138,20 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] +[[package]] +name = "rfc6979" +version = "0.6.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9935425142ac6e252364413291d96c8bc9898d0876a801824c7af4eae397b689" +dependencies = [ + "ctutils", + "hmac 0.13.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -2992,19 +3255,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid 0.9.6", - "digest", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", - "pkcs1", - "pkcs8", + "pkcs1 0.7.5", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", + "signature 2.2.0", "spki 0.7.3", "subtle", "zeroize", ] +[[package]] +name = "rsa" +version = "0.10.0-rc.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" +dependencies = [ + "const-oid 0.10.2", + "crypto-bigint 0.7.5", + "crypto-primes", + "digest 0.11.3", + "pkcs1 0.8.0-rc.4", + "pkcs8 0.11.0", + "rand_core 0.10.0", + "serde", + "serdect", + "signature 3.0.0", + "spki 0.8.0", + "zeroize", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3165,10 +3448,24 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", + "base16ct 0.2.0", "der 0.7.10", "generic-array", - "pkcs8", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct 1.0.0", + "ctutils", + "der 0.8.0", + "hybrid-array", "subtle", "zeroize", ] @@ -3296,13 +3593,23 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct 1.0.0", + "serde", +] + [[package]] name = "sev" version = "7.1.0" source = "git+https://github.com/BRA1L0R/sev?branch=prem#a0efc6be4c961cc614af1c2a11baa6fbce6c0f53" dependencies = [ - "base64 0.22.1", - "bitfield 0.19.4", + "base64", + "bitfield", "bitflags", "byteorder", "dirs", @@ -3311,11 +3618,11 @@ dependencies = [ "lazy_static", "libc", "p384", - "rsa", + "rsa 0.9.10", "serde", "serde-big-array", "serde_bytes", - "sha2", + "sha2 0.10.9", "static_assertions", "uuid", "x509-cert 0.2.5", @@ -3329,7 +3636,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -3363,10 +3681,20 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.0", +] + [[package]] name = "simple_asn1" version = "0.6.4" @@ -3403,12 +3731,12 @@ dependencies = [ "log", "p384", "reqwest 0.12.28", - "rsa", + "rsa 0.9.10", "serde", "serde_json", "sev", - "sha2", - "signature", + "sha2 0.10.9", + "signature 2.2.0", "thiserror 2.0.18", "tss-esapi", "url", @@ -3463,9 +3791,9 @@ dependencies = [ [[package]] name = "spki" -version = "0.8.0-rc.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", "der 0.8.0", @@ -3581,14 +3909,13 @@ dependencies = [ "der 0.8.0", "hex", "libattest", - "p256", - "reqwest 0.13.2", - "sec1", + "reqwest 0.13.4", + "sec1 0.7.3", "serde", "serde_json", - "sha2", - "signature", - "spki 0.8.0-rc.4", + "sha2 0.10.9", + "signature 3.0.0", + "spki 0.8.0", "thiserror 2.0.18", "tokio", "urlencoding", @@ -3896,6 +4223,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tpm2-protocol" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75452b7cfe993c1867e27ccbc9cd9bc2f5608fc6f142b592448b5fa089cfa73d" + [[package]] name = "tracing" version = "0.1.44" @@ -3965,13 +4298,13 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tss-esapi" -version = "7.6.0" +version = "7.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ea9ccde878b029392ac97b5be1f470173d06ea41d18ad0bb3c92794c16a0f2" +checksum = "3f10b25a84912b894d0e6d68f4a3771c923e9c44ddaaed7920cde92ed28aa84e" dependencies = [ - "bitfield 0.14.0", + "bitfield", "enumflags2", - "getrandom 0.2.16", + "getrandom 0.4.2", "hostname-validator", "log", "mbox", @@ -3988,9 +4321,9 @@ dependencies = [ [[package]] name = "tss-esapi-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535cd192581c2ec4d5f82e670b1d3fbba6a23ccce8c85de387642051d7cad5b5" +checksum = "a7f972672926a3d3d18ecc04524720e4d20b7d1664a3fb73dbf7d4274196dbd9" dependencies = [ "pkg-config", "target-lexicon", @@ -3998,9 +4331,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ubyte" @@ -4760,7 +5093,7 @@ checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ "const-oid 0.9.6", "der 0.7.10", - "signature", + "signature 2.2.0", "spki 0.7.3", "tls_codec", ] @@ -4773,7 +5106,7 @@ checksum = "1e21aad3a769f25f3d2d0cbf30ea8b50a1d602354bd6ab687fad112821608ba6" dependencies = [ "const-oid 0.10.2", "der 0.8.0", - "spki 0.8.0-rc.4", + "spki 0.8.0", "tls_codec", ] diff --git a/Cargo.toml b/Cargo.toml index 48360413..2626711d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["libattest", "nvidia-attest", "reticle", "snp-attest", "tdx-attest", "attestation-server"] +members = ["libattest", "nvidia-attest", "reticle", "snp-attest", "tdx-attest", "attestation-server", "azure-attest"] [patch.crates-io] sev = { git = "https://github.com/BRA1L0R/sev", branch = "prem" } diff --git a/attestation-server/Cargo.toml b/attestation-server/Cargo.toml index cab23970..d512c4c0 100644 --- a/attestation-server/Cargo.toml +++ b/attestation-server/Cargo.toml @@ -19,9 +19,11 @@ nvat = { git = "ssh://git@github.com/prem-research/nvat-rs.git", branch = "main" libattest = { path = "../libattest" } nvidia-attest = { path = "../nvidia-attest" } +azure-attest = { path = "../azure-attest", features = ["host"] } snp-attest = { path = "../snp-attest", default-features = false } env_logger = "0.11.8" log = "0.4.29" configfs-tsm = { version = "0.0.2" } nvml-wrapper = "0.12.1" +reqwest = { version = "0.13.4", features = ["blocking", "json"] } diff --git a/attestation-server/src/azure_api.rs b/attestation-server/src/azure_api.rs new file mode 100644 index 00000000..c61a5125 --- /dev/null +++ b/attestation-server/src/azure_api.rs @@ -0,0 +1,96 @@ +use std::{ + borrow::Cow, + path::{Path, PathBuf}, +}; + +use azure_attest::{AzureQuote, host::vtpm::AzureTpmCtx, nonce::AzureNonce}; +use rocket::{ + Build, Rocket, State, + fairing::{Fairing, Kind}, +}; + +use crate::{nonce::NonceParam, response::ApiJsonResult}; + +#[derive(Clone)] +pub struct TpmDevice { + tpm_path: Cow<'static, Path>, +} + +impl TpmDevice { + const TPM_PATH: &'static str = "/dev/tpm0"; + const TPMRM_PATH: &'static str = "/dev/tpmrm0"; + + pub fn with_path(device: impl Into) -> Self { + Self { + tpm_path: device.into().into(), + } + } + + pub fn auto_detect() -> anyhow::Result { + let tpm_path = Path::new(Self::TPM_PATH); + let tpmrm_path = Path::new(Self::TPMRM_PATH); + + let tpm_path = if tpmrm_path.exists() { + tpmrm_path.into() + } else if tpm_path.exists() { + tpm_path.into() + } else { + anyhow::bail!("Could not find a tpm device at /dev/tpm0 or /dev/tpmrm0"); + }; + + Ok(Self { tpm_path }) + } +} + +impl TpmDevice { + pub fn create_context(&self) -> anyhow::Result { + let ctx = AzureTpmCtx::with_device(&self.tpm_path)?; + Ok(ctx) + } +} + +pub struct AzureFairing { + device: TpmDevice, +} + +impl AzureFairing { + pub fn new(device: TpmDevice) -> Self { + Self { device } + } +} + +#[rocket::async_trait] +impl Fairing for AzureFairing { + fn info(&self) -> rocket::fairing::Info { + rocket::fairing::Info { + name: "Azure TPM Fairing", + kind: Kind::Ignite, + } + } + + async fn on_ignite(&self, rocket: Rocket) -> Result, Rocket> { + let res_context = self.device.create_context(); + if let Err(error) = res_context { + let device = self.device.tpm_path.as_ref(); + log::error!("Creating a tpm context with [{device:?}] failed with error: {error}"); + + return Err(rocket); + } + + Ok(rocket.manage(self.device.clone())) + } +} + +#[rocket::get("/azure?")] +pub async fn azure_attestation( + nonce: NonceParam, 32>, + tpm: &State, +) -> ApiJsonResult { + let NonceParam(nonce) = nonce; + let nonce = AzureNonce::new(nonce); + + let tpm = tpm.create_context()?; + let quote = azure_attest::host::azure_attest(tpm, &nonce)?; + + Ok(quote.into()) +} diff --git a/attestation-server/src/main.rs b/attestation-server/src/main.rs index 55b5b829..6e089419 100644 --- a/attestation-server/src/main.rs +++ b/attestation-server/src/main.rs @@ -1,6 +1,7 @@ mod response; // pub mod modules; +mod azure_api; pub mod modules; mod nonce; mod nvidia_api; @@ -16,18 +17,23 @@ use rocket::{State, catch, catchers, routes}; use sev::firmware::guest::Firmware; use tokio::sync::Mutex; -use crate::{modules::ModuleDetector, nvidia_api::SdkFairing, response::ApiJsonResult}; - -#[catch(404)] -fn not_found() -> &'static str { - "" -} +use crate::{ + azure_api::{AzureFairing, TpmDevice}, + modules::ModuleDetector, + nvidia_api::NvidiaFairing, + response::ApiJsonResult, +}; #[rocket::get("/modules")] fn get_modules(modules: &State) -> ApiJsonResult<&Modules> { response::ok(modules.deref()) } +#[catch(404)] +fn not_found() -> &'static str { + "route not found" +} + #[tokio::main] async fn main() -> Result<(), anyhow::Error> { env_logger::builder() @@ -57,13 +63,20 @@ async fn main() -> Result<(), anyhow::Error> { routes.extend(routes![tdx_api::tdx_attestation]); rocket } + CpuModule::Azure => { + let tpm = TpmDevice::auto_detect().context("could not detect default tpm device")?; + let tpm = AzureFairing::new(tpm); + + routes.extend(routes![azure_api::azure_attestation]); + rocket.attach(tpm) + } _ => bail!("cpu module not yet supported by attestation-server"), }; if let Some(GpuModule::Nvidia) = modules.gpu() { // attach the nvidia fairing responsible for first attestation // and enable gpus for confidential computing operations - let sdk = SdkFairing::init()?; + let sdk = NvidiaFairing::init()?; rocket = rocket.attach(sdk); routes.extend(routes![nvidia_api::nvidia_attestation]); diff --git a/attestation-server/src/modules.rs b/attestation-server/src/modules.rs index 38f80aa3..6b563087 100644 --- a/attestation-server/src/modules.rs +++ b/attestation-server/src/modules.rs @@ -1,6 +1,38 @@ use anyhow::Context; use libattest::{CpuModule, GpuModule, Modules, ModulesBuilder}; -use std::path::Path; +use serde::Deserialize; +use std::{path::Path, time::Duration}; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SecurityProfile { + security_type: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ComputeMetadata { + // name: String, + security_profile: SecurityProfile, +} + +struct AzureDetector; + +impl AzureDetector { + pub fn fetch_imds() -> anyhow::Result { + let res: ComputeMetadata = reqwest::blocking::ClientBuilder::new() + .timeout(Duration::from_secs(2)) + .build()? + .get("http://169.254.169.254/metadata/instance/compute") + .header("Metadata", "true") + .query(&[("api-version", "2025-04-07")]) + .send()? + .error_for_status()? + .json()?; + + Ok(res) + } +} pub struct ModuleDetector; @@ -13,11 +45,29 @@ impl ModuleDetector { path.as_ref().exists() } + fn detect_azure(&self) -> bool { + log::info!("Trying to detect for Azure..."); + let imds = AzureDetector::fetch_imds(); + + let imds = match imds { + Ok(imds) => imds, + Err(err) => { + log::error!("Got error from imds: {err}"); + return false; + } + }; + + log::debug!("Got security_type {}", imds.security_profile.security_type); + (imds.security_profile.security_type == "ConfidentialVM") + } + fn detect_cpu(&self) -> Option { if self.path_exists(Self::SEV_PATH) { Some(CpuModule::Sev) } else if self.path_exists(Self::TDX_PATH) { Some(CpuModule::Tdx) + } else if self.detect_azure() { + Some(CpuModule::Azure) } else { None } diff --git a/attestation-server/src/nvidia_api.rs b/attestation-server/src/nvidia_api.rs index 678112fc..e83b0e4b 100644 --- a/attestation-server/src/nvidia_api.rs +++ b/attestation-server/src/nvidia_api.rs @@ -1,9 +1,11 @@ -use anyhow::{Context, bail}; +use std::collections::HashSet; +use std::ops::Deref; + +use anyhow::Context; use nvat::{AttestationBuilder, SdkHandle, nonce::NvatNonce}; use nvidia_attest::EATToken; use nvidia_attest::keychain::KeyChain; use nvidia_attest::nonce::NvidiaNonce; -use nvidia_attest::types::GpuClaims; use nvml_wrapper::Nvml; use rocket::fairing::{Fairing, Info, Kind}; use rocket::{Build, Rocket, State}; @@ -11,17 +13,17 @@ use rocket::{Build, Rocket, State}; use crate::nonce::NonceParam; use crate::response::ApiError; -pub struct SdkFairing { +pub struct NvidiaFairing { handle: SdkHandle, nvml: Nvml, } -impl SdkFairing { +impl NvidiaFairing { pub fn init() -> anyhow::Result { let handle = SdkHandle::get_handle()?; let nvml = Nvml::init()?; - Ok(SdkFairing { handle, nvml }) + Ok(NvidiaFairing { handle, nvml }) } async fn attest_and_init(&self) -> anyhow::Result<()> { @@ -44,35 +46,40 @@ impl SdkFairing { // we gather the device uuids from the claims of the attested gpus // so we don't accidentally turn on confidential computing for // gpus we didn't explicitly verify - let attested_gpus: Vec<&GpuClaims> = claims.gpu_claims().values().collect(); - let available_gpus: Vec = (0..self.nvml.device_count()?) - .map(|idx| self.nvml.device_by_index(idx)) - .collect::>()?; - - if attested_gpus.len() != available_gpus.len() { - let attested_gpus = attested_gpus.len(); - let available_gpus = available_gpus.len(); - - bail!( - "there's a mismatch between the number of available gpus {available_gpus} on this machine and the attested number of gpus {attested_gpus}" - ); + let uuids: HashSet = claims + .gpu_claims() + .values() + .map(|gpu| &gpu.ueid) + .cloned() + .collect(); + + for uuid in uuids { + let device = self.nvml.device_by_uuid(uuid.deref()).with_context(|| { + format!("Device with UUID: {uuid} was attested but not found by nvml") + })?; + + // once we attest the device we can activate + // confidential compute state + device + .set_confidential_compute_state(true) + .with_context(|| { + format!("cannot activate confidential computing for gpu uuid:{uuid}") + })?; } - let gpu = available_gpus - .first() - .context("No GPUs available in the system for confidential computing workloads")?; - - // nvml wrapper library is broken and calling this method - // on the device enables it systemwide. It should be - // self.nvml.set_confidential_compute_state(true); - gpu.set_confidential_compute_state(true)?; + let count = self.nvml.device_count()? as usize; + if count != claims.gpu_claims().iter().count() { + log::warn!( + "there are still devices on this machine for which confidential computing wasn't enabled." + ) + } Ok(()) } } #[rocket::async_trait] -impl Fairing for SdkFairing { +impl Fairing for NvidiaFairing { fn info(&self) -> rocket::fairing::Info { Info { name: "Nvidia SDK initializer", diff --git a/azure-attest/Cargo.toml b/azure-attest/Cargo.toml new file mode 100644 index 00000000..d2a14880 --- /dev/null +++ b/azure-attest/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "azure-attest" +version = "0.1.0" +edition = "2024" + +[features] +host = ["dep:tss-esapi", "dep:anyhow"] +cli = ["dep:tss-esapi", "dep:anyhow"] + +[dependencies] +tpm2-protocol = "0.16.0" +snp-attest = { path = "../snp-attest" } +tdx-attest = { path = "../tdx-attest" } +x509-cert = "0.3.0-rc.4" +rsa = { version = "0.10.0-rc.18", features = ["serde"] } +libattest = { path = "../libattest" } +jsonwebtoken = { version = "10.3.0", default-features = false } +serde = { version = "1.0.228", features = ["derive"] } +sha2 = "0.11.0" +serde_json = "1.0.149" +thiserror = "2.0.17" +zerocopy = { version = "0.8.48", features = ["derive"] } + +tss-esapi = { version = "7.7.0", optional = true } +anyhow = { version = "*", optional = true } +base64 = "0.22.1" +hex = { version = "0.4.3", features = ["serde"] } + +[target.'cfg(target_family = "wasm")'.dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" + +[dev-dependencies] +tokio = { version = "1.49.0", features = ["full"] } + +[[bin]] +name = "generate-report" +path = "src/bin/generate.rs" +required-features = ["cli"] diff --git a/azure-attest/README.md b/azure-attest/README.md new file mode 100644 index 00000000..90f5aa9a --- /dev/null +++ b/azure-attest/README.md @@ -0,0 +1,4 @@ +vtpm2 verification consists of 3 different reads on the machine side: +- standard tpm 2.0 quote +- ak endorsement key and ak certificate from the host +- hardware report attesting the ak endorsement key diff --git a/azure-attest/src/bin/generate.rs b/azure-attest/src/bin/generate.rs new file mode 100644 index 00000000..c2158711 --- /dev/null +++ b/azure-attest/src/bin/generate.rs @@ -0,0 +1,12 @@ +use azure_attest::{host::vtpm::AzureTpmCtx, nonce::AzureNonce}; + +fn main() { + let tpm = AzureTpmCtx::default_device().unwrap(); + let nonce = AzureNonce::generate(); + + let attestation = azure_attest::host::azure_attest(tpm, &nonce).unwrap(); + + let attestation = serde_json::to_string(&attestation).unwrap(); + + println!("{attestation}"); +} diff --git a/azure-attest/src/ca/Azure.crt b/azure-attest/src/ca/Azure.crt new file mode 100644 index 00000000..35a0eac3 --- /dev/null +++ b/azure-attest/src/ca/Azure.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQUfQx2iySCIpOKeDZKd5KpzANBgkqhkiG9w0BAQwFADBp +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTow +OAYDVQQDEzFBenVyZSBWaXJ0dWFsIFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhv +cml0eSAyMDIzMB4XDTIzMDYwMTE4MDg1M1oXDTQ4MDYwMTE4MTU0MVowaTELMAkG +A1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE6MDgGA1UE +AxMxQXp1cmUgVmlydHVhbCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkg +MjAyMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALoMMwvdRJ7+bW00 +adKE1VemNqJS+268Ure8QcfZXVOsVO22+PL9WRoPnWo0r5dVoomYGbobh4HC72s9 +sGY6BGRe+Ui2LMwuWnirBtOjaJ34r1ZieNMcVNJT/dXW5HN/HLlm/gSKlWzqCEx6 +gFFAQTvyYl/5jYI4Oe05zJ7ojgjK/6ZHXpFysXnyUITJ9qgjn546IJh/G5OMC3mD +fFU7A/GAi+LYaOHSzXj69Lk1vCftNq9DcQHtB7otO0VxFkRLaULcfu/AYHM7FC/S +q6cJb9Au8K/IUhw/5lJSXZawLJwHpcEYzETm2blad0VHsACaLNucZL5wBi8GEusQ +9Wo8W1p1rUCMp89pufxa3Ar9sYZvWeJlvKggWcQVUlhvvIZEnT+fteEvwTdoajl5 +qSvZbDPGCPjb91rSznoiLq8XqgQBBFjnEiTL+ViaZmyZPYUsBvBY3lKXB1l2hgga +hfBIag4j0wcgqlL82SL7pAdGjq0Fou6SKgHnkkrV5CNxUBBVMNCwUoj5mvEjd5mF +7XPgfM98qNABb2Aqtfl+VuCkU/G1XvFoTqS9AkwbLTGFMS9+jCEU2rw6wnKuGv1T +x9iuSdNvsXt8stx4fkVeJvnFpJeAIwBZVgKRSTa3w3099k0mW8qGiMnwCI5SfdZ2 +SJyD4uEmszsnieE6wAWd1tLLg1jvAgMBAAGjVDBSMA4GA1UdDwEB/wQEAwIBhjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRL/iZalMH2M8ODSCbd8+WwZLKqlTAQ +BgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwFAAOCAgEALgNAyg8I0ANNO/8I +2BhpTOsbywN2YSmShAmig5h4sCtaJSM1dRXwA+keY6PCXQEt/PRAQAiHNcOF5zbu +OU1Bw/Z5Z7k9okt04eu8CsS2Bpc+POg9js6lBtmigM5LWJCH1goMD0kJYpzkaCzx +1TdD3yjo0xSxgGhabk5Iu1soD3OxhUyIFcxaluhwkiVINt3Jhy7G7VJTlEwkk21A +oOrQxUsJH0f2GXjYShS1r9qLPzLf7ykcOm62jHGmLZVZujBzLIdNk1bljP9VuGW+ +cISBwzkNeEMMFufcL2xh6s/oiUnXicFWvG7E6ioPnayYXrHy3Rh68XLnhfpzeCzv +bz/I4yMV38qGo/cAY2OJpXUuuD/ZbI5rT+lRBEkDW1kxHP8cpwkRwGopV8+gX2KS +UucIIN4l8/rrNDEX8T0b5U+BUqiO7Z5YnxCya/H0ZIwmQnTlLRTU2fW+OGG+xyIr +jMi/0l6/yWPUkIAkNtvS/yO7USRVLPbtGVk3Qre6HcqacCXzEjINcJhGEVg83Y8n +M+Y+a9J0lUnHytMSFZE85h88OseRS2QwqjozUo2j1DowmhSSUv9Na5Ae22ycciBk +EZSq8a4rSlwqthaELNpeoTLUk6iVoUkK/iLvaMvrkdj9yJY1O/gvlfN2aiNTST/2 +bd+PA4RBToG9rXn6vNkUWdbLibU= +-----END CERTIFICATE----- diff --git a/azure-attest/src/ca/ICA01.crt b/azure-attest/src/ca/ICA01.crt new file mode 100644 index 00000000..ad2abe31 --- /dev/null +++ b/azure-attest/src/ca/ICA01.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFnDCCA4SgAwIBAgITMwAAAALA0XtLj5ecNQAAAAAAAjANBgkqhkiG9w0BAQwF +ADBpMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTowOAYDVQQDEzFBenVyZSBWaXJ0dWFsIFRQTSBSb290IENlcnRpZmljYXRlIEF1 +dGhvcml0eSAyMDIzMB4XDTIzMDYwODE3NTMwNFoXDTI1MTEwMzE3NTMwNFowJTEj +MCEGA1UEAxMaR2xvYmFsIFZpcnR1YWwgVFBNIENBIC0gMDEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC/NqouHGBovTadw1GLMQYNxWrEciPSh7gE7Vxs +fbbPCE0SfTO80OF2raLdRYN2gEWE2Dr8+TDUuAb/WBFyczhU1aHFPssg8B3/DT6p +TXlDlohLLPWkjYU+OuT1/Ls7RzjQBe2se/MJyPaIJXI6KgCwePw7EcWFChe8aCTU +MHYBG0Ju4xNlMTUd/tcu6a53CXn6Nq48umwlaJelRh+i1f0vcwB2CY/v+Rliwb/8 +DM5Ed9FUHZOKyp5Vnaw9GWloM46sLQT/fdHB0jmugfNZzafkkhQAYiNL3jYNYFZH +5/IgUfYJ/yybwnwoxOdV2NV0Q2i+P5Pcb0WNGaJY47aqOj8BAgMBAAGjggF/MIIB +ezASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwICBDAXBgNVHSUEEDAO +BgVngQUIAQYFZ4EFCAMwHQYDVR0OBBYEFP/2zueowUhpKMuKS/LYgYG1bYCBMB8G +A1UdIwQYMBaAFEv+JlqUwfYzw4NIJt3z5bBksqqVMHYGA1UdHwRvMG0wa6BpoGeG +ZWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL0F6dXJlJTIwVmly +dHVhbCUyMFRQTSUyMFJvb3QlMjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUyMDIw +MjMuY3JsMIGDBggrBgEFBQcBAQR3MHUwcwYIKwYBBQUHMAKGZ2h0dHA6Ly93d3cu +bWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvQXp1cmUlMjBWaXJ0dWFsJTIwVFBN +JTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMy5jcnQwDQYJ +KoZIhvcNAQEMBQADggIBAEhTwx26W+Xap7zXExbAwnHYtN6kB4dIGXdgQIiQy5OQ +ltsSj2jx7qZ/af5n5OnTBQ+gXM8siVipgaAIdBkbGgOjCb6b6MTe1YpFAH4fQv8e +VwTVDziBMD0EKI30h0JbFKLdSdKe48O9Lw+T2b0PBXvMFJOSdZT7meGIkpNxSqmA +Z+RNyreLxil9knNjF5ymPT0RcGK52+MwGlElBb/jc+snhr+ZJZ1grjFky9NzjCTi +E5SG+6H3YgiHCqfXr0L3NRt/QZ5IgkuGkNPeMvn4JjevFwAhXFxBqJYJ7mY61MJu +WTdyhhoUJgzmZo1hS+GNyuMKaKBLreUwtc1y7LRH3YGGed57HbQ9bmyMdhO7x8KZ +NrBDX2/cRLzrCmpSUldmKMu9G4dpzXpde4pFMObiVFrGRq8/9HMOJwZlQvzhwQp0 +PUMY/gIU5rf23n1M1M36tM5g5CEzxQUGtVaG9ABTJQ2zijD5wDo840vbznyKt3ih +imrUs+LqpPDNXyxbwvibcZidwSdhu0QmUoyYsgSP2Zff5E8ks53h2xQSM3zz2qaW +VS1vVqG4zC0EfRnO65ogPPfrtK6ZiFmVHSWP9vPkFcUNYDnYQXW/TArO/JCe2I++ +GClM7AcDQwWLxcopzskGQdHNM1zMsprRRwYaVpTJH67xeNda6+Y7IOPJYTvyoXHP +-----END CERTIFICATE----- diff --git a/azure-attest/src/ca/ICA03.crt b/azure-attest/src/ca/ICA03.crt new file mode 100644 index 00000000..a8753d05 --- /dev/null +++ b/azure-attest/src/ca/ICA03.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFnDCCA4SgAwIBAgITMwAAAAknQOWscnsOpgAAAAAACTANBgkqhkiG9w0BAQwF +ADBpMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTowOAYDVQQDEzFBenVyZSBWaXJ0dWFsIFRQTSBSb290IENlcnRpZmljYXRlIEF1 +dGhvcml0eSAyMDIzMB4XDTI1MDQyNDE4MDExN1oXDTI3MDQyNDE4MDExN1owJTEj +MCEGA1UEAxMaR2xvYmFsIFZpcnR1YWwgVFBNIENBIC0gMDMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDYGYtis5ka0cxQkhU11jslgX6wzjR/UXQIFdUn +8juTUMJl91VokwUPX3WfXeog7mtbWyYWD8SI0BSnchRGlV8u3AhcW61/HetHqmIL +tD0c75UATi+gsTQnpwKPA/m38MGGyXFETr3xHXjilUPfIhmxO4ImuNJ0R95bZYhx +bLYmOZpVUcj8oz980An8HlIqSzrskQR6NiuEmikHkHc1/CpoNunrr8kQNPF6gxex +IrvXsKLUAuUqnNtcQWc/8Er5EN9+TdX6AOjUmKriVGbCInP1m/aC+DWH/+aJ/8aD +pKze6fe7OHh2BL9hxqIsmJAStIh4siRdLYTt8hKGmkdzOWnRAgMBAAGjggF/MIIB +ezASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwICBDAXBgNVHSUEEDAO +BgVngQUIAQYFZ4EFCAMwHQYDVR0OBBYEFGcJhvj5gV6TrfnJZOcUCtqZywotMB8G +A1UdIwQYMBaAFEv+JlqUwfYzw4NIJt3z5bBksqqVMHYGA1UdHwRvMG0wa6BpoGeG +ZWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL0F6dXJlJTIwVmly +dHVhbCUyMFRQTSUyMFJvb3QlMjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUyMDIw +MjMuY3JsMIGDBggrBgEFBQcBAQR3MHUwcwYIKwYBBQUHMAKGZ2h0dHA6Ly93d3cu +bWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvQXp1cmUlMjBWaXJ0dWFsJTIwVFBN +JTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMy5jcnQwDQYJ +KoZIhvcNAQEMBQADggIBAJPP3Z2z1zhzUS3qSRVgyoUVnaxCGuMHzPQAZuoPBVpz +wKnv4HqyjMgT8pBtQqxkqAsg7KiqbPfO97bMCHcuqkkfHjw8yg6IYt01RjUjVPKq +lrsY2iw7hFWNWr8SGMa10JdNYNyf5dxob5+mKAwEOhLzKNwq9rM/uIvZky77pNly +RLt55XEPfBMYdI9I8uQ5Uqmrw7mVJfERMfTBhSQF9BrcajAsaLcs7qEUyj0yUdJf +cgZkfCoUEUSPr3OwLHaYeV1J6VidhIYsYo53sXXal91d60NspYgei2nJFei/+R3E +SWnGbPBW+EQ4FbvZXxu57zUMX9mM7lC+GoXLvA6/vtKShEi9ZXl2PSnBQ/R2A7b3 +AXyg4fmMLFausEk6OiuU8E/bvp+gPLOJ8YrX7SAJVuEn+koJaK5G7os5DMIh7/KM +l9cI9WxPwqoWjp4VBfrF4hDOCmKWrqtFUDQCML8qD8RTxlQKQtgeGAcNDfoAuL9K +VtSG5/iIhuyBEFYEHa3vRWbSaHCUzaHJsTmLcz4cp1VDdepzqZRVuErBzJKFnBXb +zRNW32EFmcAUKZImIsE5dgB7y7eiijf33VWNfWmK05fxzQziWFWRYlET4SVc3jMn +PBiY3N8BfK8EBOYbLvzo0qn2n3SAmPhYX3Ag6vbbIHd4Qc8DQKHRV0PB8D3jPGmD +-----END CERTIFICATE----- diff --git a/azure-attest/src/ca/intermediate.rs b/azure-attest/src/ca/intermediate.rs new file mode 100644 index 00000000..fae8db6b --- /dev/null +++ b/azure-attest/src/ca/intermediate.rs @@ -0,0 +1,73 @@ +use std::sync::LazyLock; + +use libattest::{ + crypto::algorithms::{CertFormat, rsa::RsaCert}, + error::Context, +}; +use x509_cert::{ + der::DecodePem, + ext::pkix::{AuthorityKeyIdentifier, SubjectKeyIdentifier}, +}; + +pub struct IntermediateDb { + certs: Vec<(SubjectKeyIdentifier, RsaCert)>, +} + +impl IntermediateDb { + fn new() -> Self { + Self { certs: vec![] } + } + + fn add_pem(mut self, pem: &str) -> Self { + let cert = x509_cert::Certificate::from_pem(pem).unwrap(); + let cert = RsaCert::from_certificate(cert).unwrap(); + + let (_, ski) = cert + .certificate() + .get_extension::() + .unwrap() + .expect("missing intermediate SKI from intermediate certificate"); + + self.certs.push((ski, cert)); + self + } + + pub fn find_intermediate(&self, leaf: &RsaCert) -> libattest::Result<&RsaCert> { + let (_, leaf_issuer_id) = leaf + .certificate() + .get_extension::()? + .context("AuthorityKeyIdentifier extension not found in leaf certificate")?; + + let leaf_issuer_id = leaf_issuer_id + .key_identifier + .context("missing key identifier from leaf certificate")?; + + let (_, cert) = self + .certs + .iter() + .find(|(ski, _)| ski.0 == leaf_issuer_id) + .context("couldn't find appropriate intermediate certificate")?; + + Ok(cert) + } +} + +static INTERMEDIATE_DB: LazyLock = LazyLock::new(|| { + IntermediateDb::new() + // .add_pem(include_str!("./ICA01.crt")) + .add_pem(include_str!("./ICA03.crt")) +}); + +pub fn intermediate_db() -> &'static IntermediateDb { + &INTERMEDIATE_DB +} + +#[cfg(test)] +mod test { + use crate::ca::intermediate_db; + + #[test] + fn parse_intermediate_cas() { + let db = intermediate_db(); + } +} diff --git a/azure-attest/src/ca/mod.rs b/azure-attest/src/ca/mod.rs new file mode 100644 index 00000000..d2da5407 --- /dev/null +++ b/azure-attest/src/ca/mod.rs @@ -0,0 +1,27 @@ +mod intermediate; + +use std::sync::LazyLock; + +use libattest::crypto::algorithms::{CertFormat, rsa::RsaCert}; +use x509_cert::{Certificate, der::DecodePem}; + +static AZURE_CA_PEM: &[u8] = include_bytes!("./Azure.crt"); + +pub static AZURE_CA: LazyLock = LazyLock::new(|| { + RsaCert::from_certificate(Certificate::from_pem(AZURE_CA_PEM).unwrap()).unwrap() +}); + +pub use intermediate::intermediate_db; + +#[cfg(test)] +mod test { + use libattest::crypto::algorithms::{CertFormat, rsa::RsaCert}; + + use crate::ca::AZURE_CA; + + #[test] + fn read_parse_ca() { + let ca: &RsaCert = &AZURE_CA; + ca.certificate(); + } +} diff --git a/azure-attest/src/collateral.rs b/azure-attest/src/collateral.rs new file mode 100644 index 00000000..46c44e17 --- /dev/null +++ b/azure-attest/src/collateral.rs @@ -0,0 +1,96 @@ +use snp_attest::{SevQuote, verify::SevQuoteVerifier}; +use tdx_attest::{TdxQuote, verify::TdxQuoteVerifier}; + +use crate::quote::{AzureQuote, ParsedHardwareReport}; + +/// Construct the builder providing two async functions +/// that fetch collateral and build a quote verifier +/// based on the type of hardware report contained in the +/// vtpm report data. +pub struct ReportVerifierBuilder { + fetch_tdx: T, + fetch_sev: S, +} + +impl ReportVerifierBuilder<(), ()> { + pub fn new() -> ReportVerifierBuilder<(), ()> { + ReportVerifierBuilder { + fetch_sev: (), + fetch_tdx: (), + } + } +} + +impl Default for ReportVerifierBuilder<(), ()> { + fn default() -> Self { + Self::new() + } +} + +impl ReportVerifierBuilder { + pub fn tdx(self, fetch_tdx: N) -> ReportVerifierBuilder + where + N: AsyncFnOnce(&TdxQuote) -> libattest::Result, + { + ReportVerifierBuilder { + fetch_tdx, + fetch_sev: self.fetch_sev, + } + } + + pub fn sev(self, fetch_sev: N) -> ReportVerifierBuilder + where + N: AsyncFnOnce(&SevQuote) -> libattest::Result, + { + ReportVerifierBuilder { + fetch_tdx: self.fetch_tdx, + fetch_sev, + } + } +} + +impl ReportVerifierBuilder +where + T: AsyncFnOnce(&TdxQuote) -> libattest::Result, + S: AsyncFnOnce(&SevQuote) -> libattest::Result, +{ + /// Chooses the right function to collect evidence depending + /// on the type of report data (sev or tdx) contained in the azure quote + pub async fn fetch_collateral(self, quote: &AzureQuote) -> libattest::Result { + let report = quote.parse_hardware_report()?; + + let verifier = match report { + ParsedHardwareReport::Tdx(ref tdx_quote) => { + let verifier = (self.fetch_tdx)(tdx_quote).await?; + ReportVerifier::Tdx(Box::new(verifier)) + } + ParsedHardwareReport::Sev(ref sev_quote) => { + let verifier = (self.fetch_sev)(sev_quote).await?; + ReportVerifier::Sev(Box::new(verifier)) + } + }; + + Ok(verifier) + } +} + +pub enum ReportVerifier { + Tdx(Box), + Sev(Box), +} + +impl ReportVerifier { + pub fn tdx(&self) -> Option<&TdxQuoteVerifier> { + match self { + ReportVerifier::Tdx(tdx_quote_verifier) => Some(tdx_quote_verifier), + _ => None, + } + } + + pub fn sev(&self) -> Option<&SevQuoteVerifier> { + match self { + ReportVerifier::Sev(sev) => Some(sev), + _ => None, + } + } +} diff --git a/azure-attest/src/host.rs b/azure-attest/src/host.rs new file mode 100644 index 00000000..57c005be --- /dev/null +++ b/azure-attest/src/host.rs @@ -0,0 +1,36 @@ +use std::ops::Deref; + +use tpm2_protocol::{TpmUnmarshal, data::TpmsAttest}; +use tss_esapi::{structures::Signature, traits::Marshall}; + +use crate::{AzureQuote, nonce::AzureNonce, quote::AzureTrust}; + +pub mod vtpm; + +/// Performs attestation on an AzureTpm object +pub fn azure_attest( + mut tpm: vtpm::AzureTpmCtx, + nonce: &AzureNonce, +) -> anyhow::Result { + let cert = tpm.ak_cert()?; + let key = tpm.ak().unwrap(); + let report = tpm.hardware_report().unwrap(); + let pcr = tpm.read_pcr_bank()?; + let (attest, signature) = tpm.quote(nonce.deref()).unwrap(); + + // convert tpm structure to wire compatible format + let marshaled = attest.marshall()?; + let (attest, _) = TpmsAttest::unmarshal(&marshaled)?; + + let signature = match signature { + Signature::RsaSsa(ref signature) => signature.signature().value(), + _ => anyhow::bail!("unsupported signature algorithm"), + }; + + let signature = rsa::pkcs1v15::Signature::try_from(signature)?; + + let azure_trust = AzureTrust::new(signature, key, cert); + let azure_quote = AzureQuote::new(attest, pcr, report, azure_trust); + + Ok(azure_quote) +} diff --git a/azure-attest/src/host/vtpm.rs b/azure-attest/src/host/vtpm.rs new file mode 100644 index 00000000..bb54dc38 --- /dev/null +++ b/azure-attest/src/host/vtpm.rs @@ -0,0 +1,185 @@ +use std::{ops::Deref, path::Path, str::FromStr}; + +use libattest::error::Context; +use rsa::{BoxedUint, pkcs1v15::VerifyingKey}; +use sha2::Sha256; +use tss_esapi::{ + abstraction::nv, + handles::{KeyHandle, NvIndexTpmHandle}, + interface_types::{ + algorithm::HashingAlgorithm, resource_handles::NvAuth, session_handles::AuthSession, + }, + structures::{ + Attest, PcrSelectSize, PcrSelectionListBuilder, PcrSlot, Public, RsaExponent, Signature, + SignatureScheme, + }, + tcti_ldr::DeviceConfig, +}; + +use x509_cert::der::{Decode, SliceReader}; + +use crate::{quote::pcr::PcrBankReading, report::AttestationReport}; + +pub struct AzureTpmCtx { + context: tss_esapi::Context, +} + +impl AzureTpmCtx { + pub fn new(context: tss_esapi::Context) -> Self { + Self { context } + } + + pub fn with_device(device: impl AsRef) -> tss_esapi::Result { + use tss_esapi::tcti_ldr::TctiNameConf; + + let device = device.as_ref().to_str().unwrap(); + let mut context = + tss_esapi::Context::new(TctiNameConf::Device(DeviceConfig::from_str(device)?))?; + + context.set_sessions((Some(AuthSession::Password), None, None)); + + Ok(Self { context }) + } + + pub fn default_device() -> tss_esapi::Result { + Self::with_device("/dev/tpmrm0") + } + + const AK_CERT_IDX: u32 = 0x01C101D0; + const VTPM_HCL_AKPUB_PERSISTENT_HANDLE: u32 = 0x81000003; + const HARDWARE_REPORT_IDX: u32 = 0x01400001; + + pub const VTPM_DEFAULT_PCR_SLOTS: [PcrSlot; 24] = [ + PcrSlot::Slot0, + PcrSlot::Slot1, + PcrSlot::Slot2, + PcrSlot::Slot3, + PcrSlot::Slot4, + PcrSlot::Slot5, + PcrSlot::Slot6, + PcrSlot::Slot7, + PcrSlot::Slot8, + PcrSlot::Slot9, + PcrSlot::Slot10, + PcrSlot::Slot11, + PcrSlot::Slot12, + PcrSlot::Slot13, + PcrSlot::Slot14, + PcrSlot::Slot15, + PcrSlot::Slot16, + PcrSlot::Slot17, + PcrSlot::Slot18, + PcrSlot::Slot19, + PcrSlot::Slot20, + PcrSlot::Slot21, + PcrSlot::Slot22, + PcrSlot::Slot23, + ]; + + fn tpm_read(&mut self, index: u32) -> Result, tss_esapi::Error> { + let handle = NvIndexTpmHandle::new(index)?; + let read = nv::read_full(&mut self.context, NvAuth::Owner, handle)?; + + Ok(read) + } + + pub(super) fn hardware_report(&mut self) -> anyhow::Result { + let read = self.tpm_read(Self::HARDWARE_REPORT_IDX)?; + let report = crate::report::deserialize_attestation_report(&read)?; + + Ok(report) + } + + pub(super) fn ak_cert(&mut self) -> anyhow::Result { + let ak_cert = self + .tpm_read(Self::AK_CERT_IDX) + .context("unable to read ak_cert from tpm")?; + + let mut reader = SliceReader::new(&ak_cert)?; + + x509_cert::Certificate::decode(&mut reader) + .context("unable to decode certificate from DER") + .map_err(Into::into) + } + + fn ak_handle(&mut self) -> Result { + let handle = Self::VTPM_HCL_AKPUB_PERSISTENT_HANDLE.try_into()?; + + let public_key_handle = self + .context + .execute_without_session(|ctx| ctx.tr_from_tpm_public(handle))? + .into(); + + Ok(public_key_handle) + } + + pub(super) fn ak(&mut self) -> anyhow::Result> { + let public_key_handle = self.ak_handle()?; + let (public, _, _) = self + .context + .execute_without_session(|ctx| ctx.read_public(public_key_handle))?; + + let Public::Rsa { + unique, parameters, .. + } = public + else { + anyhow::bail!("received public key that was not rsa"); + }; + + let exponent = match parameters.exponent() { + RsaExponent::ZERO_EXPONENT => 65537, + exp => exp.value(), + }; + + rsa::RsaPublicKey::new( + BoxedUint::from_be_slice_vartime(unique.deref()), + exponent.into(), + ) + .context("unable to decode public key components from tpm") + .map(VerifyingKey::new) + .map_err(Into::into) + } + + pub(super) fn quote(&mut self, nonce: impl AsRef<[u8]>) -> anyhow::Result<(Attest, Signature)> { + let key_handle = self.ak_handle()?; + + let pcr_list = PcrSelectionListBuilder::new() + .with_selection(HashingAlgorithm::Sha256, &Self::VTPM_DEFAULT_PCR_SLOTS) + .with_size_of_select(PcrSelectSize::default()) + .build() + .unwrap(); + + let res = self + .context + .quote( + key_handle, + nonce.as_ref().try_into()?, + SignatureScheme::Null, + pcr_list, + ) + .context("unable to request quote from tpm")?; + + Ok(res) + } + + pub(super) fn read_pcr_bank(&mut self) -> anyhow::Result> { + let pcr_list = PcrSelectionListBuilder::new() + .with_selection(HashingAlgorithm::Sha256, &Self::VTPM_DEFAULT_PCR_SLOTS) + .with_size_of_select(PcrSelectSize::default()) + .build() + .unwrap(); + + // pcr_list. + + let list = self + .context + .execute_without_session(|ctx| tss_esapi::abstraction::pcr::read_all(ctx, pcr_list))?; + + let bank: PcrBankReading = list + .pcr_bank(HashingAlgorithm::Sha256) + .expect("we just requested this exact bank") + .into(); + + Ok(bank) + } +} diff --git a/azure-attest/src/lib.rs b/azure-attest/src/lib.rs new file mode 100644 index 00000000..e83f8fc5 --- /dev/null +++ b/azure-attest/src/lib.rs @@ -0,0 +1,12 @@ +pub mod collateral; +pub mod nonce; +pub mod quote; +pub mod report; + +#[cfg(feature = "host")] +pub mod host; + +pub mod ca; +mod serde; + +pub use quote::AzureQuote; diff --git a/azure-attest/src/nonce.rs b/azure-attest/src/nonce.rs new file mode 100644 index 00000000..dfc3699f --- /dev/null +++ b/azure-attest/src/nonce.rs @@ -0,0 +1 @@ +libattest::define_nonce_type!(#[derive(Debug, PartialEq, Eq)] pub AzureNonce, 32); diff --git a/azure-attest/src/quote.rs b/azure-attest/src/quote.rs new file mode 100644 index 00000000..cafc902d --- /dev/null +++ b/azure-attest/src/quote.rs @@ -0,0 +1,97 @@ +pub mod claims; +pub mod pcr; +pub mod verify; + +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use snp_attest::SevQuote; +use tdx_attest::TdxQuote; +use tpm2_protocol::data::TpmsAttest; + +#[cfg(target_family = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::{ + collateral::ReportVerifier, + nonce::AzureNonce, + quote::{claims::AzureClaims, pcr::PcrBankReading}, + report::{AttestationReport, HardwareReport}, +}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct AzureTrust { + quote_signature: rsa::pkcs1v15::Signature, + ak_key: rsa::pkcs1v15::VerifyingKey, + + #[serde(with = "crate::serde::serde_cert")] + ak_cert: x509_cert::Certificate, +} + +impl AzureTrust { + pub fn new( + quote_signature: rsa::pkcs1v15::Signature, + ak_key: rsa::pkcs1v15::VerifyingKey, + ak_cert: x509_cert::Certificate, + ) -> Self { + Self { + quote_signature, + ak_key, + ak_cert, + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[cfg_attr(target_family = "wasm", wasm_bindgen)] +pub struct AzureQuote { + #[serde(with = "crate::serde::serde_tpm")] + quote: TpmsAttest, + /// we store a single bank in the quote as we are requesting a single bank + /// from the vtpm (sha256) + pcr_bank: PcrBankReading, + hardware_report: AttestationReport, + trust: AzureTrust, +} + +impl AzureQuote { + /// performs cryptographic verification of the whole azure stack + /// and returns a set of verified claims on which the client can apply policies + pub fn verify( + &self, + report_verifier: ReportVerifier, + nonce: &AzureNonce, + ) -> libattest::Result> { + verify::verify_impl(self, report_verifier, nonce) + } +} + +// create only implementations +impl AzureQuote { + pub(crate) fn new( + quote: TpmsAttest, + pcr_bank: PcrBankReading, + hardware_report: AttestationReport, + trust: AzureTrust, + ) -> Self { + Self { + quote, + hardware_report, + pcr_bank, + trust, + } + } + + pub(crate) fn parse_hardware_report(&self) -> libattest::Result { + let report = match &self.hardware_report.payload { + HardwareReport::Tdx(items) => ParsedHardwareReport::Tdx(TdxQuote::from_bytes(items)?), + HardwareReport::Sev(items) => ParsedHardwareReport::Sev(SevQuote::new(items)?), + }; + + Ok(report) + } +} + +pub(crate) enum ParsedHardwareReport { + Tdx(TdxQuote), + Sev(SevQuote), +} diff --git a/azure-attest/src/quote/claims.rs b/azure-attest/src/quote/claims.rs new file mode 100644 index 00000000..df91287b --- /dev/null +++ b/azure-attest/src/quote/claims.rs @@ -0,0 +1,90 @@ +use libattest::validation::Verifiable; +use serde::Serialize; +use sha2::Sha256; +use snp_attest::claims::SevClaims; +use tdx_attest::dcap::types::TdxQuoteBody; +use tpm2_protocol::data::{TpmsAttest, TpmsClockInfo}; + +use crate::{quote::pcr::PcrBankReading, report::RuntimeClaims}; + +#[derive(Serialize, Debug)] +#[serde(tag = "type", content = "report")] +pub enum HardwareClaims { + Sev(Box), + Tdx(Box), +} + +#[derive(Serialize, Debug)] +pub struct TpmClockInfo { + clock: u64, + reset_count: u32, + restart_count: u32, + safe: bool, +} + +impl From for TpmClockInfo { + fn from(value: TpmsClockInfo) -> Self { + Self { + clock: value.clock.value(), + reset_count: value.reset_count.value(), + restart_count: value.restart_count.value(), + safe: value.safe.0, + } + } +} + +#[derive(Serialize, Debug)] +pub struct TpmAttestEvidence<'a> { + qualified_signer: &'a [u8], + #[serde(with = "hex::serde")] + extra_data: &'a [u8], + clock_info: TpmClockInfo, + firmware_version: u64, +} + +impl<'a> From<&'a TpmsAttest> for TpmAttestEvidence<'a> { + fn from(value: &'a TpmsAttest) -> Self { + Self { + qualified_signer: &value.qualified_signer, + extra_data: &value.extra_data, + clock_info: value.clock_info.into(), + firmware_version: value.firmware_version.0, + } + } +} + +#[derive(Serialize, Debug)] +pub struct AzureClaims<'a> { + pcr_bank: &'a PcrBankReading, + tpm_evidence: TpmAttestEvidence<'a>, + + hardware_claims: HardwareClaims, + runtime_claims: RuntimeClaims, +} + +impl Verifiable for AzureClaims<'_> { + type Claims<'x> + = &'x Self + where + Self: 'x; + + fn claims<'a>(&'a self) -> Self::Claims<'a> { + self + } +} + +impl<'a> AzureClaims<'a> { + pub(super) fn new( + pcr_bank: &'a PcrBankReading, + hardware_claims: HardwareClaims, + tpm_evidence: TpmAttestEvidence<'a>, + runtime_claims: RuntimeClaims, + ) -> Self { + Self { + pcr_bank, + hardware_claims, + tpm_evidence, + runtime_claims, + } + } +} diff --git a/azure-attest/src/quote/pcr.rs b/azure-attest/src/quote/pcr.rs new file mode 100644 index 00000000..1f148d5e --- /dev/null +++ b/azure-attest/src/quote/pcr.rs @@ -0,0 +1,69 @@ +use std::collections::BTreeMap; +use std::marker::PhantomData; +use std::ops::Deref; + +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "host")] +use sha2::Digest; +use sha2::digest; +#[cfg(feature = "host")] +use tss_esapi::abstraction::pcr::PcrBank; +#[cfg(feature = "host")] +use tss_esapi::structures::PcrSlot; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Pcr { + #[serde(with = "hex::serde")] + digest: Vec, +} + +impl Pcr { + pub fn new(digest: impl Into>) -> Self { + Self { + digest: digest.into(), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct PcrBankReading { + pcr_list: BTreeMap, + _digest: PhantomData, +} + +impl PcrBankReading { + pub fn pcr_digest(&self) -> digest::Output { + // Hash( PCR[0] | PCR[1] | ... ) <- concatenated + let data: Vec = self + .pcr_list + .values() + .flat_map(|pcr| pcr.digest.deref()) + .copied() + .collect(); + + D::digest(data) + } +} + +#[cfg(feature = "host")] +impl From<&PcrBank> for PcrBankReading { + fn from(value: &PcrBank) -> Self { + let pcr_list = value + .into_iter() + .map(|(slot, digest)| (pcr_slot_to_number(*slot), digest.to_vec())) + .map(|(slot, digest)| (slot, Pcr::new(digest))) + .collect(); + + PcrBankReading { + pcr_list, + _digest: PhantomData::default(), + } + } +} + +#[cfg(feature = "host")] +fn pcr_slot_to_number(slot: PcrSlot) -> u32 { + let slot: u32 = slot.into(); + slot.ilog2() +} diff --git a/azure-attest/src/quote/verify.rs b/azure-attest/src/quote/verify.rs new file mode 100644 index 00000000..3030aefb --- /dev/null +++ b/azure-attest/src/quote/verify.rs @@ -0,0 +1,278 @@ +use std::ops::Deref; + +use base64::Engine; +use jsonwebtoken::jwk::{AlgorithmParameters, JwkSet}; +use libattest::{ + ByteNonce, + crypto::{ + CertificateChain, + algorithms::{CertFormat, rsa::RsaCert}, + signature::rsa::RsaSignature, + }, + error::Context, + validation::Verifiable, +}; +use rsa::{BoxedUint, pkcs1::DecodeRsaPublicKey, pkcs1v15::VerifyingKey, signature::Verifier}; +use sha2::Sha256; +use tpm2_protocol::data::{TpmAlgId, TpmuAttest}; +use x509_cert::der::oid::db::rfc9688::RSA_ENCRYPTION; + +use crate::{ + ca::AZURE_CA, + collateral::ReportVerifier, + nonce::AzureNonce, + quote::{ + AzureQuote, ParsedHardwareReport, + claims::{AzureClaims, HardwareClaims, TpmAttestEvidence}, + }, + report::RuntimeClaims, +}; + +pub fn verify_quote_signature( + quote: &AzureQuote, + trust_chain: CertificateChain, +) -> libattest::Result<()> { + use tpm2_protocol::{TpmMarshal, TpmWriter}; + + let mut buffer = Box::new([0u8; 512]); + let mut writer = TpmWriter::new(buffer.as_mut_slice()); + + quote.quote.marshal(&mut writer)?; + + let written = writer.len(); + let marshaled = &buffer[..written]; + + let signature = RsaSignature::::new(quote.trust.quote_signature.clone()); + trust_chain.verify(marshaled, &signature)?; + + Ok(()) +} + +pub fn verify_quote_trust_chain( + quote: &AzureQuote, +) -> libattest::Result> { + let pk = quote + .trust + .ak_cert + .tbs_certificate() + .subject_public_key_info(); + + pk.algorithm + .assert_algorithm_oid(RSA_ENCRYPTION) + .context("invalid AK cert public key encryption algorithm")?; + + let public_key = rsa::RsaPublicKey::from_pkcs1_der(pk.subject_public_key.raw_bytes())?; + let public_key: VerifyingKey = VerifyingKey::new(public_key); + + if quote.trust.ak_key != public_key { + libattest::bail!(exposed: "mismatched ak key between certificate and evidence"); + } + + let cert = RsaCert::from_certificate(quote.trust.ak_cert.clone()) + .context("failed validating certificate")?; + + let intermediate = crate::ca::intermediate_db() + .find_intermediate(&cert) + .context("failed finding matching intermediate certificate for ak_cert")?; + + let chain = CertificateChain::::with_anchor(&AZURE_CA) + .with_certificate(intermediate.clone())? + .with_certificate(cert)?; + + Ok(chain) +} + +fn decode_base64_component(base64: &str) -> Result { + use base64::engine::general_purpose::URL_SAFE_NO_PAD; + + let decoded = URL_SAFE_NO_PAD.decode(base64)?; + Ok(BoxedUint::from_be_slice_vartime(&decoded)) +} + +pub fn verify_runtime_data(quote: &AzureQuote) -> libattest::Result { + let claims = quote + .hardware_report + .runtime + .claims() + .context("failed parsing Runtime Data claims")?; + + // Gather keys from runtime claims to enstablish trust into the keys used to sign + // the vTPM quote. + + let set = JwkSet { + keys: claims.keys.clone(), + }; + + let hclakpub = set + .find("HCLAkPub") + .context("Missing HCLAkPub from Runtime Claims")?; + + // Verify that keys match + let AlgorithmParameters::RSA(ref hclakpub) = hclakpub.algorithm else { + libattest::bail!("Received wrong key type in Report Data JWT for HCKLAkPub"); + }; + + // we decode the components of the rsa key from + // base64 encoded format of jsonwebkeys + let modulus = decode_base64_component(&hclakpub.n).context("failed parsing modulus")?; + let exponent = decode_base64_component(&hclakpub.e).context("failed parsing exponent")?; + + // convert the key into something we can compare + let report_ak = rsa::RsaPublicKey::new(modulus, exponent) + .map(rsa::pkcs1v15::VerifyingKey::::new) + .context("failed constructing rsa public key from report data")?; + + // Finally, we check if the vendored ak key and the trusted ak key + // whose trust we derive from the hardware report match + if report_ak != quote.trust.ak_key { + libattest::bail!(exposed: "TPM read key and Hardware Report verified key do not match"); + } + + Ok(claims) +} + +pub fn verify_report_digest( + quote: &AzureQuote, + verifier: &ReportVerifier, +) -> libattest::Result { + use libattest::quote::QuoteVerifier; + use snp_attest::nonce::SevNonce; + use tdx_attest::nonce::TdxNonce; + + let report = quote + .parse_hardware_report() + .context("unable to parse hardware report")?; + + let mut nonce = quote.hardware_report.hash_runtime_data()?; + + let padding = &[0u8; 64][..(64usize.saturating_sub(nonce.len()))]; + nonce.extend_from_slice(padding); // pad nonce to cover all 64 bytes of nonces + + // convert to fixed size nonce + let nonce = Box::<[u8; 64]>::try_from(nonce) + .ok() + .map(ByteNonce::from) + .context("nonce didn't fit")?; + + let claims = match report { + ParsedHardwareReport::Tdx(tdx_quote) => { + let verifier = verifier.tdx().context("didn't receive tdx verifier")?; + let nonce = TdxNonce::new(nonce); + + verifier.verify(&tdx_quote, &nonce)?; + HardwareClaims::Tdx(tdx_quote.body().clone().into()) + } + ParsedHardwareReport::Sev(sev_quote) => { + let verifier = verifier.sev().context("didn't receive sev verifier")?; + let nonce = SevNonce::new(nonce); + + verifier.verify(&sev_quote, &nonce)?; + HardwareClaims::Sev(sev_quote.claims().into()) + } + }; + + Ok(claims) +} + +fn verify_quote_nonce(azure_quote: &AzureQuote, nonce: &AzureNonce) -> libattest::Result<()> { + let tpm_nonce = azure_quote.quote.extra_data; + + // try to fit the tpm nonce into our azurenonce type. + // tpm nonces can be arbitrary sized (MAX 64) so we + // have to check manually + let tpm_nonce = <&[u8; _]>::try_from(tpm_nonce.deref()) + .map(AzureNonce::from) + .context("received nonce does not fit into defined AzureNonce type")?; + + if !nonce.eq(&tpm_nonce) { + libattest::bail!(exposed: "Mismatched vTPM nonce"); + } + + Ok(()) +} + +fn verify_pcr_digest(azure_quote: &AzureQuote) -> libattest::Result<()> { + let digest = azure_quote.pcr_bank.pcr_digest(); + + let TpmuAttest::Quote(ref info) = azure_quote.quote.attested else { + libattest::bail!("wrong type of attested data in quote"); + }; + + verify_pcr_selection(info)?; + + if info.pcr_digest.deref() != digest.deref() { + libattest::bail!(exposed: "PCR digest mismatch"); + } + + Ok(()) +} + +fn verify_pcr_selection(info: &tpm2_protocol::data::TpmsQuoteInfo) -> libattest::Result<()> { + const EXPECTED_PCR_SELECT: &[u8] = &[0xff, 0xff, 0xff]; + + if info.pcr_select.len() != 1 { + libattest::bail!(exposed: "TPM quote must contain exactly one PCR selection"); + } + + let selection = &info.pcr_select[0]; + if selection.hash != TpmAlgId::Sha256 { + libattest::bail!(exposed: "TPM quote PCR selection must use SHA-256"); + } + + if selection.pcr_select.deref() != EXPECTED_PCR_SELECT { + libattest::bail!(exposed: "TPM quote must select PCRs 0 through 23"); + } + + Ok(()) +} + +pub fn verify_impl<'a>( + azure_quote: &'a AzureQuote, + report_verifier: ReportVerifier, + nonce: &AzureNonce, +) -> libattest::Result> { + // 1: verify quote certificate chain against pinned trust + let trust_chain = + verify_quote_trust_chain(&azure_quote).context("while verifying quote certificates")?; + // 2: verify that AK signs Quote through leaf certificate + verify_quote_signature(&azure_quote, trust_chain).context("while verifying quote signature")?; + // 3: verify that the hardware report signs report data + let hardware_claims = verify_report_digest(&azure_quote, &report_verifier) + .context("while verifying report digest")?; + // 4: verify that report data contains the correct ak key, + // sprouting azure trust from SEV/TDX + let runtime_claims = + verify_runtime_data(&azure_quote).context("while verifying runtime data")?; + // 5: Verify that user supplied nonce and received quote nonce match + verify_quote_nonce(&azure_quote, nonce).context("while verifying quote nonce")?; + // 6: Verify pcr digest + verify_pcr_digest(&azure_quote).context("failed verifying pcr bank digest")?; + + let tpm_evidence = TpmAttestEvidence::from(&azure_quote.quote); + + let azure_claims = AzureClaims::new( + &azure_quote.pcr_bank, + hardware_claims, + tpm_evidence, + runtime_claims, + ); + + Ok(azure_claims) +} + +#[cfg(test)] +mod tests { + use super::*; + + const ATTESTATION: &str = include_str!("../../tests/attestation.json"); + + #[test] + fn bundled_quote_selects_sha256_pcrs_0_through_23() { + let quote: AzureQuote = serde_json::from_str(ATTESTATION).unwrap(); + let TpmuAttest::Quote(ref info) = quote.quote.attested else { + panic!("bundled attestation is not a TPM quote"); + }; + + verify_pcr_selection(info).unwrap(); + } +} diff --git a/azure-attest/src/report.rs b/azure-attest/src/report.rs new file mode 100644 index 00000000..1221841b --- /dev/null +++ b/azure-attest/src/report.rs @@ -0,0 +1,312 @@ +//! Azure confidential VM attestation report types. + +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256, Sha384, Sha512}; +use thiserror::Error; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned}; + +pub use jsonwebtoken::jwk::Jwk; + +/// Fixed Azure-defined attestation report header. +#[repr(C, packed)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + Deserialize, + FromBytes, + IntoBytes, + KnownLayout, + Immutable, + Unaligned, +)] +pub struct AttestationReportHeader { + /// Embedded signature. Expected value: [`ATTESTATION_REPORT_SIGNATURE`]. + pub signature: u32, + /// Format version. Expected value: [`ATTESTATION_REPORT_VERSION`]. + pub version: u32, + /// Size of the Azure-defined attestation report. + pub report_size: u32, + /// Azure-specific usage. Expected value: [`ATTESTATION_REPORT_REQUEST_TYPE`]. + pub request_type: u32, + /// Reserved status field. + pub status: u32, + /// Reserved bytes. + pub reserved: [u8; 12], +} + +impl AttestationReportHeader { + /// Azure attestation report magic value for ASCII `HCLA`. + pub const ATTESTATION_REPORT_SIGNATURE: u32 = 0x414c_4348; + /// Azure attestation report header version. + pub const ATTESTATION_REPORT_VERSION: u32 = 2; + /// Azure attestation report request type. + pub const ATTESTATION_REPORT_REQUEST_TYPE: u32 = 2; + /// Azure attestation report header size in bytes. + pub const ATTESTATION_REPORT_HEADER_SIZE: usize = 32; + /// Azure hardware report payload size in bytes. + pub const REPORT_PAYLOAD_SIZE: usize = 1184; + /// AMD SEV-SNP hardware report size in bytes. + pub const SEV_REPORT_PAYLOAD_SIZE: usize = 1184; + /// Intel TDX hardware report size in bytes. + pub const TDX_REPORT_PAYLOAD_SIZE: usize = 1024; + /// Offset of runtime data in the Azure report. + pub const RUNTIME_DATA_OFFSET: usize = + Self::ATTESTATION_REPORT_HEADER_SIZE + Self::REPORT_PAYLOAD_SIZE; +} + +/// Fixed runtime data header. +#[repr(C, packed)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + Deserialize, + FromBytes, + IntoBytes, + KnownLayout, + Immutable, + Unaligned, +)] +pub struct RuntimeDataHeader { + /// Size of runtime data, including this header. + pub data_size: u32, + /// Format version. Expected value: [`RuntimeDataHeader::RUNTIME_DATA_VERSION`]. + pub version: u32, + /// Hardware report type. + pub report_type: u32, + /// Runtime claims hash algorithm. + pub hash_type: u32, + /// Runtime claims JSON size. + pub claim_size: u32, +} + +impl RuntimeDataHeader { + /// Azure runtime data header size in bytes. + pub const RUNTIME_DATA_HEADER_SIZE: usize = 20; + /// Azure runtime data version. + pub const RUNTIME_DATA_VERSION: u32 = 1; + /// AMD SEV-SNP hardware report type. + pub const REPORT_TYPE_SEV: u32 = 2; + /// Intel TDX hardware report type. + pub const REPORT_TYPE_TDX: u32 = 4; + /// SHA-256 runtime claims hash type. + pub const HASH_TYPE_SHA256: u32 = 1; + /// SHA-384 runtime claims hash type. + pub const HASH_TYPE_SHA384: u32 = 2; + /// SHA-512 runtime claims hash type. + pub const HASH_TYPE_SHA512: u32 = 3; +} + +/// Owned Azure attestation report. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AttestationReport { + /// Fixed Azure-defined report header. + pub header: AttestationReportHeader, + /// Hardware report payload. + pub payload: HardwareReport, + /// Runtime data and runtime claims. + pub runtime: RuntimeData, +} + +impl AttestationReport { + /// Hashes the measured runtime claims JSON using the algorithm declared by + /// [`RuntimeDataHeader::hash_type`]. + pub fn hash_runtime_data(&self) -> Result, AttestationReportError> { + match self.runtime.header.hash_type { + RuntimeDataHeader::HASH_TYPE_SHA256 => { + Ok(Sha256::digest(&self.runtime.claims_json).to_vec()) + } + RuntimeDataHeader::HASH_TYPE_SHA384 => { + Ok(Sha384::digest(&self.runtime.claims_json).to_vec()) + } + RuntimeDataHeader::HASH_TYPE_SHA512 => { + Ok(Sha512::digest(&self.runtime.claims_json).to_vec()) + } + hash_type => Err(AttestationReportError::UnsupportedHashType(hash_type)), + } + } +} + +/// Owned hardware report payload. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum HardwareReport { + /// Intel TDX hardware report payload. + Tdx(#[serde(with = "hex::serde")] Vec), + /// AMD SEV-SNP hardware report payload. + Sev(#[serde(with = "hex::serde")] Vec), +} + +/// Owned runtime data. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeData { + /// Fixed runtime data header. + pub header: RuntimeDataHeader, + /// Raw runtime claims JSON bytes. + #[serde(with = "hex::serde")] + pub claims_json: Vec, +} + +impl RuntimeData { + /// Deserializes runtime claims from [`RuntimeData::claims_json`]. + pub fn claims(&self) -> Result { + serde_json::from_slice(&self.claims_json).map_err(AttestationReportError::from) + } +} + +/// Measured JSON runtime claims. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuntimeClaims { + /// vTPM public keys in JWK format. + pub keys: Vec, + /// Selective Azure confidential VM configuration. + #[serde(rename = "vm-configuration")] + pub vm_configuration: VmConfiguration, + /// 64-byte hex string read from TPM NV index `0x01400002`. + #[serde(rename = "user-data")] + pub user_data: String, +} + +/// Selective Azure confidential VM configuration runtime claim. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmConfiguration { + /// Root certificate thumbprint, when configured. + #[serde(rename = "root-cert-thumbprint")] + pub root_cert_thumbprint: Option, + /// Whether VM console access is enabled. + #[serde(rename = "console-enabled")] + pub console_enabled: bool, + /// Whether secure boot is enabled. + #[serde(rename = "secure-boot")] + pub secure_boot: bool, + /// Whether TPM is enabled. + #[serde(rename = "tpm-enabled")] + pub tpm_enabled: bool, + /// Whether TPM state is persisted. + #[serde(default, rename = "tpm-persisted")] + pub tpm_persisted: bool, + /// Azure VM unique ID. + #[serde(rename = "vmUniqueId")] + pub vm_unique_id: String, +} + +/// Deserializes an owned Azure attestation report from TPM NV index bytes. +pub fn deserialize_attestation_report( + bytes: &[u8], +) -> Result { + let header_bytes = bytes + .get(..AttestationReportHeader::ATTESTATION_REPORT_HEADER_SIZE) + .ok_or(AttestationReportError::Truncated { + needed: AttestationReportHeader::ATTESTATION_REPORT_HEADER_SIZE, + actual: bytes.len(), + })?; + let header = *AttestationReportHeader::ref_from_bytes(header_bytes) + .map_err(|_| AttestationReportError::InvalidLayout("attestation report header"))?; + + let report_size = header.report_size as usize; + if bytes.len() < report_size { + return Err(AttestationReportError::Truncated { + needed: report_size, + actual: bytes.len(), + }); + } + + let report = &bytes[..report_size]; + let payload_bytes = report + .get( + AttestationReportHeader::ATTESTATION_REPORT_HEADER_SIZE + ..AttestationReportHeader::RUNTIME_DATA_OFFSET, + ) + .ok_or(AttestationReportError::Truncated { + needed: AttestationReportHeader::RUNTIME_DATA_OFFSET, + actual: report.len(), + })?; + + let runtime = + deserialize_runtime_data(&report[AttestationReportHeader::RUNTIME_DATA_OFFSET..])?; + let payload = match runtime.header.report_type { + RuntimeDataHeader::REPORT_TYPE_TDX => HardwareReport::Tdx( + payload_bytes[..AttestationReportHeader::TDX_REPORT_PAYLOAD_SIZE].to_vec(), + ), + RuntimeDataHeader::REPORT_TYPE_SEV => HardwareReport::Sev( + payload_bytes[..AttestationReportHeader::SEV_REPORT_PAYLOAD_SIZE].to_vec(), + ), + report_type => return Err(AttestationReportError::UnsupportedReportType(report_type)), + }; + + Ok(AttestationReport { + header, + payload, + runtime, + }) +} + +fn deserialize_runtime_data(bytes: &[u8]) -> Result { + let header_bytes = bytes + .get(..RuntimeDataHeader::RUNTIME_DATA_HEADER_SIZE) + .ok_or(AttestationReportError::Truncated { + needed: RuntimeDataHeader::RUNTIME_DATA_HEADER_SIZE, + actual: bytes.len(), + })?; + let header = *RuntimeDataHeader::ref_from_bytes(header_bytes) + .map_err(|_| AttestationReportError::InvalidLayout("runtime data header"))?; + + let data_size = header.data_size as usize; + if bytes.len() < data_size { + return Err(AttestationReportError::Truncated { + needed: data_size, + actual: bytes.len(), + }); + } + + let runtime_data = &bytes[..data_size]; + let claims_start = RuntimeDataHeader::RUNTIME_DATA_HEADER_SIZE; + let claims_end = claims_start + .checked_add(header.claim_size as usize) + .ok_or(AttestationReportError::SizeOverflow)?; + let claims_json = runtime_data + .get(claims_start..claims_end) + .ok_or(AttestationReportError::Truncated { + needed: claims_end, + actual: runtime_data.len(), + })? + .to_vec(); + Ok(RuntimeData { + header, + claims_json, + }) +} + +/// Azure attestation report deserialization error. +#[derive(Debug, Error)] +pub enum AttestationReportError { + /// The input ended before the requested section. + #[error("truncated attestation report: needed {needed} bytes, got {actual}")] + Truncated { + /// Required byte count. + needed: usize, + /// Actual byte count. + actual: usize, + }, + /// Bytes could not be viewed as the requested fixed layout. + #[error("invalid {0} layout")] + InvalidLayout(&'static str), + /// Unsupported hardware report type. + #[error("unsupported hardware report type: {0}")] + UnsupportedReportType(u32), + /// Unsupported runtime hash type. + #[error("unsupported runtime hash type: {0}")] + UnsupportedHashType(u32), + /// Integer overflow while calculating a section boundary. + #[error("section size calculation overflowed")] + SizeOverflow, + /// Runtime claims JSON could not be deserialized. + #[error("invalid runtime claims: {0}")] + RuntimeClaims(#[from] serde_json::Error), +} diff --git a/azure-attest/src/serde.rs b/azure-attest/src/serde.rs new file mode 100644 index 00000000..8eba2048 --- /dev/null +++ b/azure-attest/src/serde.rs @@ -0,0 +1,2 @@ +pub mod serde_cert; +pub mod serde_tpm; diff --git a/azure-attest/src/serde/serde_cert.rs b/azure-attest/src/serde/serde_cert.rs new file mode 100644 index 00000000..e5b73792 --- /dev/null +++ b/azure-attest/src/serde/serde_cert.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Deserializer, Serializer}; +use x509_cert::der::{DecodePem, EncodePem}; + +pub fn serialize(certificate: &x509_cert::Certificate, serializer: S) -> Result +where + S: Serializer, +{ + let serialized = certificate + .to_pem(rsa::pkcs8::LineEnding::LF) + .map_err(::custom)?; + + serializer.serialize_str(&serialized) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let pem = String::deserialize(deserializer)?; + + let certificate = + x509_cert::Certificate::from_pem(pem).map_err(::custom)?; + + Ok(certificate) +} diff --git a/azure-attest/src/serde/serde_tpm.rs b/azure-attest/src/serde/serde_tpm.rs new file mode 100644 index 00000000..82907964 --- /dev/null +++ b/azure-attest/src/serde/serde_tpm.rs @@ -0,0 +1,34 @@ +use serde::{Deserializer, Serializer}; +use tpm2_protocol::{TpmMarshal, TpmSized, TpmUnmarshal, TpmWriter}; + +pub fn serialize(obj: &T, serializer: S) -> Result +where + S: Serializer, + T: TpmMarshal + TpmSized, +{ + let mut buf = vec![0; T::SIZE]; + let mut writer = TpmWriter::new(&mut buf); + + obj.marshal(&mut writer) + .map_err(::custom)?; + + hex::serde::serialize(buf, serializer) +} + +pub fn deserialize<'de, T, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: TpmUnmarshal + TpmSized, +{ + let buf: Vec = hex::serde::deserialize(deserializer)?; + + if buf.len() != T::SIZE { + return Err(serde::de::Error::custom( + "too many bytes to deserialize tpm type", + )); + } + + let (unmarshaled, _) = T::unmarshal(&buf).map_err(::custom)?; + + Ok(unmarshaled) +} diff --git a/azure-attest/tests/attestation.json b/azure-attest/tests/attestation.json new file mode 100644 index 00000000..d9a20d34 --- /dev/null +++ b/azure-attest/tests/attestation.json @@ -0,0 +1,121 @@ +{ + "quote": "ff54434780180022000b5ac884d7ac7bc7f9229c3d6949f32ef8937a460551c226fbf425735936564d90002000000000000000000000000000000000000000000000000000000000000000000000000030344cd4000000060000000001202003120012000300000001000b03ffffff0020679507d7cf91c3a52455ec7d66a4c5d445062c6d092d264ac87cc215508f43e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "pcr_bank": { + "pcr_list": { + "1": { + "digest": "84275b2f4312cd4fc6cbe6b152ad3c3683e513d9f1e23c34fca160c8cca7a6a7" + }, + "2": { + "digest": "5fc7f27b7963bbcdbe47d5c87f856a6cb875bd05009b8bba228b191d067eef73" + }, + "3": { + "digest": "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969" + }, + "4": { + "digest": "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969" + }, + "5": { + "digest": "933b0e2ab92e51216205b8033f4bf1d38984dd63ae556fe3f9f2c89d97e46fb0" + }, + "6": { + "digest": "b9f3e9f84a0130697af06f94ee652dec810b921eda0d4ef364a7b551532cd23a" + }, + "7": { + "digest": "b96cb94856feefa1cd24669ec724d7f7b8001ab792b09de85f1423668cccb937" + }, + "8": { + "digest": "3b20e022416fdf61d72e4da32b4354781be3de0608116976d28ffdad8c341d2a" + }, + "9": { + "digest": "0000000000000000000000000000000000000000000000000000000000000000" + }, + "10": { + "digest": "b14b6ddea210eee8f9bc505c001d1410dd1d6e54cf06b6821e109be79e80ae46" + }, + "11": { + "digest": "18dbff9d7c37f2b9e9c080256711f81059a8e2887ac01f5c2debc3b0979b2d4d" + }, + "12": { + "digest": "560c99b9bd329ef3c37b82ffaced660fc4f1f23bc8bb95c84e96b7aefa458d3d" + }, + "13": { + "digest": "f1a142c53586e7e2223ec74e5f4d1a4942956b1fd9ac78fafcdf85117aa345da" + }, + "14": { + "digest": "0000000000000000000000000000000000000000000000000000000000000000" + }, + "15": { + "digest": "306f9d8b94f17d93dc6e7cf8f5c79d652eb4c6c4d13de2dddc24af416e13ecaf" + }, + "16": { + "digest": "8157e23afdd6fbcb95118fb299823143d37d41cbcf4d4cf033e894cb060e99eb" + }, + "17": { + "digest": "0000000000000000000000000000000000000000000000000000000000000000" + }, + "18": { + "digest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "19": { + "digest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "20": { + "digest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "21": { + "digest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "22": { + "digest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "23": { + "digest": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "24": { + "digest": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "_digest": null + }, + "hardware_report": { + "header": { + "signature": 1095517000, + "version": 1, + "report_size": 2346, + "request_type": 2, + "status": 0, + "reserved": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "payload": { + "Sev": "030000000a0000001f000300000000000221200000000000000000000000000002000000000000000000000000000000000000000100000004000000000018db2500000000000000000000000000000021ac7778bc7d5d0af0696a439b37fe99fa5cfb4b3c2be912aea064d28cda7880000000000000000000000000000000000000000000000000000000000000000041f77fe5c1416343f84dbeeded504eb4a2c450861317ed3e4e46cd771c79243a4cbeb3d75ec663e6a7a47bd1f4fab5030000000000000000000000000000000000000000000000000000000000000000942fd93ebde6ea7a96efadeafc60f1c6b3d10e703b1dafd7555b92f7f3d32d0e006767648cba5b102af3d65756af4177000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c62938d3cd7984c017efab10b0a2d3b531e693e7d96f0f7609a13a650450bbe7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff04000000000018db1901010000000000000000000000000000000000000000000927a1df724ac8d325be526f764834f0b8effecc980828ba8d71b1cd79790091163f3bc7da71338b23333dd324076c21004017204af6d09a75d4ad9b469733e504000000000018db1d3701001d37010004000000000018db0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001882f8e127b75c1a0b9b7fbf8b1caa4700ef75c04fcb00d38325cdfffd9240c0762a2cbe4e64c3ed8a4ba8d7e44ce299000000000000000000000000000000000000000000000000703abc7f9fb7232f64bd3087c7e5f9bbc4b59d706f872fc7a3323d724c5eb05ba77d225adccaba5f9e454854325e61d30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + "runtime": { + "header": { + "data_size": 1130, + "version": 1, + "report_type": 2, + "hash_type": 1, + "claim_size": 1110 + }, + "claims_json": "7b226b657973223a5b7b226b6964223a2248434c416b507562222c226b65795f6f7073223a5b227369676e225d2c226b7479223a22525341222c2265223a2241514142222c226e223a2234507a74664141415f4171323545523271413168337a4d633236725a4b79524a78704a396175764674394b796a5a62534d70586c386d77415966696d6d345443676b53544849706330526a6e6873434c6230544c445a31635861335056414c3654346a776e6c4f2d644f2d48746e73464e786b513431625673705f3343727a50344462793077414863424545775773534d77646e56424c755363325a616f44426c6452666e636a3235754b564a4163394e5a6a4e684a3253473331454550644d42437457485a316d3969574f44655f516d4936576c77724c47627048782d2d6c665568456563736f5a6648716a666a4b782d31724e35525770667276687051734d5170494e3971506e6b454962546f64524f6d584f5741323270377a6d486659724b546a7655396f434573524f79332d764c3166692d774367507046654b533839696e4e59696c4f6867364f67554b594f3278573351227d2c7b226b6964223a2248434c456b507562222c226b65795f6f7073223a5b22656e6372797074225d2c226b7479223a22525341222c2265223a2241514142222c226e223a2234764f707267414253585a3967775a5175774b63664b68504a5967476a47645436586e4a494c4a564c7a677857325544544f57437972744b36305a632d546f4f65526e4a5f714574757136646e694964424b733450395735444e6a64343651377665385874784665366d632d6a33615630776a5a5a70355544445a556e6a792d6b6947574d4954684378485753316d53455f4e663476426b72797264644865384e5851757355324d68764839554547775a724c584336356f4d693072695831545335716f4b6b59645a6c594e4533695571716a70516a6d354462784d5748567849644b6e4e5074473035724f31656e55574f6671617776562d6963714166744d4d336e49366e4236614b4d67386a4b64717778437a704d3557792d70476a716a302d65467055354b67414e4a633632737946654f676e745f4b327655356441563055444f552d47576b6274527a3968616333384b5f51227d5d2c22766d2d636f6e66696775726174696f6e223a7b22636f6e736f6c652d656e61626c6564223a747275652c227365637572652d626f6f74223a747275652c2274706d2d656e61626c6564223a747275652c22766d556e697175654964223a2236464144363830332d374346412d343746362d413833332d313431463243374430414343227d2c22757365722d64617461223a223030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030227d" + } + }, + "trust": { + "quote_signature": "40e0580c3983253594777cd9f07defb26e8f3a6533953d512c33e1cb31e0bdd33b71d3eab2de70a24b73957dadd7b19e5ebd69cc310f75da869a4801b8e48b5517525de13715750e44259d8a9b82361d8426b843e88446a6b0d96ad6b64ee8d35b5ac9f48be73527e7958b6149eeae6155edf9aa38d3750dba4063113472314604e0ec632ced6b33c41374c80eb5c3b33e7cda0259335c2b5fdcbb62f4daf6e16592429c5198bf399527142fcbb3821763de3fcd9990eb44e2394412f91d4a35242839fc4e385b40ea9c90bba13e72be9441241c57779ca27556a6ed3cb3f518ec12c85cb8b96f584fa62f7f874723a91383727c62431eeede3653eb075fbe47", + "ak_key": "30820122300d06092a864886f70d01010105000382010f003082010a0282010100e0fced7c0000fc0ab6e44476a80d61df331cdbaad92b2449c6927d6aebc5b7d2b28d96d23295e5f26c0061f8a69b84c28244931c8a5cd118e786c08b6f44cb0d9d5c5dadcf5402fa4f88f09e53be74ef87b67b05371910e356d5b29ff70abccfe036f2d30007701104c16b123307675412ee49cd996a80c195d45f9dc8f6e6e29524073d3598cd849d921b7d4410f74c042b561d9d66f6258e0defd0988e96970acb19ba47c7efa57d484479cb2865f1ea8df8cac7ed6b379456a5faef86942c310a4837da8f9e41086d3a1d44e997396036da9ef39877d8aca4e3bd4f68084b113b2dfebcbd5f8bec0280fa4578a4bcf629cd62294e860e8e8142983b6c56dd0203010001", + "ak_cert": "-----BEGIN CERTIFICATE-----\nMIID7jCCAtagAwIBAgIRAIoBT94NClWkSBy3wIuaEXEwDQYJKoZIhvcNAQELBQAw\nJTEjMCEGA1UEAxMaR2xvYmFsIFZpcnR1YWwgVFBNIENBIC0gMDMwHhcNMjYwNjI3\nMDAwMDAwWhcNMjcwNDIxMDAwMDAwWjA4MTYwNAYDVQQDEy0wMmU1ODg2OWEyYjYu\nQ29uZmlkZW50aWFsVk0uQXp1cmUud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDg/O18AAD8CrbkRHaoDWHfMxzbqtkrJEnGkn1q68W3\n0rKNltIyleXybABh+KabhMKCRJMcilzRGOeGwItvRMsNnVxdrc9UAvpPiPCeU750\n74e2ewU3GRDjVtWyn/cKvM/gNvLTAAdwEQTBaxIzB2dUEu5JzZlqgMGV1F+dyPbm\n4pUkBz01mM2EnZIbfUQQ90wEK1YdnWb2JY4N79CYjpaXCssZukfH76V9SER5yyhl\n8eqN+MrH7Ws3lFal+u+GlCwxCkg32o+eQQhtOh1E6Zc5YDbanvOYd9ispOO9T2gI\nSxE7Lf68vV+L7AKA+kV4pLz2Kc1iKU6GDo6BQpg7bFbdAgMBAAGjggEEMIIBADAO\nBgNVHQ8BAf8EBAMCB4AwGAYDVR0gBBEwDzANBgsrBgEEAYI3bIFIATAcBgNVHSUE\nFTATBgorBgEEAYI3CgMMBgVngQUIAzAdBgNVHQ4EFgQUr57RFs5NWi58XdGtOgBu\n3FKq2zEwGgYKKwYBBAGCNw0CAwQMFgo2LjIuOTIwMC4yMFoGCSsGAQQBgjcVFARN\nMEsCAQUMD0FNUzA5MTA3MjcxMDAwNQwaV09SS0dST1VQXEFNUzA5MTA3MjcxMDAw\nNSQMGVZ0cG1NYW5hZ2VtZW50U2VydmljZS5leGUwHwYDVR0jBBgwFoAUZwmG+PmB\nXpOt+clk5xQK2pnLCi0wDQYJKoZIhvcNAQELBQADggEBAJoddxcTOfWimIJvejrK\n8k9SpJBu5FQ3S9gAoPNsO2jZ7PxK4FeSp39WHl2ks9pJIJiopuSDV+G1rPccY5zZ\nL9c4G3kffhgFNwvIj0+JCcudPF8vn/Vs4JRYc/VBvzG+bjQo+S+GWi49akWiR3Eg\nXbgZDA4sDyLQEtraP0Z2JOaJTVVvM/ldUjIl3rFpo8Dijehd8auJe8EObmXkS7AU\n63jI7jT6sU91y3s6X360esdmKN+/Z7i01HhnaBjUgROcs6y5kCXn7g1NO7PLqkpF\nFHsvHZFG9Jf+zGLORS60JLNLDzz5yl6hFq1zzETH2Pw68bjfbxiStS4jyiz6E17k\nG9I=\n-----END CERTIFICATE-----\n" + } +} diff --git a/azure-attest/tests/decode.rs b/azure-attest/tests/decode.rs new file mode 100644 index 00000000..ca6a3bbb --- /dev/null +++ b/azure-attest/tests/decode.rs @@ -0,0 +1,32 @@ +use azure_attest::{collateral::ReportVerifierBuilder, nonce::AzureNonce}; +use snp_attest::{kds::Kds, verify::SevQuoteVerifier}; + +const ATTESTATION: &'static str = include_str!("./attestation.json"); + +#[test] +fn decode_attestation() { + let attestation: azure_attest::AzureQuote = serde_json::from_str(ATTESTATION).unwrap(); +} + +#[tokio::test] +async fn verify_attestation() { + let quote: azure_attest::AzureQuote = serde_json::from_str(ATTESTATION).unwrap(); + + let kds = Kds::default(); + + let verifier = ReportVerifierBuilder::new() + .sev(async |quote| { + kds.fetch_certificates(quote) + .await + .map(SevQuoteVerifier::new) + }) + .tdx(async |_| todo!()) + .fetch_collateral("e) + .await + .unwrap(); + + let nonce = AzureNonce::from([0u8; 32]); + let claims = quote.verify(verifier, &nonce).unwrap(); + + println!("{claims:?}"); +} diff --git a/libattest/Cargo.toml b/libattest/Cargo.toml index 1abef54d..f71142f7 100644 --- a/libattest/Cargo.toml +++ b/libattest/Cargo.toml @@ -8,12 +8,23 @@ getrandom = "0.4.1" hex = "0.4.3" serde = { version = "1.0.228", features = ["derive"] } anyhow = "1.0" -regorus = { git = "https://github.com/microsoft/regorus.git", default-features = false, features = ["arc", "rvm", "std"] } +regorus = { git = "https://github.com/microsoft/regorus.git", default-features = false, features = ["arc", "rvm", "regex", "std", "coverage"] } # serde_json = "1.0.149" serde-value = "0.7.0" +x509-cert = "0.3.0-rc.4" +sha2 = "0.11.0" +p256 = "0.14.0-rc.14" +signature = "3.0" +sec1 = "0.8.1" +spki = "0.8.0" +thiserror = "2.0.18" +der = "0.8.0" +rsa = { version = "0.10.0-rc.18", features = ["serde"] } +digest = "0.11.3" [target.'cfg(target_family = "wasm")'.dependencies] wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" getrandom = { version = "0.4.2", features = ["wasm_js"] } js-sys = "0.3.93" +web-time = "1.1" diff --git a/libattest/src/crypto.rs b/libattest/src/crypto.rs new file mode 100644 index 00000000..1c3cf435 --- /dev/null +++ b/libattest/src/crypto.rs @@ -0,0 +1,10 @@ +pub mod algorithms; +pub mod chain; +pub mod error; +pub mod signature; + +pub use error::CertificateError; + +pub use algorithms::Cert; +pub use chain::CertificateChain; +pub use signature::DigestSignature; diff --git a/libattest/src/crypto/algorithms.rs b/libattest/src/crypto/algorithms.rs new file mode 100644 index 00000000..782048a7 --- /dev/null +++ b/libattest/src/crypto/algorithms.rs @@ -0,0 +1,52 @@ +use x509_cert::certificate::TbsCertificateInner; + +use crate::crypto::CertificateError; + +pub mod ecdsa; +pub mod rsa; + +mod sealed { + pub trait Sealed {} +} + +pub trait CertFormat: Sized + sealed::Sealed { + /// Parse this certificate format from the generix `x509_cert::Certificate` type + fn from_certificate(cert: x509_cert::Certificate) -> Result; + + /// Get inner signed data of this certificate + fn certificate(&self) -> &TbsCertificateInner; +} + +pub trait Cert: CertFormat { + /// Verifies that the public component of this certificate signs another certificate's TBS + fn verify_cert(&self, other: &Self) -> Result<(), CertificateError>; + + /// Verifies that the public component of this certificate signs its own TBS. + /// This is the case for Certificate Authorities (root of trust) + fn verify_self(&self) -> Result<(), CertificateError> { + self.verify_cert(self) + } +} + +fn verify_cert_common(certificate: &x509_cert::Certificate) -> Result<(), CertificateError> { + // check for certificate validity + let not_after = certificate + .tbs_certificate() + .validity() + .not_after + .to_system_time(); + + let not_before = certificate + .tbs_certificate() + .validity() + .not_before + .to_system_time(); + + let now = crate::now(); + + if not_before > now || not_after < now { + return Err(CertificateError::Expired); + } + + Ok(()) +} diff --git a/libattest/src/crypto/algorithms/ecdsa.rs b/libattest/src/crypto/algorithms/ecdsa.rs new file mode 100644 index 00000000..d5369ee5 --- /dev/null +++ b/libattest/src/crypto/algorithms/ecdsa.rs @@ -0,0 +1,121 @@ +use der::{ + Encode, + oid::db::{rfc5753::ID_EC_PUBLIC_KEY, rfc5912::ECDSA_WITH_SHA_256}, +}; +use p256::ecdsa::{Signature, VerifyingKey}; +use signature::Verifier; +use x509_cert::certificate::TbsCertificateInner; + +use crate::crypto::{ + CertificateError, + algorithms::{Cert, CertFormat, verify_cert_common}, +}; + +#[derive(Debug)] +pub struct EcdsaCert { + /// the original certificate where public_key and signature were derived from + certificate: x509_cert::Certificate, + /// this is the public key the certificate is trying to attest + public_key: VerifyingKey, + /// this is the signature attesting the authenticity and trustworthyness of the public key + signature: Signature, +} + +impl super::sealed::Sealed for EcdsaCert {} + +impl CertFormat for EcdsaCert { + fn from_certificate(cert: x509_cert::Certificate) -> Result { + Self::from_cert(cert) + } + + fn certificate(&self) -> &TbsCertificateInner { + self.certificate.tbs_certificate() + } +} + +impl signature::Verifier for EcdsaCert { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { + self.public_key.verify(msg, signature) + } +} + +impl Cert for EcdsaCert { + /// verifies that this certificate (`self`) contains a signed public key + /// that attests for the authenticity of the signature of another certificate (`other`) + /// + /// # Errors + /// Returns error if the two certificates cannot be linked by cryptographical means + fn verify_cert(&self, other: &Self) -> Result<(), CertificateError> { + let other_tbs = other.certificate.tbs_certificate().to_der()?; + self.public_key + .verify(&other_tbs, &other.signature) + .map_err(CertificateError::BadSignature)?; + + Ok(()) + } +} + +pub type PinnedCertificate = &'static EcdsaCert; + +impl EcdsaCert { + #[must_use] + pub fn cert(&self) -> &TbsCertificateInner { + self.certificate.tbs_certificate() + } + + /// verifies if the certificate is self-signed + /// + /// # Errors + /// Returns error if trust cannot be enstablished + /// within self + pub fn verify_self(&self) -> Result<(), CertificateError> { + self.verify_cert(self) + } + + /// Steps: + /// - decode signature and public key + /// - check for certificate validity + fn from_cert(certificate: x509_cert::Certificate) -> Result { + // Check that the signature and public key are in the + // format supported by the library (elliptic curve certificates) + certificate + .signature_algorithm() + .assert_algorithm_oid(ECDSA_WITH_SHA_256) + .map_err(CertificateError::WrongAlgorithm)?; + + certificate + .tbs_certificate() + .subject_public_key_info() + .algorithm + .assert_algorithm_oid(ID_EC_PUBLIC_KEY) + .map_err(CertificateError::WrongAlgorithm)?; + + // signature + let signature = certificate + .signature() + .as_bytes() + .ok_or(CertificateError::WrongFormat)?; + + let signature = + Signature::from_der(signature).expect("could not re-decode an encoded signature"); + + // signed public key + let public_key = certificate + .tbs_certificate() + .subject_public_key_info() + .subject_public_key + .as_bytes() + .ok_or(CertificateError::WrongFormat)?; + + let public_key = VerifyingKey::from_sec1_bytes(public_key) + .expect("could not re-decode an encoded public key"); + + verify_cert_common(&certificate)?; + + Ok(Self { + certificate, + public_key, + signature, + }) + } +} diff --git a/libattest/src/crypto/algorithms/rsa.rs b/libattest/src/crypto/algorithms/rsa.rs new file mode 100644 index 00000000..d6cf898e --- /dev/null +++ b/libattest/src/crypto/algorithms/rsa.rs @@ -0,0 +1,91 @@ +use std::sync::Arc; + +use der::{ + Encode, + oid::db::rfc5912::{RSA_ENCRYPTION, SHA_256_WITH_RSA_ENCRYPTION, SHA_384_WITH_RSA_ENCRYPTION}, +}; +use rsa::{RsaPublicKey, pkcs1::DecodeRsaPublicKey, pkcs1v15::Signature, signature::Verifier}; +use sha2::{Sha256, Sha384}; +use x509_cert::certificate::TbsCertificateInner; + +use crate::crypto::{ + CertificateError, DigestSignature, + algorithms::{Cert, CertFormat, verify_cert_common}, + signature::rsa::RsaSignature, +}; + +#[derive(Clone)] +pub struct RsaCert { + certificate: x509_cert::Certificate, + + /// verifyingkey algorithm is dictated by the format + /// of the signature, not by our public key, so we keep generic + public_key: RsaPublicKey, + + // this should be Signature. + signature: Arc, +} + +impl super::sealed::Sealed for RsaCert {} + +impl CertFormat for RsaCert { + fn from_certificate(cert: x509_cert::Certificate) -> Result { + Self::from_cert(cert) + } + + fn certificate(&self) -> &TbsCertificateInner { + self.certificate.tbs_certificate() + } +} + +impl Verifier for RsaCert { + fn verify(&self, msg: &[u8], signature: &D) -> Result<(), signature::Error> { + signature.verify(msg, &self.public_key.clone()) + } +} + +impl Cert for RsaCert { + fn verify_cert(&self, other: &Self) -> Result<(), CertificateError> { + let other_tbs = other.certificate.tbs_certificate().to_der()?; + + let public_key = self.public_key.clone(); + + other + .signature + .verify(&other_tbs, &public_key) + .map_err(CertificateError::BadSignature) + } +} + +impl RsaCert { + fn from_cert(certificate: x509_cert::Certificate) -> Result { + let pk = certificate.tbs_certificate().subject_public_key_info(); + pk.algorithm + .assert_algorithm_oid(RSA_ENCRYPTION) + .map_err(CertificateError::WrongAlgorithm)?; + + let signature = certificate + .signature() + .as_bytes() + .ok_or(CertificateError::WrongFormat) + .and_then(|sig| Signature::try_from(sig).map_err(Into::into))?; + + let public_key = RsaPublicKey::from_pkcs1_der(pk.subject_public_key.raw_bytes()) + .map_err(|_| CertificateError::WrongFormat)?; + + verify_cert_common(&certificate)?; + + let signature: Arc = + match certificate.signature_algorithm().oid { + SHA_384_WITH_RSA_ENCRYPTION => Arc::new(RsaSignature::::new(signature)), + SHA_256_WITH_RSA_ENCRYPTION => Arc::new(RsaSignature::::new(signature)), + _ => return Err(CertificateError::Unsupported), + }; + + Ok(Self { + certificate, + public_key, + signature, + }) + } +} diff --git a/libattest/src/crypto/chain.rs b/libattest/src/crypto/chain.rs new file mode 100644 index 00000000..817cce0c --- /dev/null +++ b/libattest/src/crypto/chain.rs @@ -0,0 +1,85 @@ +pub mod crl; + +use crate::crypto::{CertificateError, algorithms::Cert}; + +pub type PinnedCertificate = &'static C; + +#[derive(Debug)] +/// represents a cryptographically verified +/// certificate chain +pub struct CertificateChain { + /// optional trust anchor of a pinned certificate + anchor: Option>, + /// a chain of certificates. Last one is leaf. + chain: Vec, +} + +impl CertificateChain { + #[must_use] + pub fn with_anchor(anchor: PinnedCertificate) -> Self { + Self { + anchor: Some(anchor), + chain: vec![], + } + } + + #[must_use] + /// Returns the leaf certificate or None if this certificate chain is empty + /// and does not have any anchor of trust + pub fn leaf(&self) -> Option<&C> { + self.chain.last() + } + + pub fn with_certificate(mut self, other: C) -> Result { + self.push_certificate(other)?; + Ok(self) + } + + /// Inserts a new leaf into this certificate chain. The certificate is first + /// verified by the previous certificates + pub fn push_certificate(&mut self, other: C) -> Result<(), CertificateError> { + let verifier = self.chain.last().or(self.anchor); + + // todo: add match between SKI and AKI + + match verifier { + Some(verifier) => verifier.verify_cert(&other)?, + None => other.verify_self()?, + }; + + self.chain.push(other); + + Ok(()) + } + + pub fn parse_pem_chain(mut self, chain: &[u8]) -> Result { + let chain = chain.strip_suffix(b"\0").unwrap_or(chain); // chain from tdx could be 0 terminated so we do a little sanitization + + let chain: Vec = x509_cert::Certificate::load_pem_chain(chain)? + .into_iter() + .map(C::from_certificate) + .collect::>()?; + + let mut chain = chain.into_iter().rev(); + + if let Some(_anchor) = self.anchor { + // discard the root certificate from the pem chain if we already have our own embedded trust + let _root = chain.next().ok_or(CertificateError::BadChain)?; + + // todo: maybe verify they match? + } + + chain.try_for_each(|cert| self.push_certificate(cert))?; + + Ok(self) + } +} + +impl signature::Verifier for CertificateChain +where + C: Cert + signature::Verifier, +{ + fn verify(&self, msg: &[u8], sig: &S) -> Result<(), signature::Error> { + self.leaf().unwrap().verify(msg, sig) + } +} diff --git a/tdx-attest/src/certificates/crl.rs b/libattest/src/crypto/chain/crl.rs similarity index 78% rename from tdx-attest/src/certificates/crl.rs rename to libattest/src/crypto/chain/crl.rs index 530edde2..8fc216c6 100644 --- a/tdx-attest/src/certificates/crl.rs +++ b/libattest/src/crypto/chain/crl.rs @@ -1,14 +1,12 @@ -use crate::certificates::now; - use der::oid::db::rfc5912::ECDSA_WITH_SHA_256; use der::{Decode, Encode}; use p256::ecdsa::Signature; -use signature::Verifier; +use x509_cert::crl::CertificateList; use x509_cert::der::DecodePem; -use x509_cert::time::Time; -use x509_cert::{certificate::Rfc5280, crl::CertificateList}; -use crate::certificates::{CertificateChain, CertificateError, EcdsaCert}; +use crate::crypto::CertificateError; +use crate::crypto::algorithms::{Cert, CertFormat}; +use crate::crypto::chain::CertificateChain; mod sealed { pub trait Sealed {} @@ -16,22 +14,22 @@ mod sealed { pub struct Crl { list: CertificateList, - - signature: Signature, + _signature: Signature, } impl sealed::Sealed for Crl {} impl Crl { pub fn from_pem( - verifier: impl Verifier, + verifier: impl signature::Verifier, pem: impl AsRef<[u8]>, ) -> Result { let list = CertificateList::from_pem(pem)?; Self::from_certificate_list(verifier, list) } + pub fn from_der( - verifier: impl Verifier, + verifier: impl signature::Verifier, der: impl AsRef<[u8]>, ) -> Result { let list = CertificateList::from_der(der.as_ref())?; @@ -39,7 +37,7 @@ impl Crl { } pub fn from_certificate_list( - verifier: impl Verifier, + verifier: impl signature::Verifier, list: CertificateList, ) -> Result { // let list: CertificateList = CertificateList::from_pem(pem)?; @@ -48,7 +46,7 @@ impl Crl { let expired = list .tbs_cert_list .next_update - .is_some_and(|a| a.to_system_time() < now()); + .is_some_and(|a| a.to_system_time() < crate::now()); if expired { return Err(CertificateError::Expired); @@ -72,12 +70,13 @@ impl Crl { Signature::from_der(signature).expect("could not re-decode an encoded signature"); // verify the signature of the certificate chain - verifier - .verify(&tbs_list, &signature) - .map_err(CertificateError::BadSignature)?; + verifier.verify(&tbs_list, &signature)?; // ok! - Ok(Self { list, signature }) + Ok(Self { + list, + _signature: signature, + }) } } @@ -85,16 +84,16 @@ pub trait VerifyCrl: sealed::Sealed { fn check_revoked(&self, tbs: &Tbs) -> Result<(), CertificateError>; } -impl VerifyCrl for Crl { - fn check_revoked(&self, tbs: &CertificateChain) -> Result<(), CertificateError> { +impl VerifyCrl> for Crl { + fn check_revoked(&self, tbs: &CertificateChain) -> Result<(), CertificateError> { tbs.chain .iter() .try_for_each(|cert| self.check_revoked(cert)) } } -impl VerifyCrl for Crl { - fn check_revoked(&self, tbs: &EcdsaCert) -> Result<(), CertificateError> { +impl VerifyCrl for Crl { + fn check_revoked(&self, tbs: &C) -> Result<(), CertificateError> { let certificates = self .list .tbs_cert_list @@ -103,7 +102,7 @@ impl VerifyCrl for Crl { .flatten(); // if this serial number is found in the list, the certificate has been revoked - let serial_number = tbs.certificate.tbs_certificate().serial_number().clone(); + let serial_number = tbs.certificate().serial_number().clone(); let found = certificates .map(|cert| &cert.serial_number) diff --git a/tdx-attest/src/certificates/error.rs b/libattest/src/crypto/error.rs similarity index 76% rename from tdx-attest/src/certificates/error.rs rename to libattest/src/crypto/error.rs index 836ca810..a0f7f2ad 100644 --- a/tdx-attest/src/certificates/error.rs +++ b/libattest/src/crypto/error.rs @@ -6,6 +6,9 @@ pub enum CertificateError { #[error("tried parsing a certificate with an unsupported algorithm: {0}")] WrongAlgorithm(spki::Error), + #[error("the signature or protocol used by this certificated is not supported")] + Unsupported, + #[error("a signature or key was serialized in the wrong format in the certificate")] WrongFormat, @@ -21,6 +24,12 @@ pub enum CertificateError { #[error("certificate extension is malformed")] Extension, + #[error("this certificate chain does not contain any leaf certificates")] + NoLeaf, + + #[error("signature error: {0}")] + Signature(#[from] signature::Error), + #[error("an error occurred while parsing the certificate: {0}")] Der(#[from] der::Error), diff --git a/libattest/src/crypto/signature.rs b/libattest/src/crypto/signature.rs new file mode 100644 index 00000000..41bf5439 --- /dev/null +++ b/libattest/src/crypto/signature.rs @@ -0,0 +1,8 @@ +use ::rsa::RsaPublicKey; + +pub mod rsa; + +pub trait DigestSignature { + /// automatically converts this public key into an appropriate + fn verify(&self, msg: &[u8], pk: &RsaPublicKey) -> signature::Result<()>; +} diff --git a/libattest/src/crypto/signature/rsa.rs b/libattest/src/crypto/signature/rsa.rs new file mode 100644 index 00000000..d6aef3ba --- /dev/null +++ b/libattest/src/crypto/signature/rsa.rs @@ -0,0 +1,32 @@ +use std::marker::PhantomData; + +use digest::FixedOutputReset; +use rsa::{ + RsaPublicKey, + pkcs1v15::{Signature, VerifyingKey}, +}; +use signature::Verifier; + +/// PKCS1v15 signature, digest agnostic +pub struct RsaSignature { + signature: Signature, + _digest: PhantomData, +} + +impl RsaSignature { + pub fn new(signature: Signature) -> Self { + Self { + signature, + _digest: PhantomData, + } + } +} + +impl super::DigestSignature + for RsaSignature +{ + fn verify(&self, msg: &[u8], pk: &RsaPublicKey) -> signature::Result<()> { + let verifying = VerifyingKey::::new(pk.clone()); + verifying.verify(msg, &self.signature) + } +} diff --git a/libattest/src/crypto/test/ca_cert.pem b/libattest/src/crypto/test/ca_cert.pem new file mode 100644 index 00000000..d37ca072 --- /dev/null +++ b/libattest/src/crypto/test/ca_cert.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB7TCCAZKgAwIBAgIUEcpkZPPXHIC+2XRnoigNGxXX9b0wCgYIKoZIzj0EAwIw +VDELMAkGA1UEBhMCWFgxGDAWBgNVBAoMD0V4YW1wbGUgVGVzdCBDQTErMCkGA1UE +AwwiRXhhbXBsZSBUZXN0IENBIFJvb3QgKEVDRFNBIFAtMjU2KTAeFw0yNjAyMTcy +MjM5NTdaFw0zNjAyMTUyMjQwNTdaMFQxCzAJBgNVBAYTAlhYMRgwFgYDVQQKDA9F +eGFtcGxlIFRlc3QgQ0ExKzApBgNVBAMMIkV4YW1wbGUgVGVzdCBDQSBSb290IChF +Q0RTQSBQLTI1NikwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT3IfU2Nwsv43yz +1mW5gEY2zcQkYz60HCJMcaweVsKletVFYhwFmejanyHf4Em88j+BcOLS5OVP9Yhn +iIHJRZhjo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNV +HQ4EFgQUvmll4rii1/H8OJslwnBcVlPzyOAwCgYIKoZIzj0EAwIDSQAwRgIhAOC+ +tloHkQVQQI3inbesJMvYWpM8Vy68FlWQrqWQXz2AAiEA+dT5QF58RhQzQfSCc0uz +kbHNfkHmk4hhEFoHcLug0f0= +-----END CERTIFICATE----- diff --git a/libattest/src/crypto/test/ca_crl.pem b/libattest/src/crypto/test/ca_crl.pem new file mode 100644 index 00000000..d63fd068 --- /dev/null +++ b/libattest/src/crypto/test/ca_crl.pem @@ -0,0 +1,9 @@ +-----BEGIN X509 CRL----- +MIIBRDCB6wIBATAKBggqhkjOPQQDAjBUMQswCQYDVQQGEwJYWDEYMBYGA1UECgwP +RXhhbXBsZSBUZXN0IENBMSswKQYDVQQDDCJFeGFtcGxlIFRlc3QgQ0EgUm9vdCAo +RUNEU0EgUC0yNTYpFw0yNjAyMTcyMjM5NTdaFw0yNjAzMTkyMjQwNTdaMDUwMwIU +Zw5n0aeNdF9mB0Egovmdk1l0D4oXDTI2MDIxNzIyNDA1N1owDDAKBgNVHRUEAwoB +AaAvMC0wCgYDVR0UBAMCAQEwHwYDVR0jBBgwFoAUvmll4rii1/H8OJslwnBcVlPz +yOAwCgYIKoZIzj0EAwIDSAAwRQIhAIFn1xA5dGibpcTZvfu3aYqHR9th+MRy11ZO +4CgZ/fX/AiB+o6QBYKaYLLBJreQIj74FwV46yjtWlMB32ptaj2nX5Q== +-----END X509 CRL----- diff --git a/libattest/src/crypto/test/revoked_cert.pem b/libattest/src/crypto/test/revoked_cert.pem new file mode 100644 index 00000000..b2c7bc4b --- /dev/null +++ b/libattest/src/crypto/test/revoked_cert.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICFjCCAbygAwIBAgIUZw5n0aeNdF9mB0Egovmdk1l0D4owCgYIKoZIzj0EAwIw +VDELMAkGA1UEBhMCWFgxGDAWBgNVBAoMD0V4YW1wbGUgVGVzdCBDQTErMCkGA1UE +AwwiRXhhbXBsZSBUZXN0IENBIFJvb3QgKEVDRFNBIFAtMjU2KTAeFw0yNjAyMTcy +MjM5NTdaFw0yNzAyMTcyMjQwNTdaMEsxCzAJBgNVBAYTAlhYMRowGAYDVQQKDBFF +eGFtcGxlIFRlc3QgTGVhZjEgMB4GA1UEAwwXcmV2b2tlZC5leGFtcGxlLmludmFs +aWQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASQsdqcM6MSGhU0OS7NpYlaw0nA +CoQrHqSZGWDo47feoO5TFQ4uqS0x5Wj+HKX2SJK5JuQf6lPjhG73jwIhgLJEo3Uw +czAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIDiDATBgNVHSUEDDAKBggrBgEF +BQcDATAdBgNVHQ4EFgQUIXlAyoGCLtBkMSKZC+94Pcw4y9owHwYDVR0jBBgwFoAU +vmll4rii1/H8OJslwnBcVlPzyOAwCgYIKoZIzj0EAwIDSAAwRQIgHi9hCRKfepRE +rjInOJymO+iXZRFxLv6uYdxbupIwctUCIQCumpP9fHkY1Ua+Ey7S/dwbFRyWb3t0 +SRYBH25lTh+AEQ== +-----END CERTIFICATE----- diff --git a/libattest/src/lib.rs b/libattest/src/lib.rs index 4756c5be..ac40a37d 100644 --- a/libattest/src/lib.rs +++ b/libattest/src/lib.rs @@ -1,62 +1,27 @@ +pub mod crypto; pub mod error; pub mod modules; +pub mod nonce; +pub mod quote; pub mod validation; pub use modules::*; +pub use nonce::*; pub type Result = std::result::Result; +pub use error::AttestationError; -#[derive(Debug, PartialEq, Eq)] -pub struct ByteNonce(Box<[u8; N]>); +pub use p256; -impl ByteNonce { - pub fn generate() -> Self { - let mut bytes = Box::new([0u8; N]); - - getrandom::fill(bytes.as_mut_slice()).unwrap(); - - Self(bytes) - } - - pub fn to_hex(&self) -> String { - hex::encode_upper(self.0.as_ref()) - } +#[cfg(target_arch = "wasm32")] +pub(crate) fn now() -> std::time::SystemTime { + std::time::SystemTime::UNIX_EPOCH + + web_time::SystemTime::now() + .duration_since(web_time::SystemTime::UNIX_EPOCH) + .unwrap_or_default() } -impl From<[u8; N]> for ByteNonce { - fn from(value: [u8; N]) -> Self { - Self(Box::new(value)) - } +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn now() -> std::time::SystemTime { + std::time::SystemTime::now() } - -impl From> for ByteNonce { - fn from(value: Box<[u8; N]>) -> Self { - Self(value) - } -} - -impl std::ops::Deref for ByteNonce { - type Target = [u8; N]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl AsRef<[u8]> for ByteNonce { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl AsRef<[u8; N]> for ByteNonce { - fn as_ref(&self) -> &[u8; N] { - self.0.as_ref() - } -} - -// impl From> for ByteNonce { -// fn from(value: Box<[u8; N]>) -> Self { -// Self(value) -// } -// } diff --git a/libattest/src/modules.rs b/libattest/src/modules.rs index 7cb2f9d6..2ed33f04 100644 --- a/libattest/src/modules.rs +++ b/libattest/src/modules.rs @@ -9,6 +9,7 @@ use wasm_bindgen::prelude::wasm_bindgen; pub enum CpuModule { Sev, Tdx, + Azure, } #[cfg_attr(target_family = "wasm", wasm_bindgen)] diff --git a/libattest/src/nonce.rs b/libattest/src/nonce.rs new file mode 100644 index 00000000..e4535c84 --- /dev/null +++ b/libattest/src/nonce.rs @@ -0,0 +1,108 @@ +#[macro_export] +macro_rules! define_nonce_type { + ($(#[$meta:meta])* $vis:vis $name:ident, $size:literal $(,)?) => { + $(#[$meta])* + #[cfg_attr(target_family = "wasm", wasm_bindgen::prelude::wasm_bindgen)] + $vis struct $name($crate::ByteNonce<$size>); + + impl $name { + pub const SIZE: usize = $size; + + pub fn new(byte_nonce: $crate::ByteNonce<$size>) -> Self { + Self(byte_nonce) + } + } + + impl From<$crate::ByteNonce<$size>> for $name { + fn from(value: $crate::ByteNonce<$size>) -> Self { + Self::new(value) + } + } + + impl From<[u8; $size]> for $name { + fn from(value: [u8; $size]) -> Self { + Self::new((&value).into()) + } + } + + impl From<&[u8; $size]> for $name { + fn from(value: &[u8; $size]) -> Self { + Self::new(value.into()) + } + } + + impl From> for $name { + fn from(value: Box<[u8; $size]>) -> Self { + Self::new(value.into()) + } + } + + impl std::ops::Deref for $name { + type Target = $crate::ByteNonce<$size>; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + #[cfg_attr(target_family = "wasm", wasm_bindgen::prelude::wasm_bindgen)] + impl $name { + pub fn generate() -> Self { + Self($crate::ByteNonce::<$size>::generate()) + } + + pub fn to_hex(&self) -> String { + self.0.to_hex() + } + } + }; +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ByteNonce(Box<[u8; N]>); + +impl ByteNonce { + pub fn generate() -> Self { + let mut bytes = Box::new([0u8; N]); + + getrandom::fill(bytes.as_mut_slice()).unwrap(); + + Self(bytes) + } + + pub fn to_hex(&self) -> String { + hex::encode_upper(self.0.as_ref()) + } +} + +impl From<&[u8; N]> for ByteNonce { + fn from(value: &[u8; N]) -> Self { + Self(Box::new(*value)) + } +} + +impl From> for ByteNonce { + fn from(value: Box<[u8; N]>) -> Self { + Self(value) + } +} + +impl std::ops::Deref for ByteNonce { + type Target = [u8; N]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef<[u8]> for ByteNonce { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl AsRef<[u8; N]> for ByteNonce { + fn as_ref(&self) -> &[u8; N] { + self.0.as_ref() + } +} diff --git a/libattest/src/quote.rs b/libattest/src/quote.rs new file mode 100644 index 00000000..6bc3a7b9 --- /dev/null +++ b/libattest/src/quote.rs @@ -0,0 +1,6 @@ +pub trait QuoteVerifier { + type Nonce; + type Quote; + + fn verify(&self, quote: &Self::Quote, nonce: &Self::Nonce) -> crate::Result<()>; +} diff --git a/libattest/src/validation.rs b/libattest/src/validation.rs index cc00088c..49e0af8f 100644 --- a/libattest/src/validation.rs +++ b/libattest/src/validation.rs @@ -5,20 +5,23 @@ use serde::{Deserialize, Serialize}; use crate::error::{AttestationError, Context}; -pub trait Claim: AssignedPolicy { +pub trait Verifiable { + type Claims<'a>: Serialize + where + Self: 'a; + /// Converts this set of claims into a rego engine compatible format - fn rego_repr(&self) -> impl Serialize; + fn claims<'a>(&'a self) -> Self::Claims<'a>; } -pub trait IntoClaims { - type Claims: Claim; - fn into_claims(self) -> Self::Claims; -} +impl Verifiable for &V { + type Claims<'a> + = V::Claims<'a> + where + Self: 'a; -impl IntoClaims for C { - type Claims = C; - fn into_claims(self) -> C { - self + fn claims<'a>(&'a self) -> Self::Claims<'a> { + (*self).claims() } } @@ -33,13 +36,19 @@ pub trait AssignedPolicy { fn policy(&self) -> Cow<'static, str>; } -pub struct WithPolicy { - claims: T, +impl AssignedPolicy for &A { + fn policy(&self) -> Cow<'static, str> { + (*self).policy() + } +} + +pub struct WithPolicy { + claims: C, policy: Cow<'static, str>, } -impl WithPolicy { - pub fn new(policy: impl Into>, claims: T) -> Self { +impl WithPolicy { + pub fn new(policy: impl Into>, claims: C) -> Self { Self { claims, policy: policy.into(), @@ -47,13 +56,18 @@ impl WithPolicy { } } -impl Claim for WithPolicy { - fn rego_repr(&self) -> impl Serialize { - &self.claims +impl Verifiable for WithPolicy { + type Claims<'a> + = C::Claims<'a> + where + Self: 'a; + + fn claims<'a>(&'a self) -> Self::Claims<'a> { + self.claims.claims() } } -impl AssignedPolicy for WithPolicy { +impl AssignedPolicy for WithPolicy { fn policy(&self) -> Cow<'static, str> { self.policy.clone() } @@ -130,19 +144,23 @@ impl Validator { /// gets the rego query and input from `impl Claim` and then /// drives the engine to verify the query - pub fn verify_claim(&self, claims: impl IntoClaims) -> Result { - let claims = claims.into_claims(); + pub fn verify_claim(&self, claims: C) -> Result + where + C: Verifiable + AssignedPolicy, + { // avois polluting the engine for further verifications // and allows us to have this method &self let mut engine = self.engine.clone(); // convert claims to rego compatible format - let value = serde_value::to_value(claims.rego_repr())?; + let value = serde_value::to_value(claims.claims())?; let value = regorus::Value::deserialize(value)?; // here we set what input. will be in rego engine.set_input(value); let query = format!("data.{}", claims.policy()); + + engine.set_enable_coverage(true); let result = engine .eval_rule(query) .map_err(AttestationError::from_anyhow) @@ -154,17 +172,34 @@ impl Validator { _ => return AttestationError::internal("rego policy returned a non boolean result"), }; - Ok(ValidationResult(result)) + let res = if result { + ValidationResult::Success + } else { + let coverage = engine + .get_coverage_report() + .and_then(|report| report.to_string_pretty()) + .map_err(AttestationError::from_anyhow)?; + + ValidationResult::Failure { coverage } + }; + + Ok(res) } } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] #[must_use] -pub struct ValidationResult(pub bool); +pub enum ValidationResult { + Success, + Failure { coverage: String }, +} impl ValidationResult { pub fn or_err(self, msg: &'static str) -> Result<(), AttestationError> { - self.0.then_some(()).context(msg) + match self { + Self::Success => Ok(()), + Self::Failure { coverage } => Err(AttestationError::new(coverage)).context(msg), + } } } diff --git a/nvidia-attest/src/lib.rs b/nvidia-attest/src/lib.rs index e8f244b3..9473f4ce 100644 --- a/nvidia-attest/src/lib.rs +++ b/nvidia-attest/src/lib.rs @@ -8,7 +8,7 @@ use jsonwebtoken::{DecodingKey, Validation}; use libattest::{ bail, error::{AttestationError, Context, Expose}, - // validation::AssignedPolicy, + validation::Verifiable, }; use serde::Serialize; use serde_json::Value; @@ -31,6 +31,17 @@ pub struct DecodedClaims { gpu_claims: HashMap, } +impl Verifiable for DecodedClaims { + type Claims<'a> + = &'a DecodedClaims + where + Self: 'a; + + fn claims<'a>(&'a self) -> Self::Claims<'a> { + self + } +} + impl DecodedClaims { pub fn overall_claims(&self) -> &OverallClaims { &self.overall_claims diff --git a/nvidia-attest/src/nonce.rs b/nvidia-attest/src/nonce.rs index bfe26d8f..7e64cda4 100644 --- a/nvidia-attest/src/nonce.rs +++ b/nvidia-attest/src/nonce.rs @@ -1,27 +1 @@ -use std::ops::Deref; - -#[cfg(target_family = "wasm")] -use wasm_bindgen::prelude::*; - -#[derive(Debug, PartialEq, Eq)] -#[cfg_attr(target_family = "wasm", wasm_bindgen)] -pub struct NvidiaNonce(libattest::ByteNonce<32>); - -impl Deref for NvidiaNonce { - type Target = libattest::ByteNonce<32>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[cfg_attr(target_family = "wasm", wasm_bindgen)] -impl NvidiaNonce { - pub fn generate() -> Self { - Self(libattest::ByteNonce::generate()) - } - - pub fn to_hex(&self) -> String { - self.0.to_hex() - } -} +libattest::define_nonce_type!(#[derive(Debug, PartialEq, Eq)] pub NvidiaNonce, 32); diff --git a/nvidia-attest/src/types.rs b/nvidia-attest/src/types.rs index 606c8694..fd396890 100644 --- a/nvidia-attest/src/types.rs +++ b/nvidia-attest/src/types.rs @@ -86,9 +86,6 @@ pub struct GpuClaims { /// Universal entity id of the GPU pub ueid: String, - // hardware model of the gpu - pub hwmodel: String, - /// Secure boot status. #[serde(default, skip_serializing_if = "Option::is_none")] pub secboot: Option, diff --git a/reticle/Cargo.toml b/reticle/Cargo.toml index 2ec70af4..b873dc30 100755 --- a/reticle/Cargo.toml +++ b/reticle/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "reticle" -version = "0.4.8" +version = "0.5.0" edition = "2024" [lib] @@ -12,6 +12,7 @@ nvidia-attest = { path = "../nvidia-attest" } reqwest = "0.12.25" snp-attest = { path = "../snp-attest" } tdx-attest = { path = "../tdx-attest" } +azure-attest = { path = "../azure-attest" } thiserror = "2.0.17" wasm-bindgen = "0.2.106" wasm-bindgen-futures = "0.4.56" diff --git a/reticle/examples/endtoend.rs b/reticle/examples/endtoend.rs index 5563096d..8166dfc6 100644 --- a/reticle/examples/endtoend.rs +++ b/reticle/examples/endtoend.rs @@ -1,4 +1,5 @@ use reticle::{ClientBuilder, query::QueryParams}; +use snp_attest::kds::Kds; #[tokio::main] async fn main() { @@ -6,7 +7,12 @@ async fn main() { .nth(1) .expect("must supply api url as first argument"); - let client = ClientBuilder::new(&api_url).build().await.unwrap(); + let kds = Kds::default(); + let client = ClientBuilder::new(&api_url) + .with_kds(kds) + .build() + .await + .unwrap(); let result = client.attest().await.unwrap(); println!("{result:?}"); diff --git a/reticle/src/lib.rs b/reticle/src/lib.rs index b684abee..d065378b 100644 --- a/reticle/src/lib.rs +++ b/reticle/src/lib.rs @@ -5,16 +5,18 @@ pub mod gateway; pub mod query; pub mod rego; -use std::borrow::Cow; +use std::{borrow::Cow, pin::Pin}; -use futures::future::{Either, OptionFuture}; +use azure_attest::{AzureQuote, collateral::ReportVerifierBuilder, nonce::AzureNonce}; +use futures::future::OptionFuture; use libattest::{ CpuModule, GpuModule, Modules, bail, error::{AttestationError, Context, Expose}, + quote::QuoteVerifier, validation::{Validator, WithPolicy}, }; use nvidia_attest::{EATToken, keychain::KeyChain, nonce::NvidiaNonce}; -use snp_attest::{ParsedAttestation, kds::Kds, nonce::SevNonce}; +use snp_attest::{SevQuote, kds::Kds, nonce::SevNonce, verify::SevQuoteVerifier}; pub use nvidia_attest; use reqwest::{ @@ -23,7 +25,7 @@ use reqwest::{ }; pub use snp_attest; -use tdx_attest::{TdxQuote, nonce::TdxNonce, pcs::Pcs, verify::QuoteVerifier}; +use tdx_attest::{TdxQuote, nonce::TdxNonce, pcs::Pcs, verify::TdxQuoteVerifier}; #[cfg(target_family = "wasm")] use wasm_bindgen::prelude::*; @@ -205,15 +207,12 @@ impl Client { /// This method exposes core functionality and does not perform cryptographic /// or measurement checks on the attestation. If you want to perform end-to-end attestation /// please refer to [`Self::attest_sev`] - pub async fn request_sev( - &self, - nonce: &SevNonce, - ) -> Result { + pub async fn request_sev(&self, nonce: &SevNonce) -> Result { let url = self.url.join("/attestation/sev").unwrap(); let query = [("nonce", &nonce.to_hex())]; let response = self.request(url, &query).await?.bytes().await?; - let attestation = ParsedAttestation::new(&response)?; + let attestation = SevQuote::new(&response)?; Ok(attestation) } @@ -260,16 +259,57 @@ impl Client { Ok(quote) } + /// Requests and parses an Azure Quote + /// + pub async fn request_azure(&self, nonce: &AzureNonce) -> Result { + let url = self.url.join("/attestation/azure").unwrap(); + let query = [("nonce", &nonce.to_hex())]; + + let response: AzureQuote = self.request(url, &query).await?.json().await?; + + Ok(response) + } + + pub async fn attest_azure(&self) -> Result<(), AttestationError> { + let nonce = AzureNonce::generate(); + + let quote = self.request_azure(&nonce).await?; + let verifier = ReportVerifierBuilder::default() + .sev(async |quote| { + self.kds + .fetch_certificates(quote) + .await + .map(SevQuoteVerifier::new) + }) + .tdx(async |_| unimplemented!()) + .fetch_collateral("e) + .await?; + + let evidence = quote.verify(verifier, &nonce)?; + + let claims = WithPolicy::new("azure.allow", evidence); + + self.policy_validator + .verify_claim(claims)? + .or_err("azure claims did not match specified opa policy") + .expose_error()?; + + Ok(()) + } + /// Performs end-to-end sev-snp attestation. Generates nonce and validates claims all in one pub async fn attest_sev(&self) -> Result<(), AttestationError> { let nonce = SevNonce::generate(); let attestation = self.request_sev(&nonce).await?; let keychain = self.kds.fetch_certificates(&attestation).await?; - attestation.verify(&keychain, &nonce)?; + let verifier = SevQuoteVerifier::new(keychain); + verifier.verify(&attestation, &nonce)?; + + let claims = WithPolicy::new("sev.allow", attestation); self.policy_validator - .verify_claim(&attestation)? + .verify_claim(&claims)? .or_err("sev claims did not match specified OPA policy") .expose_error()?; @@ -288,17 +328,16 @@ impl Client { .context("failed fetching collateral from pcs server") .expose_error()?; - let claims = quote.body().clone(); - - let verifier = QuoteVerifier::new(collateral, quote); + let verifier = TdxQuoteVerifier::new(collateral); verifier - .verify(&nonce) + .verify("e, &nonce) .context("TDX quote verification has failed") .expose_error()?; - let claims = WithPolicy::new("tdx.allow", claims); + let claims = WithPolicy::new("tdx.allow", quote); + self.policy_validator - .verify_claim(claims)? + .verify_claim(&claims)? .or_err("tdx claims did not match specified OPA policy") .expose_error()?; @@ -335,11 +374,13 @@ impl Client { .context("failed to request modules from attestation server") .expose_error()?; - let cpu_attest = match modules.cpu() { - CpuModule::Sev => Either::Left(self.attest_sev()), - CpuModule::Tdx => Either::Right(self.attest_tdx()), - _ => bail!("we do not yet support the advertised cpu platform"), - }; + let cpu_attest: Pin>>> = + match modules.cpu() { + CpuModule::Sev => Box::pin(self.attest_sev()), + CpuModule::Tdx => Box::pin(self.attest_tdx()), + CpuModule::Azure => Box::pin(self.attest_azure()), + _ => bail!("we do not yet support the advertised cpu platform"), + }; let gpu_attest = match modules.gpu() { None => None, diff --git a/snp-attest/examples/read-report.rs b/snp-attest/examples/read-report.rs index d57ea698..4c7b9b6f 100644 --- a/snp-attest/examples/read-report.rs +++ b/snp-attest/examples/read-report.rs @@ -2,7 +2,7 @@ use sev::parser::ByteParser; fn main() { let report = sev::firmware::guest::AttestationReport::from_bytes( - &std::fs::read(std::env::args().nth(1).unwrap()).unwrap() + &std::fs::read(std::env::args().nth(1).unwrap()).unwrap(), ); println!("{:?}", report); -} \ No newline at end of file +} diff --git a/snp-attest/src/attestation/kds.rs b/snp-attest/src/attestation/kds.rs index 66ccfdcb..c29df892 100644 --- a/snp-attest/src/attestation/kds.rs +++ b/snp-attest/src/attestation/kds.rs @@ -7,7 +7,7 @@ use x509_cert::certificate::{CertificateInner, Rfc5280}; use wasm_bindgen::prelude::*; use crate::{ - ParsedAttestation, + SevQuote, chain::{CRL, VerifiedChain}, }; @@ -86,7 +86,7 @@ impl Kds { )) .query(&query); - let resp = req.send().await?.bytes().await?; + let resp = req.send().await?.error_for_status()?.bytes().await?; Ok(sev::certs::snp::Certificate::from_der(&resp).expect("invalid vcek from AMD KDS")) } @@ -107,7 +107,7 @@ impl Kds { pub async fn fetch_certificates( &self, - attestation: &ParsedAttestation, + attestation: &SevQuote, ) -> Result { log::info!("Fetching the chain from KDS"); let chain = self @@ -124,10 +124,7 @@ impl Kds { } /// Fetches the certificate revocation list from AMD's KDS - pub async fn fetch_crl( - &self, - attestation: &ParsedAttestation, - ) -> Result { + pub async fn fetch_crl(&self, attestation: &SevQuote) -> Result { let client = Client::new(); let req = client.get(format!( "{}/crl", diff --git a/snp-attest/src/attestation/mod.rs b/snp-attest/src/attestation/mod.rs index 4b10deab..12fd4d20 100644 --- a/snp-attest/src/attestation/mod.rs +++ b/snp-attest/src/attestation/mod.rs @@ -2,15 +2,19 @@ pub mod chain; /// Methods for interacting with AMD's keyserver pub mod kds; +pub mod verify; // pub mod nonce; -use libattest::error::{AttestationError, Context, Expose}; +use libattest::{ + error::{AttestationError, Context, Expose}, + validation::Verifiable, +}; #[cfg(target_family = "wasm")] use wasm_bindgen::prelude::*; -use crate::{kds::chipid_from_gen, nonce::SevNonce, oid}; +use crate::{claims::SevClaims, kds::chipid_from_gen, nonce::SevNonce, oid}; use der::Encode; use sev::{ CpuFamily, CpuModel, Generation, firmware::guest::AttestationReport, parser::ByteParser, @@ -23,7 +27,7 @@ use self::chain::VerifiedChain; /// Represents a parsed attestation report with some already /// parsed commonly accessed fields #[allow(unused)] -pub struct ParsedAttestation { +pub struct SevQuote { cpu_fam_id: CpuFamily, cpu_mod_id: CpuModel, generation: Generation, @@ -32,7 +36,7 @@ pub struct ParsedAttestation { } #[cfg_attr(target_family = "wasm", wasm_bindgen)] -impl ParsedAttestation { +impl SevQuote { /// Parses and constructs a new attestation report from a stream of binary data pub fn new(bytes: &[u8]) -> Result { let report = @@ -48,7 +52,7 @@ impl ParsedAttestation { let generation = sev::Generation::identify_cpu(cpu_fam_id, cpu_mod_id) .context("could not identify cpu from attestation report")?; - Ok(ParsedAttestation { + Ok(SevQuote { cpu_fam_id, cpu_mod_id, generation, @@ -65,7 +69,7 @@ impl ParsedAttestation { } /// Verifies the attestation report against a certificate chain - pub fn verify(&self, chain: &VerifiedChain, nonce: &SevNonce) -> Result<(), AttestationError> { + fn verify(&self, chain: &VerifiedChain, nonce: &SevNonce) -> Result<(), AttestationError> { // let certificates = chain.parse_certificates()?; // TODO: unify everything and use either x509-cert or x509-parser @@ -119,7 +123,7 @@ impl ParsedAttestation { } } -impl ParsedAttestation { +impl SevQuote { pub fn generation(&self) -> Generation { self.generation } @@ -128,3 +132,14 @@ impl ParsedAttestation { &self.report } } + +impl Verifiable for SevQuote { + fn claims(&self) -> SevClaims { + SevClaims::from(self.report()) + } + + type Claims<'a> + = SevClaims + where + Self: 'a; +} diff --git a/snp-attest/src/attestation/verify.rs b/snp-attest/src/attestation/verify.rs new file mode 100644 index 00000000..1b1dd9d3 --- /dev/null +++ b/snp-attest/src/attestation/verify.rs @@ -0,0 +1,22 @@ +use libattest::quote::QuoteVerifier; + +use crate::{SevQuote, chain::VerifiedChain, nonce::SevNonce}; + +pub struct SevQuoteVerifier { + collateral: VerifiedChain, +} + +impl SevQuoteVerifier { + pub fn new(collateral: VerifiedChain) -> Self { + Self { collateral } + } +} + +impl QuoteVerifier for SevQuoteVerifier { + type Nonce = SevNonce; + type Quote = SevQuote; + + fn verify(&self, quote: &SevQuote, nonce: &SevNonce) -> libattest::Result<()> { + quote.verify(&self.collateral, nonce) + } +} diff --git a/snp-attest/src/claims.rs b/snp-attest/src/claims.rs index bdef074e..d60aaabb 100644 --- a/snp-attest/src/claims.rs +++ b/snp-attest/src/claims.rs @@ -1,9 +1,6 @@ -use libattest::validation::WithPolicy; use serde::Serialize; use sev::firmware::guest::{AttestationReport, GuestPolicy}; -use crate::ParsedAttestation; - #[derive(Debug, Serialize)] pub struct GuestPolicyClaims { pub abi_minor: u64, @@ -60,10 +57,3 @@ impl From<&AttestationReport> for SevClaims { } } } - -impl libattest::validation::IntoClaims for &ParsedAttestation { - type Claims = WithPolicy; - fn into_claims(self) -> WithPolicy { - WithPolicy::new("sev.allow", SevClaims::from(self.report())) - } -} diff --git a/snp-attest/src/nonce.rs b/snp-attest/src/nonce.rs index 644ddf44..be51f24c 100644 --- a/snp-attest/src/nonce.rs +++ b/snp-attest/src/nonce.rs @@ -1,92 +1 @@ -use std::ops::Deref; - -#[cfg(target_family = "wasm")] -use wasm_bindgen::prelude::*; - -#[cfg_attr(target_family = "wasm", wasm_bindgen)] -pub struct SevNonce(libattest::ByteNonce<64>); - -impl Deref for SevNonce { - type Target = libattest::ByteNonce<64>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[cfg_attr(target_family = "wasm", wasm_bindgen)] -impl SevNonce { - pub fn generate() -> Self { - Self(libattest::ByteNonce::generate()) - } - - pub fn to_hex(&self) -> String { - self.0.to_hex() - } -} -// #[cfg_attr(target_family = "wasm", wasm_bindgen(constructor))] -// pub fn new() -> Self { -// let mut bytes = Box::new([0u8; 64]); - -// getrandom::getrandom(bytes.as_mut_slice()).unwrap(); - -// SevNonce(bytes) -// } - -// pub fn generate() -> Self { -// Self::new() -// } - -// pub fn to_hex(&self) -> String { -// hex::encode_upper(self.0.as_ref()) -// } -// } - -// impl Deref for SevNonce { -// type Target = [u8; 64]; - -// fn deref(&self) -> &Self::Target { -// &self.0 -// } -// } - -// impl SevNonce { -// pub fn get_bytes(&self) -> &[u8; 64] { -// &self.0 -// } -// } - -// impl Default for SevNonce { -// fn default() -> Self { -// Self::new() -// } -// } - -// impl From> for SevNonce { -// fn from(value: Box<[u8; 64]>) -> Self { -// Self(value) -// } -// } - -// impl TryFrom for SevNonce { -// type Error = anyhow::Error; - -// fn try_from(value: std::string::String) -> anyhow::Result { -// let mut b_arr: [u8; 64] = [0u8; 64]; - -// hex::decode(&value) -// .expect("invalid hex") -// .into_iter() -// .take(64) -// .enumerate() -// .for_each(|(i, val)| b_arr[i] = val); - -// Ok(SevNonce { bytes: b_arr }) -// } -// } - -// impl From<[u8; 64]> for SevNonce { -// fn from(value: [u8; 64]) -> Self { -// SevNonce { bytes: value } -// } -// } +libattest::define_nonce_type!(pub SevNonce, 64); diff --git a/tdx-attest/Cargo.toml b/tdx-attest/Cargo.toml index 38b7f7fa..a4c9fcbb 100644 --- a/tdx-attest/Cargo.toml +++ b/tdx-attest/Cargo.toml @@ -6,17 +6,14 @@ edition = "2024" [dependencies] # dcap-qvl = "0.3" # tdx-quote = "0.0.5" -wasm-bindgen.workspace = true -wasm-bindgen-futures.workspace = true libattest = { path = "../libattest" } thiserror = "2" zerocopy = { version = "0.8.39", features = ["derive"] } -p256 = "0.13.2" x509-cert = { version = "0.3.0-rc.4", features = ["pem"] } spki = "0.8.0-rc.4" der = { version = "0.8.0-rc.10", features = ["derive", "oid", "std"] } # x509-verify = { version = "0.4.8", features = ["ed25519", "p256", "sha2", "x509"], default-features = false } -signature = "2.2.0" +signature = "3.0" sec1 = "0.7.3" reqwest = { version = "0.13.2", features = ["json", "query"] } chrono = { version = "0.4.44", features = ["serde"] } @@ -29,6 +26,8 @@ const-oid = "0.10.2" sha2 = "0.10.9" [target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen.workspace = true +wasm-bindgen-futures.workspace = true web-time = "1.1" [dev-dependencies] diff --git a/tdx-attest/examples/parse.rs b/tdx-attest/examples/parse.rs index df5c1eb8..16c94c55 100644 --- a/tdx-attest/examples/parse.rs +++ b/tdx-attest/examples/parse.rs @@ -1,9 +1,9 @@ -use libattest::ByteNonce; +use libattest::{ByteNonce, quote::QuoteVerifier}; use tdx_attest::{ TdxQuote, dcap::parser::ParseErrorExt, pcs::Pcs, - verify::{self, QuoteVerifier}, + verify::{self, TdxQuoteVerifier}, }; // static DATA: [u8; 64] = [ @@ -17,19 +17,19 @@ static DATA: [u8; 64] = [0; 64]; #[tokio::main] async fn main() -> anyhow::Result<()> { - let input = std::fs::read("./examples/quote.test").unwrap(); - let pcs = Pcs::new("https://pccs.prem.io/")?; + // let input = std::fs::read("./examples/quote.test").unwrap(); + // let pcs = Pcs::new("https://pccs.prem.io/")?; - let quote = TdxQuote::from_bytes(&input)?; - let collateral = pcs.fetch_collateral("e).await?; + // let quote = TdxQuote::from_bytes(&input)?; + // let collateral = pcs.fetch_collateral("e).await?; - let nonce = ByteNonce::from(DATA).into(); - let verifier = QuoteVerifier::new(collateral, quote); + // let nonce = ByteNonce::from(DATA).into(); + // let verifier = TdxQuoteVerifier::new(collateral); - verifier.verify(&nonce)?; + // verifier.verify("e, &nonce)?; - println!("Verification success"); - // println!("{identity:?}"); + // println!("Verification success"); + // // println!("{identity:?}"); Ok(()) } diff --git a/tdx-attest/src/certificates.rs b/tdx-attest/src/certificates.rs index 3ea181ae..af43a4e8 100644 --- a/tdx-attest/src/certificates.rs +++ b/tdx-attest/src/certificates.rs @@ -1,34 +1,13 @@ pub mod ca; -pub mod crl; -pub mod error; pub mod extensions; -pub use error::CertificateError; - use std::fmt::Display; use std::time::SystemTime; -#[cfg(target_arch = "wasm32")] -pub(crate) fn now() -> SystemTime { - SystemTime::UNIX_EPOCH - + web_time::SystemTime::now() - .duration_since(web_time::SystemTime::UNIX_EPOCH) - .unwrap_or_default() -} - -#[cfg(not(target_arch = "wasm32"))] -pub(crate) fn now() -> SystemTime { - SystemTime::now() -} - use der::{ Encode, oid::db::rfc5912::{ECDSA_WITH_SHA_256, ID_EC_PUBLIC_KEY}, }; -use p256::{ - PublicKey, - ecdsa::{DerSignature, Signature, VerifyingKey, signature::Verifier}, -}; use spki::{DecodePublicKey, ObjectIdentifier}; use thiserror::Error; use x509_cert::{ @@ -51,213 +30,4 @@ impl IntermediateCa { } } -#[derive(Debug)] -pub struct EcdsaCert { - /// the original certificate where public_key and signature were derived from - certificate: x509_cert::Certificate, - /// this is the public key the certificate is trying to attest - public_key: VerifyingKey, - /// this is the signature attesting the authenticity and trustworthyness of the public key - signature: Signature, -} - -pub type PinnedCertificate = &'static EcdsaCert; - -impl EcdsaCert { - /// verifies that this certificate (`self`) contains a signed public key - /// that attests for the authenticity of the signature of another certificate (`other`) - /// - /// # Errors - /// Returns error if the two certificates cannot be linked by cryptographical means - pub fn verify_cert(&self, other: &Self) -> Result<(), CertificateError> { - let other_tbs = other.certificate.tbs_certificate().to_der()?; - self.public_key - .verify(&other_tbs, &other.signature) - .map_err(CertificateError::BadSignature)?; - - Ok(()) - } - - #[must_use] - pub fn cert(&self) -> &TbsCertificateInner { - self.certificate.tbs_certificate() - } - - /// verifies if the certificate is self-signed - /// - /// # Errors - /// Returns error if trust cannot be enstablished - /// within self - pub fn verify_self(&self) -> Result<(), CertificateError> { - self.verify_cert(self) - } - - /// Steps: - /// - decode signature and public key - /// - check for certificate validity - fn from_cert(certificate: x509_cert::Certificate) -> Result { - // Check that the signature and public key are in the - // format supported by the library (elliptic curve certificates) - certificate - .signature_algorithm() - .assert_algorithm_oid(ECDSA_WITH_SHA_256) - .map_err(CertificateError::WrongAlgorithm)?; - - certificate - .tbs_certificate() - .subject_public_key_info() - .algorithm - .assert_algorithm_oid(ID_EC_PUBLIC_KEY) - .map_err(CertificateError::WrongAlgorithm)?; - - // signature - let signature = certificate - .signature() - .as_bytes() - .ok_or(CertificateError::WrongFormat)?; - - let signature = - Signature::from_der(signature).expect("could not re-decode an encoded signature"); - - // signed public key - let public_key = certificate - .tbs_certificate() - .subject_public_key_info() - .subject_public_key - .as_bytes() - .ok_or(CertificateError::WrongFormat)?; - - let public_key = VerifyingKey::from_sec1_bytes(public_key) - .expect("could not re-decode an encoded public key"); - - // check for certificate validity - let not_after = certificate - .tbs_certificate() - .validity() - .not_after - .to_system_time(); - - let not_before = certificate - .tbs_certificate() - .validity() - .not_before - .to_system_time(); - - let now = now(); - - if not_before > now || not_after < now { - return Err(CertificateError::Expired); - } - - Ok(Self { - certificate, - public_key, - signature, - }) - } -} - -impl Verifier for EcdsaCert { - fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { - self.public_key.verify(msg, signature) - } -} - -impl TryFrom for EcdsaCert { - type Error = CertificateError; - fn try_from(value: x509_cert::Certificate) -> Result { - Self::from_cert(value) - } -} - -#[derive(Debug)] -/// represents a cryptographically verified -/// certificate chain -pub struct CertificateChain { - /// optional trust anchor of a pinned certificate - anchor: Option, - /// a chain of certificates. Last one is leaf. - chain: Vec, -} - -impl Verifier for CertificateChain { - // #[must_use] - fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { - let certificate = self.chain.last().or(self.anchor).unwrap(); - certificate.verify(msg, signature) - } -} - -impl Verifier for &CertificateChain { - fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), signature::Error> { - (*self).verify(msg, signature) - } -} - -impl CertificateChain { - #[must_use] - pub fn with_anchor(anchor: PinnedCertificate) -> Self { - Self { - anchor: Some(anchor), - chain: vec![], - } - } - - #[must_use] - /// Returns the leaf certificate or None if this certificate chain is empty - /// and does not have any anchor of trust - pub fn leaf(&self) -> Option<&EcdsaCert> { - self.chain.last().or(self.anchor) - } - - /// Inserts a new leaf into this certificate chain. The certificate is first - /// verified by the previous certificates - pub fn push_certificate(&mut self, other: EcdsaCert) -> Result<(), CertificateError> { - let verifier = self.chain.last().or(self.anchor); - - match verifier { - Some(verifier) => verifier.verify_cert(&other)?, - None => other.verify_self()?, - }; - - self.chain.push(other); - - Ok(()) - } - - pub fn parse_pem_chain(mut self, chain: &[u8]) -> Result { - let chain = chain.strip_suffix(b"\0").unwrap_or(chain); // chain from tdx could be 0 terminated so we do a little sanitization - - let chain: Vec = x509_cert::Certificate::load_pem_chain(chain)? - .into_iter() - .map(EcdsaCert::from_cert) - .collect::>()?; - - let mut chain = chain.into_iter().rev(); - - if let Some(anchor) = self.anchor { - // discard the root certificate from the pem chain if we already have our own embedded trust - let _root = chain.next().ok_or(CertificateError::BadChain)?; - } - - chain.try_for_each(|cert| self.push_certificate(cert))?; - - Ok(self) - } - - // verifies the current certificate chain with an out of bounds - // certificate, by veryfying it signs our root of trust - // fn verify_oob(&self, oob_root: &EcdsaCert) -> Result<(), CertificateError> { - // let mut chain = self.chain.iter(); - // let chain_root = chain.next().unwrap(); - - // oob_root.verify_cert(chain_root)?; - // if let Some(certificate) = chain.next() { - // // if there's an intermediate right after the root - // // certificate optionally verify that as well - // oob_root.verify_cert(certificate); - // } - - // Ok(()) - // } -} +// pub type CertificateL diff --git a/tdx-attest/src/certificates/ca.rs b/tdx-attest/src/certificates/ca.rs index 9b15bea7..9ad4a70f 100644 --- a/tdx-attest/src/certificates/ca.rs +++ b/tdx-attest/src/certificates/ca.rs @@ -1,11 +1,11 @@ use std::sync::LazyLock; use der::Decode; +use libattest::crypto::algorithms::{CertFormat, ecdsa::EcdsaCert}; use x509_cert::Certificate; -use crate::certificates::EcdsaCert; - static INTEL_CA_DER: &[u8; 659] = include_bytes!("./IntelCA.der"); -pub static INTEL_CA: LazyLock = - LazyLock::new(|| EcdsaCert::from_cert(Certificate::from_der(INTEL_CA_DER).unwrap()).unwrap()); +pub static INTEL_CA: LazyLock = LazyLock::new(|| { + EcdsaCert::from_certificate(Certificate::from_der(INTEL_CA_DER).unwrap()).unwrap() +}); diff --git a/tdx-attest/src/certificates/extensions.rs b/tdx-attest/src/certificates/extensions.rs index 48431948..a3d4e1cb 100644 --- a/tdx-attest/src/certificates/extensions.rs +++ b/tdx-attest/src/certificates/extensions.rs @@ -7,9 +7,10 @@ use der::{ asn1::{Int, ObjectIdentifier, OctetString, OctetStringRef, SequenceRef}, oid::{self, AssociatedOid, ObjectIdentifierRef}, }; +use libattest::error::Context; // use sec1::der::asn1::SequenceOf; -use crate::{certificates::CertificateError, dcap::types}; +use crate::dcap::types; #[derive(Sequence, Debug)] struct TaggedField<'a> { @@ -24,10 +25,10 @@ pub struct TcbExtension { } impl TcbExtension { - fn decode<'a>(mut from: AnyRef<'a>) -> Result { - let fields: Vec = from.decode_as()?; + fn decode<'a>(mut from: AnyRef<'a>) -> Option { + let fields: Vec = from.decode_as().ok()?; - let cpu_svn = fields.get(..16).ok_or(CertificateError::WrongFormat)?; + let cpu_svn = fields.get(..16)?; let cpu_svn = cpu_svn .iter() .map(|a| { @@ -35,23 +36,17 @@ impl TcbExtension { .then(|| a.data.decode_as::().ok()) .flatten() }) - .collect::>>() - .ok_or(CertificateError::WrongFormat)?; + .collect::>>()?; - let cpu_svn = cpu_svn - .try_into() - .map_err(|_| CertificateError::WrongFormat)?; - - let pce_svn = fields - .get(16) - .and_then(|TaggedField { data, .. }| { - (data.tag() == Tag::Integer) - .then(|| data.decode_as::().ok()) - .flatten() - }) - .ok_or(CertificateError::WrongFormat)?; + let cpu_svn = Box::<[u32; 16]>::try_from(cpu_svn).ok()?; + + let pce_svn = fields.get(16).and_then(|TaggedField { data, .. }| { + (data.tag() == Tag::Integer) + .then(|| data.decode_as::().ok()) + .flatten() + })?; - Ok(TcbExtension { cpu_svn, pce_svn }) + Some(TcbExtension { cpu_svn, pce_svn }) } } @@ -83,14 +78,17 @@ impl SgxExtension<'_> { } impl<'a> Decode<'a> for SgxExtension<'a> { - type Error = CertificateError; + type Error = der::Error; fn decode>(decoder: &mut R) -> Result { let sequence: TaggedField = decoder.decode()?; let extension = match sequence.object_identifier { Self::FMSPC_OID => Self::Fmspc(sequence.data.decode_as()?), - Self::TCB_OID => Self::Tcb(TcbExtension::decode(sequence.data)?), + Self::TCB_OID => Self::Tcb( + TcbExtension::decode(sequence.data) + .ok_or(der::Error::new(der::ErrorKind::Failed, Length::new(0)))?, + ), _ => Self::Unknown { identifier: sequence.object_identifier, value: sequence.data, @@ -126,7 +124,7 @@ impl AssociatedOid for SgxExtensions<'_> { } impl<'a> der::Decode<'a> for SgxExtensions<'a> { - type Error = CertificateError; + type Error = der::Error; fn decode>(decoder: &mut R) -> Result { let extensions: Vec> = decoder.decode()?; diff --git a/tdx-attest/src/error.rs b/tdx-attest/src/error.rs index e30b12bd..6949605f 100644 --- a/tdx-attest/src/error.rs +++ b/tdx-attest/src/error.rs @@ -1,30 +1,3 @@ -use std::{backtrace::Backtrace, convert::Infallible, fmt::Display}; - use libattest::error::AttestationError; -use crate::{certificates::CertificateError, dcap::parser::ParseError}; - pub type TdxError = AttestationError; -// #[derive(Error, Debug)] -// pub enum Error { -// #[error("certificate error: {0}")] -// Certificate(#[from] CertificateError), - -// #[error("error parsing the dcap quote: {0}")] -// Parse(#[from] ParseError), - -// #[error("cryptographic error: {0}")] -// Crypto(#[from] p256::ecdsa::Error), - -// #[error("this error")] -// SignatureFormat(#[from] sec1::Error), - -// #[error("error returned from pcs: {0}")] -// Reqwest(#[from] reqwest::Error), - -// #[error("failed parsing json data: {0}")] -// Json(#[from] serde_json::Error), - -// #[error("pcs response did not include a certificate chain header")] -// MissingCertChain, -// } diff --git a/tdx-attest/src/lib.rs b/tdx-attest/src/lib.rs index 8f1bacf4..0f88b27e 100644 --- a/tdx-attest/src/lib.rs +++ b/tdx-attest/src/lib.rs @@ -1,20 +1,13 @@ #![warn(clippy::pedantic)] #![allow(unused, clippy::missing_panics_doc)] -use p256::ecdsa::VerifyingKey; -use p256::{EncodedPoint, elliptic_curve}; -use p256::{PublicKey, ecdsa::Signature}; - use crate::certificates::ca; use crate::certificates::extensions::{SgxExtension, SgxExtensions}; use crate::dcap::TdQuote; use crate::dcap::parser::Parse; use crate::error::TdxError; -use crate::{ - certificates::CertificateChain, - dcap::types::{EnclaveReport, TdxQuoteBody, TdxQuoteHeader}, -}; +use crate::dcap::types::{EnclaveReport, TdxQuoteBody, TdxQuoteHeader}; pub(crate) mod certificates; pub mod dcap; diff --git a/tdx-attest/src/nonce.rs b/tdx-attest/src/nonce.rs index 648100e4..bb4cd977 100644 --- a/tdx-attest/src/nonce.rs +++ b/tdx-attest/src/nonce.rs @@ -1,35 +1 @@ -use std::ops::Deref; - -use libattest::ByteNonce; - -#[cfg(target_family = "wasm")] -use wasm_bindgen::prelude::*; - -#[cfg_attr(target_family = "wasm", wasm_bindgen)] -pub struct TdxNonce(libattest::ByteNonce<64>); - -impl Deref for TdxNonce { - type Target = libattest::ByteNonce<64>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[cfg_attr(target_family = "wasm", wasm_bindgen)] -impl TdxNonce { - #[cfg_attr(target_family = "wasm", wasm_bindgen(constructor))] - pub fn generate() -> Self { - Self(libattest::ByteNonce::generate()) - } - - pub fn to_hex(&self) -> String { - self.0.to_hex() - } -} - -impl From> for TdxNonce { - fn from(value: ByteNonce<64>) -> Self { - Self(value) - } -} +libattest::define_nonce_type!(pub TdxNonce, 64); diff --git a/tdx-attest/src/pcs.rs b/tdx-attest/src/pcs.rs index 5534c702..84a20679 100644 --- a/tdx-attest/src/pcs.rs +++ b/tdx-attest/src/pcs.rs @@ -4,15 +4,17 @@ pub mod tcb; use std::str::FromStr; -use libattest::error::Context; -use p256::ecdsa::Signature; +use libattest::{ + crypto::{CertificateChain, algorithms::ecdsa::EcdsaCert, chain::crl::Crl}, + error::Context, +}; use reqwest::{Client, IntoUrl, Url}; use serde::Deserialize; use x509_cert::Certificate; use crate::{ TdxQuote, - certificates::{CertificateChain, IntermediateCa, ca::INTEL_CA, crl::Crl}, + certificates::{IntermediateCa, ca::INTEL_CA}, dcap::types::Fmspc, error::TdxError, pcs::{qe::EnclaveIdentity, signed_response::ParseSignedResponse, tcb::TcbInfo}, @@ -60,7 +62,7 @@ impl Pcs { .get("SGX-PCK-CRL-Issuer-Chain") .context("crl response does not contain a certificate chain")?; - let chain = CertificateChain::with_anchor(&INTEL_CA) + let chain = CertificateChain::::with_anchor(&INTEL_CA) .parse_pem_chain(&urlencoding::decode_binary(certificate_chain.as_bytes())) .context("failed parsing crl certificate chain")?; @@ -68,7 +70,7 @@ impl Pcs { // TODO: review this very bad thing. We really should build our own pccs already. let crl = hex::decode(crl)?; - let crl = Crl::from_der(&chain, crl).context("failed parsing and verifying crl")?; + let crl = Crl::from_der(chain, crl).context("failed parsing and verifying crl")?; Ok(crl) } diff --git a/tdx-attest/src/pcs/signed_response.rs b/tdx-attest/src/pcs/signed_response.rs index 5412e17b..bef74276 100644 --- a/tdx-attest/src/pcs/signed_response.rs +++ b/tdx-attest/src/pcs/signed_response.rs @@ -3,14 +3,15 @@ use std::marker::PhantomData; use crate::{ca::INTEL_CA, error::TdxError}; use anyhow::bail; use chrono::Utc; -use libattest::error::Context; -use p256::ecdsa::Signature; +use libattest::p256::ecdsa::Signature; +use libattest::{ + crypto::{CertificateChain, algorithms::ecdsa::EcdsaCert}, + error::Context, +}; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_json::Value; use signature::Verifier; -use crate::certificates::CertificateChain; - #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct Header { @@ -20,7 +21,7 @@ struct Header { /// Every signed response from the PCS has these 3 things. pub struct SignedResponse { - chain: CertificateChain, + chain: CertificateChain, signature: Signature, header: Header, @@ -90,7 +91,7 @@ impl ParseSignedResponse for reqwest::Response { // anchor our trust in embedded intel_ca, still parsing pem chain from // response - let chain = CertificateChain::with_anchor(&INTEL_CA) + let chain = CertificateChain::::with_anchor(&INTEL_CA) .parse_pem_chain(&chain) .context("failed parsing certificate chain from header")?; diff --git a/tdx-attest/src/quote.rs b/tdx-attest/src/quote.rs index 4a3c4c4e..c914f4b3 100644 --- a/tdx-attest/src/quote.rs +++ b/tdx-attest/src/quote.rs @@ -1,8 +1,13 @@ -use libattest::error::Context; +use libattest::{ + crypto::{CertificateChain, algorithms::ecdsa::EcdsaCert}, + error::Context, + p256, + validation::Verifiable, +}; use p256::ecdsa::{Signature, VerifyingKey}; use crate::{ - certificates::{CertificateChain, ca, extensions::SgxExtensions}, + certificates::{ca, extensions::SgxExtensions}, dcap::{ self, TdQuote, parser::Parse, @@ -27,7 +32,7 @@ pub enum CertificationData { PlainText(Vec), EncryptedCpuSvnsRSA2048(Vec), EncryptedCpuSvnsRSA3072(Vec), - PckChain(CertificateChain), + PckChain(CertificateChain), QeReportCertificationData(Box), } @@ -37,7 +42,7 @@ impl CertificationData { /// /// Returns Some if the leaf of this data structure has a certificate chain, None if /// trust is based on other identifiers - pub fn pck_chain(&self) -> Option<&CertificateChain> { + pub fn pck_chain(&self) -> Option<&CertificateChain> { match self { Self::QeReportCertificationData(data) => data.certification_data.pck_chain(), Self::PckChain(chain) => Some(chain), @@ -119,6 +124,14 @@ impl TdxQuote { } } +impl Verifiable for TdxQuote { + type Claims<'a> = &'a TdxQuoteBody; + + fn claims(&self) -> Self::Claims<'_> { + &self.body + } +} + /// public keys are encoded in dcap without the header for sec1 (0x04) /// so we have to add it manually fn decode_public_key(public_key: &[u8; 64]) -> Result { @@ -169,9 +182,9 @@ impl TryFrom> for CertificationData { PlainText(x) => Self::PlainText(x.to_owned()), EncryptedCpuSvnsRSA2048(x) => Self::EncryptedCpuSvnsRSA2048(x.to_owned()), EncryptedCpuSvnsRSA3072(x) => Self::EncryptedCpuSvnsRSA3072(x.to_owned()), - PckChain(chain) => { - Self::PckChain(CertificateChain::with_anchor(&ca::INTEL_CA).parse_pem_chain(chain)?) - } + PckChain(chain) => Self::PckChain( + CertificateChain::::with_anchor(&ca::INTEL_CA).parse_pem_chain(chain)?, + ), QeReportCertificationData(report) => { Self::QeReportCertificationData(Box::new((*report).try_into()?)) } diff --git a/tdx-attest/src/verify.rs b/tdx-attest/src/verify.rs index 580738a5..8ebf9e4c 100644 --- a/tdx-attest/src/verify.rs +++ b/tdx-attest/src/verify.rs @@ -1,13 +1,12 @@ use std::ops::Deref; -use libattest::error::Context; +use libattest::{crypto::chain::crl::VerifyCrl, error::Context, quote::QuoteVerifier}; use sha2::{Digest, Sha256, digest::Update}; use signature::Verifier; use zerocopy::IntoBytes; use crate::{ TdxCertification, TdxQuote, - certificates::crl::VerifyCrl, dcap::types::{ReportData, TdxQuoteBody}, error::TdxError, nonce::TdxNonce, @@ -262,25 +261,30 @@ fn verify_mask(quote: &[u8; N], expected: &[u8; N], mask: &[u8; } #[cfg_attr(target_family = "wasm", wasm_bindgen)] -pub struct QuoteVerifier { +pub struct TdxQuoteVerifier { collateral: Collateral, - quote: TdxQuote, + // quote: TdxQuote, minimum_tcb_level: TcbStatus, } #[cfg_attr(target_family = "wasm", wasm_bindgen)] -impl QuoteVerifier { +impl TdxQuoteVerifier { #[cfg_attr(target_family = "wasm", wasm_bindgen(constructor))] - pub fn new(collateral: Collateral, quote: TdxQuote) -> Self { + #[must_use] + pub fn new(collateral: Collateral) -> Self { Self { collateral, - quote, minimum_tcb_level: TcbStatus::UpToDate, } } +} + +impl QuoteVerifier for TdxQuoteVerifier { + type Nonce = TdxNonce; + type Quote = TdxQuote; - pub fn verify(&self, nonce: &TdxNonce) -> Result<(), TdxError> { - let tcb_levels = verify(&self.quote, &self.collateral, nonce)?; + fn verify(&self, quote: &TdxQuote, nonce: &TdxNonce) -> Result<(), TdxError> { + let tcb_levels = verify(quote, &self.collateral, nonce)?; let minimum_tcb = &self.minimum_tcb_level; if tcb_levels.qe_tcb.tcb_status < self.minimum_tcb_level {