diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5e75f595..666bd15b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,8 +17,8 @@ jobs: command: fmt args: --all -- --check - clippy: - name: cargo clippy + clippy-openssl: + name: cargo clippy openssl runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -31,7 +31,23 @@ jobs: - uses: actions-rs/cargo@v1 with: command: clippy - args: --all-features --all-targets -- -D clippy::all -D unused_imports -Dwarnings + args: --features=openssl,hw_tests,dangerous_hw_tests --all-targets -- -D clippy::all -D unused_imports -Dwarnings + + clippy-crypto_nossl: + name: cargo clippy crypto_nossl + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + components: clippy + toolchain: nightly + profile: minimal + override: true + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: --features=crypto_nossl,hw_tests,dangerous_hw_tests --all-targets -- -D clippy::all -D unused_imports -Dwarnings readme: name: cargo readme diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9614ad77..d10353a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,8 @@ on: [push, pull_request] name: test jobs: - sw: - name: sw ${{ matrix.toolchain }} ${{ matrix.profile.name }} ${{ matrix.features }} + sw-openssl: + name: sw openssl ${{ matrix.toolchain }} ${{ matrix.profile.name }} ${{ matrix.features }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -27,3 +27,30 @@ jobs: flag: --release features: - openssl + + sw-crypto_nossl: + name: sw crypto_nossl ${{ matrix.toolchain }} ${{ matrix.profile.name }} ${{ matrix.features }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + override: true + - uses: actions-rs/cargo@v1 + with: + command: test + args: ${{ matrix.profile.flag }} --features=${{ matrix.features }} + strategy: + fail-fast: false + matrix: + toolchain: + - nightly + - beta + - stable + profile: + - name: debug + - name: release + flag: --release + features: + - crypto_nossl diff --git a/Cargo.lock b/Cargo.lock index b5bdd145..f8d75c50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bincode" version = "1.3.3" @@ -35,6 +47,21 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cc" version = "1.0.83" @@ -56,6 +83,43 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12170080f3533d6f09a19f81596f836854d0fa4867dc32c8172b8474b4e9de61" +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -69,6 +133,42 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + [[package]] name = "dirs" version = "5.0.1" @@ -90,6 +190,57 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ecdsa" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "flagset" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a7e408202050813e6f1d9addadcaafef3dca7530c7ddfb005d4081cce6779" + [[package]] name = "foreign-types" version = "0.3.2" @@ -182,6 +333,17 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -193,6 +355,17 @@ dependencies = [ "wasi", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "hashbrown" version = "0.14.1" @@ -205,6 +378,24 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "iocuddle" version = "0.1.1" @@ -236,6 +427,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -243,6 +437,12 @@ version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "lock_api" version = "0.4.10" @@ -265,6 +465,54 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -315,6 +563,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -338,6 +598,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -350,12 +619,48 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "primeorder" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.67" @@ -374,6 +679,35 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -403,12 +737,56 @@ dependencies = [ "thiserror", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rsa" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "serde" version = "1.0.188" @@ -479,6 +857,7 @@ dependencies = [ "bincode", "bitfield", "bitflags 1.3.2", + "byteorder", "codicon", "dirs", "hex", @@ -488,12 +867,37 @@ dependencies = [ "lazy_static", "libc", "openssl", + "p384", + "rsa", "serde", "serde-big-array", "serde_bytes", "serial_test", + "sha2", "static_assertions", "uuid", + "x509-cert", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest", + "rand_core", ] [[package]] @@ -511,12 +915,34 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "2.0.37" @@ -548,6 +974,12 @@ dependencies = [ "syn", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -569,6 +1001,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "vmm-sys-util" version = "0.11.2" @@ -650,3 +1088,20 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "x509-cert" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25eefca1d99701da3a57feb07e5079fc62abba059fc139e98c13bbb250f3ef29" +dependencies = [ + "const-oid", + "der", + "spki", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/Cargo.toml b/Cargo.toml index 9387acff..96ec1660 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ hw_tests = [] dangerous_hw_tests = ["hw_tests"] sev = [] snp = [] +crypto_nossl = ["dep:p384", "dep:rsa", "dep:sha2", "dep:x509-cert"] [target.'cfg(target_os = "linux")'.dependencies] iocuddle = "0.1" @@ -61,6 +62,11 @@ bincode = "^1.3" hex = "0.4.3" libc = "0.2.147" lazy_static = "1.4.0" +p384 = { version = "0.13.0", optional = true } +rsa = { version = "0.9.2", optional = true } +sha2 = { version = "0.10.8", optional = true } +x509-cert = { version = "0.2.4", optional = true } +byteorder = "1.4.3" [dev-dependencies] kvm-ioctls = ">=0.12" diff --git a/README.md b/README.md index 6cf79f14..43957a23 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,16 @@ Refer to the [`firmware`] module for more information. Refer to the [`launch`] module for more information. +### Cryptographic Verification + +To enable the cryptographic verification of certificate chains and +attestation reports, either the `openssl` or `crypto_nossl` feature +has to be enabled manually. With `openssl`, OpenSSL is used for the +verification. With `crypto_nossl`, OpenSSL is _not_ used for the +verification and instead pure-Rust libraries (e.g., `p384`, `rsa`, +etc.) are used. `openssl` and `crypto_nossl` are mutually exclusive, +and enabling both at the same time leads to a compiler error. + ### Remarks Note that the Linux kernel provides access to these APIs through a set diff --git a/src/certs/snp/builtin/genoa/mod.rs b/src/certs/snp/builtin/genoa/mod.rs index b92edade..2a821159 100644 --- a/src/certs/snp/builtin/genoa/mod.rs +++ b/src/certs/snp/builtin/genoa/mod.rs @@ -10,12 +10,12 @@ pub const ASK: &[u8] = include_bytes!("ask.pem"); /// Get the Genoa ARK Certificate. pub fn ark() -> Result { - Ok(Certificate::from(X509::from_pem(ARK)?)) + Certificate::from_pem(ARK) } /// Get the Genoa ASK Certificate. pub fn ask() -> Result { - Ok(Certificate::from(X509::from_pem(ASK)?)) + Certificate::from_pem(ASK) } mod tests { diff --git a/src/certs/snp/builtin/milan/mod.rs b/src/certs/snp/builtin/milan/mod.rs index 84c03ab1..574db92e 100644 --- a/src/certs/snp/builtin/milan/mod.rs +++ b/src/certs/snp/builtin/milan/mod.rs @@ -10,12 +10,12 @@ pub const ASK: &[u8] = include_bytes!("ask.pem"); /// Get the Milan ARK Certificate. pub fn ark() -> Result { - Ok(Certificate::from(X509::from_pem(ARK)?)) + Certificate::from_pem(ARK) } /// Get the Milan ASK Certificate. pub fn ask() -> Result { - Ok(Certificate::from(X509::from_pem(ASK)?)) + Certificate::from_pem(ASK) } mod tests { diff --git a/src/certs/snp/cert.rs b/src/certs/snp/cert.rs index 072ce357..cde15420 100644 --- a/src/certs/snp/cert.rs +++ b/src/certs/snp/cert.rs @@ -3,10 +3,11 @@ use super::*; use openssl::pkey::{PKey, Public}; +use openssl::x509::X509; /// Structures/interfaces for SEV-SNP certificates. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Certificate(X509); /// Wrap an X509 struct into a Certificate. diff --git a/src/certs/snp/cert_nossl.rs b/src/certs/snp/cert_nossl.rs new file mode 100644 index 00000000..88aeb1a4 --- /dev/null +++ b/src/certs/snp/cert_nossl.rs @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 + +use super::*; + +use der::{referenced::OwnedToRef, Decode, DecodePem, Encode}; +use rsa::signature; // re-export of signature crate +use signature::Verifier; +use spki::ObjectIdentifier; +use std::convert::TryFrom; +use std::io; +use std::io::ErrorKind; +use x509_cert::der; // re-export of der crate +use x509_cert::spki; // re-export of spki crate + +/// Structures/interfaces for SEV-SNP certificates. + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Certificate(x509_cert::Certificate); + +const RSA_SSA_PSS_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.10"); + +/// Verify if the public key of one Certificate signs another Certificate. +impl Verifiable for (&Certificate, &Certificate) { + type Output = (); + + fn verify(self) -> Result { + let signer = &self.0 .0; + let signee = &self.1 .0; + + if signee.signature_algorithm.oid != RSA_SSA_PSS_OID { + return Err(io_error_other(format!( + "unsupported signature algorithm: {:?}", + signee.signature_algorithm + ))); + } + + let rsa_verifying_key = { + let signer_spki_ref = signer + .tbs_certificate + .subject_public_key_info + .owned_to_ref(); + let signer_pubkey_rsa = rsa::RsaPublicKey::try_from(signer_spki_ref) + .map_err(|e| io_error_other(format!("invalid RSA public key: {e:?}")))?; + rsa::pss::VerifyingKey::::new(signer_pubkey_rsa) + }; + + let message = signee.tbs_certificate.to_der().map_err(|e| { + io_error_other(format!("failed to encode tbs_certificate as DER: {e:?}")) + })?; + + let rsa_signature = rsa::pss::Signature::try_from(signee.signature.raw_bytes()) + .map_err(|e| io_error_other(format!("invalid RSA signature: {e:?}")))?; + + rsa_verifying_key + .verify(&message, &rsa_signature) + .map_err(|e| { + io_error_other(format!( + "Signer certificate does not RSA sign signee certificate: {e}" + )) + }) + } +} + +impl Certificate { + /// Create a Certificate from a PEM-encoded X509 structure. + pub fn from_pem(pem: &[u8]) -> Result { + let cert = x509_cert::Certificate::from_pem(pem) + .map_err(|e| io::Error::new(ErrorKind::InvalidData, format!("invalid PEM: {}", e)))?; + Ok(Self(cert)) + } + + /// Serialize a Certificate struct to PEM. + pub fn to_pem(&self) -> Result> { + use der::EncodePem; + Ok(self + .0 + .to_pem(der::pem::LineEnding::default()) + .map_err(|e| io_error_other(format!("PEM-encoding failed: {}", e)))? + .into_bytes()) + } + + /// Create a Certificate from a DER-encoded X509 structure. + pub fn from_der(der: &[u8]) -> Result { + let cert = x509_cert::Certificate::from_der(der) + .map_err(|e| io::Error::new(ErrorKind::InvalidData, format!("invalid DER: {}", e)))?; + Ok(Self(cert)) + } + + /// Serialize a Certificate struct to DER. + pub fn to_der(&self) -> Result> { + self.0 + .to_der() + .map_err(|e| io_error_other(format!("DER-encoding failed: {e:?}"))) + } + + /// Retrieve the public key in SEC1 encoding. + pub fn public_key_sec1(&self) -> &[u8] { + self.0 + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes() + } +} + +fn io_error_other>(error: S) -> io::Error { + io::Error::new(ErrorKind::Other, error.into()) +} diff --git a/src/certs/snp/ecdsa/mod.rs b/src/certs/snp/ecdsa/mod.rs index 01fcd537..c582e4d2 100644 --- a/src/certs/snp/ecdsa/mod.rs +++ b/src/certs/snp/ecdsa/mod.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -#[cfg(feature = "openssl")] +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] use super::*; use crate::util::hexdump; @@ -8,7 +8,7 @@ use crate::util::hexdump; #[cfg(feature = "openssl")] use crate::certs::snp::{AsLeBytes, FromLe}; -#[cfg(feature = "openssl")] +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] use std::convert::TryFrom; use serde::{Deserialize, Serialize}; @@ -121,6 +121,29 @@ impl TryFrom<&Signature> for ecdsa::EcdsaSig { } } +#[cfg(feature = "crypto_nossl")] +impl TryFrom<&Signature> for p384::ecdsa::Signature { + type Error = Error; + + #[inline] + fn try_from(signature: &Signature) -> Result { + let r_big_endian: Vec = signature.r.iter().copied().take(48).rev().collect(); + let s_big_endian: Vec = signature.s.iter().copied().take(48).rev().collect(); + + use p384::elliptic_curve::generic_array::GenericArray; + p384::ecdsa::Signature::from_scalars( + GenericArray::clone_from_slice(&r_big_endian), + GenericArray::clone_from_slice(&s_big_endian), + ) + .map_err(|e| { + Error::new( + ErrorKind::Other, + format!("failed to deserialize signature from scalars: {e:?}"), + ) + }) + } +} + #[cfg(feature = "openssl")] impl TryFrom<&Signature> for Vec { type Error = Error; diff --git a/src/certs/snp/mod.rs b/src/certs/snp/mod.rs index dc27ebb6..73f8524c 100644 --- a/src/certs/snp/mod.rs +++ b/src/certs/snp/mod.rs @@ -3,39 +3,40 @@ /// ECDSA signatures. pub mod ecdsa; -#[cfg(feature = "openssl")] +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] /// Certificate Authority (CA) certificates. pub mod ca; -#[cfg(feature = "openssl")] +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] /// Built-in certificates for Milan and Genoa machines. pub mod builtin; #[cfg(feature = "openssl")] mod cert; +#[cfg(feature = "crypto_nossl")] +mod cert_nossl; -#[cfg(feature = "openssl")] +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] mod chain; #[cfg(feature = "openssl")] pub use cert::Certificate; +#[cfg(feature = "crypto_nossl")] +pub use cert_nossl::Certificate; -#[cfg(feature = "openssl")] +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] pub use chain::Chain; use std::io::Result; -#[cfg(feature = "openssl")] +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] use std::io::{Error, ErrorKind}; -#[cfg(feature = "openssl")] -use openssl::x509::X509; - #[cfg(feature = "openssl")] #[allow(dead_code)] struct Body; -#[cfg(feature = "openssl")] +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] /// An interface for types that may contain entities such as /// signatures that must be verified. pub trait Verifiable { diff --git a/src/error.rs b/src/error.rs index ab9c05ee..abb4fe2e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 +use bincode; use std::{ + array::TryFromSliceError, convert::From, error, fmt::{Debug, Display}, @@ -595,3 +597,232 @@ impl From> for c_int { } } } + +#[derive(Debug)] +/// Errors which may be encountered when building custom guest context. +pub enum GCTXError { + /// Malformed guest context page. + InvalidPageSize(usize, usize), + + /// Block size data was the incorrect size + InvalidBlockSize, + + /// Unknown Error. + UnknownError, +} + +impl std::fmt::Display for GCTXError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + GCTXError::InvalidPageSize(actual, expected) => write!( + f, + "Page information was not the correct length ({actual} vs {expected})" + ), + GCTXError::InvalidBlockSize => { + write!(f, "Provided data does not conform to a 4096 block size") + } + GCTXError::UnknownError => write!(f, "An unknown Guest Context error encountered"), + } + } +} + +impl std::error::Error for GCTXError {} + +#[derive(Debug)] +/// Errors which may be encountered when handling OVMF data +pub enum OVMFError { + /// An invalid section type was provided for OVMF METADATA + InvalidSectionType, + + /// Part of the SEV METADATA failed verification + SEVMetadataVerification(String), + + /// Desired entry is missing from table + EntryMissingInTable(String), + + /// Invalid Entry Size was provided + InvalidSize(String, usize, usize), + + /// GUID doesn't match expected GUID + MismatchingGUID(), + + /// Unknown Error. + UnknownError, +} + +impl std::fmt::Display for OVMFError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + OVMFError::InvalidSectionType => write!(f, "An invalid section type was found"), + OVMFError::SEVMetadataVerification(section) => { + write!(f, "Wrong SEV metadata {section}") + } + OVMFError::EntryMissingInTable(entry) => { + write!(f, "Can't find {entry} entry in OVMF table") + } + OVMFError::InvalidSize(entry, actual, expected) => { + write!(f, "Invalid size of {entry}: {actual} < {expected}") + } + OVMFError::MismatchingGUID() => { + write!(f, "OVMF table footer GUID does not match expected GUID") + } + OVMFError::UnknownError => write!(f, "An unknown Guest Context error encountered"), + } + } +} + +impl std::error::Error for OVMFError {} + +#[derive(Debug)] +/// Errors which may be encountered when building SEV hashes. +pub enum SevHashError { + /// Provided page has invalid size + InvalidSize(usize, usize), + + /// Provided page has invalid offset + InvalidOffset(usize, usize), + + /// Unknown Error. + UnknownError, +} + +impl std::fmt::Display for SevHashError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + SevHashError::InvalidOffset(actual, expected) => { + write!(f, "Invalid page Offset: {actual} vs {expected}") + } + SevHashError::InvalidSize(actual, expected) => { + write!(f, "Invalid page Size: {actual} vs {expected}") + } + SevHashError::UnknownError => write!(f, "An unknown Guest Context error encountered"), + } + } +} + +impl std::error::Error for SevHashError {} + +#[derive(Debug)] +/// Errors which may be encountered when calculating the guest measurement. +pub enum MeasurementError { + /// TryFrom Slice Error handling + FromSliceError(TryFromSliceError), + + /// UUID Error handling + UUIDError(uuid::Error), + + /// Bincode Error Handling + BincodeError(bincode::ErrorKind), + + /// File Error Handling + FileError(std::io::Error), + + /// Vec from hex Error Handling + FromHexError(hex::FromHexError), + + /// Guest Context Error Handling + GCTXError(GCTXError), + + /// OVMF Error Handling + OVMFError(OVMFError), + + /// SEV Hash Error Handling + SevHashError(SevHashError), + + /// Invalid VCPU provided + InvalidVcpuTypeError(String), + + /// Invalid VMM Provided + InvalidVmmError(String), + + /// Invalid SEV Mode provided + InvalidSevModeError(String), + + /// Kernel specified for wrong OVMF + KernelSpecifiedError, + + /// OVMF is missing required section with kernel specified + MissingSection(String), +} + +impl std::fmt::Display for MeasurementError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + MeasurementError::FromSliceError(e) => write!(f, "Error converting slice: {e}"), + MeasurementError::UUIDError(e) => write!(f, "UUID Error encountered: {e}"), + MeasurementError::BincodeError(e) => write!(f, "Bincode error encountered: {e}"), + MeasurementError::FileError(e) => write!(f, "Failed handling file: {e}"), + MeasurementError::FromHexError(e) => write!(f, "Converting hex to vector error: {e}"), + MeasurementError::GCTXError(e) => write!(f, "GCTX Error Encountered: {e}"), + MeasurementError::OVMFError(e) => write!(f, "OVMF Error Encountered: {e}"), + MeasurementError::SevHashError(e) => write!(f, "Sev hash Error Encountered: {e}"), + MeasurementError::InvalidVcpuTypeError(value) => { + write!(f, "Invalid VCPU type value provided: {value}") + } + MeasurementError::InvalidVmmError(value) => { + write!(f, "Invalid VMM type provided: {value}") + } + MeasurementError::InvalidSevModeError(value) => { + write!(f, "Invalid SEV mode provided: {value}") + } + MeasurementError::KernelSpecifiedError => write!( + f, + "Kernel specified but OVMF doesn't support kernel/initrd/cmdline measurement" + ), + MeasurementError::MissingSection(section) => write!( + f, + "Kernel specified but OVMF metadata doesn't include {section} section" + ), + } + } +} + +impl std::error::Error for MeasurementError {} + +impl std::convert::From for MeasurementError { + fn from(value: TryFromSliceError) -> Self { + Self::FromSliceError(value) + } +} + +impl std::convert::From for MeasurementError { + fn from(value: uuid::Error) -> Self { + Self::UUIDError(value) + } +} + +impl std::convert::From for MeasurementError { + fn from(value: bincode::ErrorKind) -> Self { + Self::BincodeError(value) + } +} + +impl std::convert::From for MeasurementError { + fn from(value: std::io::Error) -> Self { + Self::FileError(value) + } +} + +impl std::convert::From for MeasurementError { + fn from(value: hex::FromHexError) -> Self { + Self::FromHexError(value) + } +} + +impl std::convert::From for MeasurementError { + fn from(value: GCTXError) -> Self { + Self::GCTXError(value) + } +} + +impl std::convert::From for MeasurementError { + fn from(value: OVMFError) -> Self { + Self::OVMFError(value) + } +} + +impl std::convert::From for MeasurementError { + fn from(value: SevHashError) -> Self { + Self::SevHashError(value) + } +} diff --git a/src/firmware/guest/types/snp.rs b/src/firmware/guest/types/snp.rs index 6764a138..fd501648 100644 --- a/src/firmware/guest/types/snp.rs +++ b/src/firmware/guest/types/snp.rs @@ -2,12 +2,12 @@ use crate::{certs::snp::ecdsa::Signature, firmware::host::TcbVersion, util::hexdump}; -#[cfg(feature = "openssl")] +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] use crate::certs::snp::{Chain, Verifiable}; use std::fmt::Display; -#[cfg(feature = "openssl")] +#[cfg(any(feature = "openssl", feature = "crypto_nossl"))] use std::{ convert::TryFrom, io::{self, Error, ErrorKind}, @@ -348,6 +348,48 @@ impl Verifiable for (&Chain, &AttestationReport) { } } +#[cfg(feature = "crypto_nossl")] +impl Verifiable for (&Chain, &AttestationReport) { + type Output = (); + + fn verify(self) -> io::Result { + // According to Chapter 3 of the [Versioned Chip Endorsement Key (VCEK) Certificate and + // KDS Interface Specification][spec], the VCEK certificate certifies an ECDSA public key on curve P-384, + // and the signature hash algorithm is sha384. + // [spec]: https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/57230.pdf + + let vcek = self.0.verify()?; + + let sig = p384::ecdsa::Signature::try_from(&self.1.signature)?; + + let measurable_bytes: &[u8] = &bincode::serialize(self.1).map_err(|e| { + Error::new( + ErrorKind::Other, + format!("Unable to serialize bytes: {}", e), + ) + })?[..0x2a0]; + + use sha2::Digest; + let base_digest = sha2::Sha384::new_with_prefix(measurable_bytes); + + let verifying_key = p384::ecdsa::VerifyingKey::from_sec1_bytes(vcek.public_key_sec1()) + .map_err(|e| { + io::Error::new( + ErrorKind::Other, + format!("failed to deserialize public key from sec1 bytes: {e:?}"), + ) + })?; + + use p384::ecdsa::signature::DigestVerifier; + verifying_key.verify_digest(base_digest, &sig).map_err(|e| { + io::Error::new( + ErrorKind::Other, + format!("VCEK does not sign the attestation report: {e:?}"), + ) + }) + } +} + bitfield! { /// The firmware associates each guest with a guest policy that the guest owner provides. The /// firmware restricts what actions the hypervisor can take on this guest according to the guest policy. diff --git a/src/lib.rs b/src/lib.rs index 2c11d90b..b6b49a59 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,16 @@ //! //! Refer to the [`launch`] module for more information. //! +//! ## Cryptographic Verification +//! +//! To enable the cryptographic verification of certificate chains and +//! attestation reports, either the `openssl` or `crypto_nossl` feature +//! has to be enabled manually. With `openssl`, OpenSSL is used for the +//! verification. With `crypto_nossl`, OpenSSL is _not_ used for the +//! verification and instead pure-Rust libraries (e.g., `p384`, `rsa`, +//! etc.) are used. `openssl` and `crypto_nossl` are mutually exclusive, +//! and enabling both at the same time leads to a compiler error. +//! //! ## Remarks //! //! Note that the Linux kernel provides access to these APIs through a set @@ -46,12 +56,19 @@ #![allow(clippy::identity_op)] #![allow(clippy::unreadable_literal)] +#[cfg(all(feature = "openssl", feature = "crypto_nossl"))] +compile_error!( + "feature \"openssl\" and feature \"crypto_nossl\" cannot be enabled at the same time" +); + /// SEV and SEV-SNP certificates interface. pub mod certs; pub mod firmware; #[cfg(target_os = "linux")] pub mod launch; +#[cfg(all(any(feature = "sev", feature = "snp"), feature = "openssl"))] +pub mod measurement; #[cfg(all(target_os = "linux", feature = "openssl", feature = "sev"))] pub mod session; mod util; diff --git a/src/measurement/gctx.rs b/src/measurement/gctx.rs new file mode 100644 index 00000000..5f19f0aa --- /dev/null +++ b/src/measurement/gctx.rs @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 + +use openssl::sha::sha384; + +use crate::error::*; +use std::convert::TryInto; + +// Launch digest size in bytes +const LD_SIZE: usize = 384 / 8; + +// VMSA page is recorded in the RMP table with GPA (u64)(-1). +// However, the address is page-aligned, and also all the bits above +// 51 are cleared. +const VMSA_GPA: u64 = 0xFFFFFFFFF000; + +// Launch digest intialized in all zeros +const ZEROS: [u8; LD_SIZE] = [0; LD_SIZE]; + +/// Guest context field structure +pub struct Gctx { + /// Launch Digest, 48 bytes long + ld: [u8; LD_SIZE], +} + +/// Default init of GCTX, launch digest of all 0s +impl Default for Gctx { + fn default() -> Gctx { + Gctx { ld: ZEROS } + } +} + +impl Gctx { + /// Initialize a new guest context using existing data + pub fn new(seed: &[u8]) -> Result { + Ok(Self { + ld: seed.try_into()?, + }) + } + + /// Get the launch digest bytes + pub fn get_ld(self) -> [u8; LD_SIZE] { + self.ld + } + + /// Will update guest context launch digest with provided data from page + fn update( + &mut self, + page_type: u8, + gpa: u64, + contents: [u8; LD_SIZE], + ) -> Result<(), GCTXError> { + let page_info_len: u16 = 0x70; + let is_imi: u8 = 0; + let vmpl3_perms: u8 = 0; + let vmpl2_perms: u8 = 0; + let vmpl1_perms: u8 = 0; + + let mut page_info: Vec = self.ld.to_vec(); + page_info.extend_from_slice(&contents); + + page_info.extend_from_slice(&page_info_len.to_le_bytes()); + page_info.extend_from_slice(&page_type.to_le_bytes()); + page_info.extend_from_slice(&is_imi.to_le_bytes()); + + page_info.extend_from_slice(&vmpl3_perms.to_le_bytes()); + page_info.extend_from_slice(&vmpl2_perms.to_le_bytes()); + page_info.extend_from_slice(&vmpl1_perms.to_le_bytes()); + page_info.extend_from_slice(&(0_u8).to_le_bytes()); + + page_info.extend_from_slice(&gpa.to_le_bytes()); + + if page_info.len() != (page_info_len as usize) { + return Err(GCTXError::InvalidPageSize( + page_info.len(), + page_info_len as usize, + )); + } + + self.ld = sha384(&page_info); + + Ok(()) + } + + /// Update launch digest using normal memory pages + pub fn update_normal_pages(&mut self, start_gpa: u64, data: &[u8]) -> Result<(), GCTXError> { + if (data.len() % 4096) != 0 { + return Err(GCTXError::InvalidBlockSize); + } + let mut offset = 0; + while offset < data.len() { + let page_data = &data[offset..offset + 4096]; + self.update(0x01, start_gpa + offset as u64, sha384(page_data))?; + offset += 4096; + } + Ok(()) + } + + /// Update launch digest using VMSA memory pages + pub fn update_vmsa_page(&mut self, data: &[u8]) -> Result<(), GCTXError> { + if data.len() != 4096 { + return Err(GCTXError::InvalidBlockSize); + } + self.update(0x02, VMSA_GPA, sha384(data))?; + Ok(()) + } + + /// Update launch digest using ZERO pages + pub fn update_zero_pages(&mut self, gpa: u64, length_bytes: usize) -> Result<(), GCTXError> { + if (length_bytes % 4096) != 0 { + return Err(GCTXError::InvalidBlockSize); + }; + let mut offset = 0; + while offset < length_bytes { + self.update(0x03, gpa + offset as u64, ZEROS)?; + offset += 4096; + } + Ok(()) + } + + /// Update launch digest using an unmeasured page + fn _update_unmeasured_page(&mut self, gpa: u64) -> Result<(), GCTXError> { + self.update(0x04, gpa, ZEROS)?; + Ok(()) + } + + /// Update launch digest using a secret page + pub fn update_secrets_page(&mut self, gpa: u64) -> Result<(), GCTXError> { + self.update(0x05, gpa, ZEROS)?; + Ok(()) + } + + /// Update launch digest using a CPUID page + pub fn update_cpuid_page(&mut self, gpa: u64) -> Result<(), GCTXError> { + self.update(0x06, gpa, ZEROS)?; + Ok(()) + } +} diff --git a/src/measurement/measurement_functions.rs b/src/measurement/measurement_functions.rs new file mode 100644 index 00000000..b1e39072 --- /dev/null +++ b/src/measurement/measurement_functions.rs @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Operations to calculate guest measurement for different SEV modes +use crate::measurement::{ + gctx::Gctx, + ovmf::{OvmfSevMetadataSectionDesc, SectionType, OVMF}, + sev_hashes::SevHashes, + vcpu_types::CpuType, + vmsa::{SevMode, VMMType, VMSA}, +}; +use hex::FromHex; +use std::path::PathBuf; +use std::str::FromStr; + +use crate::error::*; + +use openssl::sha::Sha256; + +const _PAGE_MASK: u64 = 0xfff; + +/// Get the launch digest as a hex string +pub fn get_hex_ld(ld: Vec) -> String { + hex::encode(ld) +} + +/// Update launch digest with SEV kernel hashes +fn snp_update_kernel_hashes( + gctx: &mut Gctx, + ovmf: &OVMF, + sev_hashes: Option<&SevHashes>, + gpa: u64, + size: usize, +) -> Result<(), MeasurementError> { + match sev_hashes { + Some(hash) => { + let sev_hashes_table_gpa = ovmf.sev_hashes_table_gpa()?; + let page_offset = sev_hashes_table_gpa & _PAGE_MASK; + let sev_hashes_page = hash.construct_page(page_offset as usize)?; + assert_eq!(sev_hashes_page.len(), size); + gctx.update_normal_pages(gpa, sev_hashes_page.as_slice())? + } + None => gctx.update_zero_pages(gpa, size)?, + } + + Ok(()) +} + +/// Update launch digest with different section types +fn snp_update_section( + desc: &OvmfSevMetadataSectionDesc, + gctx: &mut Gctx, + ovmf: &OVMF, + sev_hashes: Option<&SevHashes>, + vmm_type: VMMType, +) -> Result<(), MeasurementError> { + match desc.section_type { + SectionType::SnpSecMemory => gctx.update_zero_pages(desc.gpa.into(), desc.size as usize)?, + SectionType::SnpSecrets => gctx.update_secrets_page(desc.gpa.into())?, + SectionType::CPUID => { + if vmm_type != VMMType::EC2 { + gctx.update_cpuid_page(desc.gpa.into())? + } + } + SectionType::SnpKernelHashes => { + snp_update_kernel_hashes(gctx, ovmf, sev_hashes, desc.gpa.into(), desc.size as usize)? + } + } + + Ok(()) +} + +/// Update GCTX with different metadata pages +fn snp_update_metadata_pages( + gctx: &mut Gctx, + ovmf: &OVMF, + sev_hashes: Option<&SevHashes>, + vmm_type: VMMType, +) -> Result<(), MeasurementError> { + for desc in ovmf.metadata_items().iter() { + snp_update_section(desc, gctx, ovmf, sev_hashes, vmm_type)? + } + + if vmm_type == VMMType::EC2 { + for desc in ovmf.metadata_items() { + if desc.section_type == SectionType::CPUID { + gctx.update_cpuid_page(desc.gpa.into())? + } + } + } + + if sev_hashes.is_some() && !ovmf.has_metadata_section(SectionType::SnpKernelHashes) { + return Err(MeasurementError::MissingSection( + "SNP_KERNEL_HASHES".to_string(), + )); + } + + Ok(()) +} + +/// Calculate the OVMF hash from OVMF file +pub fn calc_snp_ovmf_hash(ovmf_file: PathBuf) -> Result<[u8; 48], MeasurementError> { + let ovmf = OVMF::new(ovmf_file)?; + let mut gctx = Gctx::default(); + + gctx.update_normal_pages(ovmf.gpa(), ovmf.data())?; + + Ok(gctx.get_ld()) +} + +/// Calulate an SEV-SNP launch digest +#[allow(clippy::too_many_arguments)] +pub fn snp_calc_launch_digest( + vcpus: u32, + vcpu_type: String, + ovmf_file: PathBuf, + kernel_file: Option, + initrd_file: Option, + append: Option<&str>, + ovmf_hash_str: Option<&str>, + vmm_type: Option, +) -> Result<[u8; 48], MeasurementError> { + let ovmf = OVMF::new(ovmf_file)?; + + let mut gctx = match ovmf_hash_str { + Some(hash) => { + let ovmf_hash = Vec::from_hex(hash)?; + Gctx::new(ovmf_hash.as_slice()) + } + None => { + let mut gctx = Gctx::default(); + + gctx.update_normal_pages(ovmf.gpa(), ovmf.data())?; + + Ok(gctx) + } + }?; + + let sev_hashes = match kernel_file { + Some(kernel) => Some(SevHashes::new(kernel, initrd_file, append)?), + None => None, + }; + + let official_vmm_type = match vmm_type { + Some(vmm) => vmm, + None => VMMType::QEMU, + }; + + snp_update_metadata_pages(&mut gctx, &ovmf, sev_hashes.as_ref(), official_vmm_type)?; + + let vmsa = VMSA::new( + SevMode::SevSnp, + ovmf.sev_es_reset_eip()?.into(), + CpuType::from_str(vcpu_type.as_str())?, + official_vmm_type, + Some(vcpus as u64), + ); + + for vmsa_page in vmsa.pages(vcpus as usize)?.iter() { + gctx.update_vmsa_page(vmsa_page.as_slice())? + } + + Ok(gctx.get_ld()) +} + +/// Calculate an SEV-ES launch digest +pub fn seves_calc_launch_digest( + vcpus: u32, + vcpu_type: String, + ovmf_file: PathBuf, + kernel_file: Option, + initrd_file: Option, + append: Option<&str>, + vmm_type: Option, +) -> Result<[u8; 32], MeasurementError> { + let ovmf = OVMF::new(ovmf_file)?; + let mut launch_hash = Sha256::new(); + launch_hash.update(ovmf.data().as_slice()); + + if let Some(kernel) = kernel_file { + if !ovmf.is_sev_hashes_table_supported() { + return Err(MeasurementError::KernelSpecifiedError); + } + let sev_hashes = SevHashes::new(kernel, initrd_file, append)?.construct_table()?; + launch_hash.update(sev_hashes.as_slice()); + }; + + let official_vmm_type = match vmm_type { + Some(vmm) => vmm, + None => VMMType::QEMU, + }; + + let vmsa = VMSA::new( + SevMode::SevEs, + ovmf.sev_es_reset_eip()?.into(), + CpuType::from_str(vcpu_type.as_str())?, + official_vmm_type, + Some(vcpus as u64), + ); + + for vmsa_page in vmsa.pages(vcpus as usize)?.iter() { + launch_hash.update(vmsa_page.as_slice()) + } + + Ok(launch_hash.finish()) +} + +/// Calculate an SEV launch digest +pub fn sev_calc_launch_digest( + ovmf_file: PathBuf, + kernel_file: Option, + initrd_file: Option, + append: Option<&str>, +) -> Result<[u8; 32], MeasurementError> { + let ovmf = OVMF::new(ovmf_file)?; + let mut launch_hash = Sha256::new(); + launch_hash.update(ovmf.data().as_slice()); + + if let Some(kernel) = kernel_file { + if !ovmf.is_sev_hashes_table_supported() { + return Err(MeasurementError::KernelSpecifiedError); + } + let sev_hashes = SevHashes::new(kernel, initrd_file, append)?.construct_table()?; + launch_hash.update(sev_hashes.as_slice()); + }; + + Ok(launch_hash.finish()) +} diff --git a/src/measurement/mod.rs b/src/measurement/mod.rs new file mode 100644 index 00000000..14f783ca --- /dev/null +++ b/src/measurement/mod.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Everything one needs to calculate a launch measurement for a SEV encrypted confidential guest. +//! +//! This module contains the structures and functions needed to calculate a launch measurement. +//! This includes, GCTX, SEV-HASHES, VMSA and OVMF pages. + +/// +#[cfg(all(any(feature = "sev", feature = "snp"), feature = "openssl"))] +pub mod gctx; + +#[cfg(any(feature = "sev", feature = "snp"))] +pub mod ovmf; + +#[cfg(any(feature = "sev", feature = "snp"))] +pub mod vmsa; + +#[cfg(all(any(feature = "sev", feature = "snp"), feature = "openssl"))] +pub mod sev_hashes; + +#[cfg(any(feature = "sev", feature = "snp"))] +pub mod vcpu_types; + +#[cfg(all(any(feature = "sev", feature = "snp"), feature = "openssl"))] +pub mod measurement_functions; diff --git a/src/measurement/ovmf.rs b/src/measurement/ovmf.rs new file mode 100644 index 00000000..4ed6cd50 --- /dev/null +++ b/src/measurement/ovmf.rs @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Operations to handle ovmf data +use crate::error::*; +use bincode; +use byteorder::{ByteOrder, LittleEndian}; +use serde::Deserialize; +use std::{ + collections::HashMap, + convert::{TryFrom, TryInto}, + fs::File, + io::Read, + path::PathBuf, +}; +use uuid::{uuid, Uuid}; + +/// Convert a UUID into a little endian slice +pub fn guid_le_to_slice(guid: &str) -> Result<[u8; 16], MeasurementError> { + let guid = Uuid::try_from(guid)?; + let guid = guid.to_bytes_le(); + let guid = guid.as_slice(); + + Ok(guid.try_into()?) +} + +/// Types of sections declared by OVMF SEV Metadata, as appears in: https://github.com/tianocore/edk2/blob/edk2-stable202205/OvmfPkg/ResetVector/X64/OvmfSevMetadata.asm +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +#[serde(into = "u8", try_from = "u8")] +pub enum SectionType { + /// SNP Secure Memory + SnpSecMemory = 1, + /// SNP secret + SnpSecrets = 2, + /// CPUID + CPUID = 3, + /// SNP kernel hashes + SnpKernelHashes = 0x10, +} + +impl From for u8 { + fn from(value: SectionType) -> u8 { + value as u8 + } +} + +impl TryFrom for SectionType { + type Error = OVMFError; + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(SectionType::SnpSecMemory), + 2 => Ok(SectionType::SnpSecrets), + 3 => Ok(SectionType::CPUID), + 0x10 => Ok(SectionType::SnpKernelHashes), + _ => Err(OVMFError::InvalidSectionType), + } + } +} + +/// OVMF SEV Metadata Section Description +#[repr(C)] +#[derive(Debug, Clone, Copy, Deserialize)] +pub struct OvmfSevMetadataSectionDesc { + /// Guest Physical Adress + pub gpa: u32, + /// Size + pub size: u32, + /// Section Type + pub section_type: SectionType, +} + +impl OvmfSevMetadataSectionDesc { + /// Generate Description from bytes + fn from_bytes(value: &[u8], offset: usize) -> Result { + let value = &value[offset..offset + std::mem::size_of::()]; + bincode::deserialize(value).map_err(|e| MeasurementError::BincodeError(*e)) + } +} + +/// OVMF Metadata Header +#[repr(C)] +#[derive(Debug, Clone, Copy, Deserialize)] +struct OvmfSevMetadataHeader { + /// Header Signature + signature: [u8; 4], + /// Size + size: u32, + /// Version + version: u32, + /// Number of items + num_items: u32, +} + +impl OvmfSevMetadataHeader { + /// Generate Header from bytes + fn from_bytes(value: &[u8], offset: usize) -> Result { + let value = &value[offset..offset + std::mem::size_of::()]; + bincode::deserialize(value).map_err(|e| MeasurementError::BincodeError(*e)) + } + + /// Verify Header Signature + fn verify(&self) -> Result<(), OVMFError> { + let expected_signature: &[u8] = b"ASEV"; + if !self.signature.eq(expected_signature) { + return Err(OVMFError::SEVMetadataVerification("signature".to_string())); + } + + if self.version != 1 { + return Err(OVMFError::SEVMetadataVerification("version".to_string())); + } + + Ok(()) + } +} + +/// OVMF Footer +#[repr(C, packed)] +#[derive(Debug)] +struct OvmfFooterTableEntry { + /// Size + size: u16, + /// GUID + guid: [u8; 16], +} + +impl TryFrom<&[u8]> for OvmfFooterTableEntry { + type Error = MeasurementError; + + /// Grenerate footer from data + fn try_from(value: &[u8]) -> Result { + // Bytes 2-17 are the GUID + let guid: [u8; 16] = value[2..18].try_into()?; + // first 2 bytes are the size + let size_nums: [u8; 2] = value[0..2].try_into()?; + let size = u16::from_le_bytes(size_nums); + Ok(OvmfFooterTableEntry { size, guid }) + } +} + +const FOUR_GB: u64 = 0x100000000; +const OVMF_TABLE_FOOTER_GUID: Uuid = uuid!("96b582de-1fb2-45f7-baea-a366c55a082d"); +const SEV_HASH_TABLE_RV_GUID: Uuid = uuid!("7255371f-3a3b-4b04-927b-1da6efa8d454"); +const SEV_ES_RESET_BLOCK_GUID: Uuid = uuid!("00f771de-1a7e-4fcb-890e-68c77e2fb44e"); +const OVMF_SEV_META_DATA_GUID: Uuid = uuid!("dc886566-984a-4798-a75e-5585a7bf67cc"); + +/// OVMF Structure +pub struct OVMF { + /// OVMF data + data: Vec, + /// Table matching GUID to it's data + table: HashMap>, + /// Metadata item description + metadata_items: Vec, +} + +impl OVMF { + /// Generate new OVMF structure by parsing the foorter table and SEV metadata + pub fn new(ovmf_file: PathBuf) -> Result { + let mut data = Vec::new(); + let mut file = match File::open(ovmf_file) { + Ok(file) => file, + Err(e) => return Err(MeasurementError::FileError(e)), + }; + + file.read_to_end(&mut data)?; + + let mut ovmf = OVMF { + data, + table: HashMap::new(), + metadata_items: Vec::new(), + }; + + ovmf.parse_footer_table()?; + ovmf.parse_sev_metadata()?; + + Ok(ovmf) + } + + /// Grab OVMF data + pub fn data(&self) -> &Vec { + &self.data + } + + /// Calculate OVMF GPA + pub fn gpa(&self) -> u64 { + FOUR_GB - self.data.len() as u64 + } + + /// Get an item from the OVMF table + fn table_item(&self, guid: &Uuid) -> Option<&Vec> { + self.table.get(guid) + } + + /// Get the OVMF metadata items + pub fn metadata_items(&self) -> &Vec { + &self.metadata_items + } + + /// Check if the metadata items have the desired section + pub fn has_metadata_section(&self, section_type: SectionType) -> bool { + self.metadata_items() + .iter() + .any(|s| s.section_type == section_type) + } + + /// Check that the table supports SEV hashes + pub fn is_sev_hashes_table_supported(&self) -> bool { + self.table.contains_key(&SEV_HASH_TABLE_RV_GUID) + && self.sev_hashes_table_gpa().unwrap_or(0) != 0 + } + + /// Get the SEV HASHES GPA + pub fn sev_hashes_table_gpa(&self) -> Result { + if !(self.table.contains_key(&SEV_HASH_TABLE_RV_GUID)) { + return Err(OVMFError::EntryMissingInTable( + "SEV_HASH_TABLE_RV_GUID".to_string(), + )); + } + Ok(self + .table_item(&SEV_HASH_TABLE_RV_GUID) + .and_then(|entry| entry.get(..4)) + .map(|bytes| LittleEndian::read_u32(bytes) as u64) + .unwrap()) + } + + /// Get the SEV-ES EIP + pub fn sev_es_reset_eip(&self) -> Result { + if !(self.table.contains_key(&SEV_ES_RESET_BLOCK_GUID)) { + return Err(OVMFError::EntryMissingInTable( + "SEV_ES_RESET_BLOCK_GUID".to_string(), + )); + } + + Ok(self + .table_item(&SEV_ES_RESET_BLOCK_GUID) + .and_then(|entry| entry.get(..4)) + .map(LittleEndian::read_u32) + .unwrap()) + } + + /// Parse footer table data + fn parse_footer_table(&mut self) -> Result<(), MeasurementError> { + self.table.clear(); + let size = self.data.len(); + const ENTRY_HEADER_SIZE: usize = std::mem::size_of::(); + //The OVMF table ends 32 bytes before the end of the firmware binary + let start_of_footer_table = size - 32 - ENTRY_HEADER_SIZE; + let footer = + OvmfFooterTableEntry::try_from(&self.data.as_slice()[start_of_footer_table..])?; + + let expected_footer_guid = guid_le_to_slice(OVMF_TABLE_FOOTER_GUID.to_string().as_str())?; + + if !footer.guid.eq(&expected_footer_guid) { + return Err(MeasurementError::OVMFError(OVMFError::MismatchingGUID())); + } + + if (footer.size as usize) < ENTRY_HEADER_SIZE { + return Err(MeasurementError::OVMFError(OVMFError::InvalidSize( + "OVMF Table Footer".to_string(), + footer.size as usize, + ENTRY_HEADER_SIZE, + ))); + } + + let table_size = footer.size as usize - ENTRY_HEADER_SIZE; + + let table_start = start_of_footer_table - table_size; + let table_bytes = &self.data[table_start..start_of_footer_table]; + let mut offset = table_size; + while offset >= ENTRY_HEADER_SIZE { + let entry = + OvmfFooterTableEntry::try_from(&table_bytes[offset - ENTRY_HEADER_SIZE..offset])?; + if entry.size < ENTRY_HEADER_SIZE as u16 { + return Err(MeasurementError::OVMFError(OVMFError::InvalidSize( + "OVMF Table Entry".to_string(), + entry.size as usize, + ENTRY_HEADER_SIZE, + ))); + } + let entry_guid = Uuid::from_slice_le(&entry.guid)?; + + if offset < entry.size as usize { + break; + } + let entry_data = &table_bytes[offset - entry.size as usize..offset - ENTRY_HEADER_SIZE]; + self.table.insert(entry_guid, entry_data.to_vec()); + + offset -= entry.size as usize; + } + + Ok(()) + } + + /// parse SEV metadata + fn parse_sev_metadata(&mut self) -> Result<(), MeasurementError> { + match self.table.get(&OVMF_SEV_META_DATA_GUID) { + Some(entry) => { + let offset_from_end = i32::from_le_bytes(entry[..4].try_into()?); + let header_start = self.data.len() - (offset_from_end as usize); + let header = OvmfSevMetadataHeader::from_bytes(self.data.as_slice(), header_start)?; + header.verify()?; + let items = &self.data[header_start + std::mem::size_of::() + ..header_start + header.size as usize]; + for i in 0..header.num_items { + let offset = (i as usize) * std::mem::size_of::(); + let item = OvmfSevMetadataSectionDesc::from_bytes(items, offset)?; + self.metadata_items.push(item.to_owned()); + } + } + + None => { + return Err(MeasurementError::OVMFError(OVMFError::EntryMissingInTable( + "OVMF_SEV_METADATA_GUID".to_string(), + ))); + } + } + + Ok(()) + } +} diff --git a/src/measurement/sev_hashes.rs b/src/measurement/sev_hashes.rs new file mode 100644 index 00000000..26219365 --- /dev/null +++ b/src/measurement/sev_hashes.rs @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Operations to handle OVMF SEV-HASHES +use openssl::sha::sha256; +use serde::Serialize; +use std::fs::File; +use std::{ + convert::{TryFrom, TryInto}, + io::Read, + mem::size_of, + path::PathBuf, + str::FromStr, +}; + +use uuid::{uuid, Uuid}; + +use crate::error::*; + +type Sha256Hash = [u8; 32]; + +/// GUID stored as little endian +#[derive(Debug, Clone, Copy, Serialize, Default)] +struct GuidLe { + _data: [u8; 16], +} + +impl GuidLe { + fn from_uuid(guid: &Uuid) -> Result { + let guid = guid.to_bytes_le(); + let guid = guid.as_slice(); + Ok(Self { + _data: guid.try_into()?, + }) + } +} + +impl FromStr for GuidLe { + type Err = MeasurementError; + + fn from_str(guid: &str) -> Result { + let guid = Uuid::try_from(guid)?; + let guid = guid.to_bytes_le(); + let guid = guid.as_slice(); + Ok(Self { + _data: guid.try_into()?, + }) + } +} + +/// SEV hash table entry +#[repr(C)] +#[derive(Debug, Clone, Copy, Serialize, Default)] +struct SevHashTableEntry { + /// Entry GUID + guid: GuidLe, + /// Length + length: u16, + /// Hash + hash: Sha256Hash, +} + +impl SevHashTableEntry { + fn new(guid: &Uuid, hash: Sha256Hash) -> Result { + Ok(Self { + guid: GuidLe::from_uuid(guid)?, + length: std::mem::size_of::() as u16, + hash, + }) + } +} + +/// Table of SEV hashes +#[repr(C)] +#[derive(Debug, Clone, Copy, Serialize, Default)] +struct SevHashTable { + /// GUID + guid: GuidLe, + /// Length + length: u16, + /// Cmd line append table entry + cmdline: SevHashTableEntry, + /// initrd table entry + initrd: SevHashTableEntry, + /// Kernel table entry + kernel: SevHashTableEntry, +} + +impl SevHashTable { + fn new( + guid: &str, + cmdline: SevHashTableEntry, + initrd: SevHashTableEntry, + kernel: SevHashTableEntry, + ) -> Result { + Ok(Self { + guid: GuidLe::from_str(guid)?, + length: std::mem::size_of::() as u16, + cmdline, + initrd, + kernel, + }) + } +} + +/// Padded SEV hash table +#[repr(C)] +#[derive(Debug, Clone, Copy, Serialize, Default)] +struct PaddedSevHashTable { + ht: SevHashTable, + padding: [u8; PaddedSevHashTable::PADDING_SIZE], +} + +impl PaddedSevHashTable { + const PADDING_SIZE: usize = + ((size_of::() + 15) & !15) - size_of::(); + + fn new(hash_table: SevHashTable) -> Self { + PaddedSevHashTable { + ht: hash_table, + ..Default::default() + } + } +} + +const SEV_HASH_TABLE_HEADER_GUID: Uuid = uuid!("9438d606-4f22-4cc9-b479-a793d411fd21"); +const SEV_KERNEL_ENTRY_GUID: Uuid = uuid!("4de79437-abd2-427f-b835-d5b172d2045b"); +const SEV_INITRD_ENTRY_GUID: Uuid = uuid!("44baf731-3a2f-4bd7-9af1-41e29169781d"); +const SEV_CMDLINE_ENTRY_GUID: Uuid = uuid!("97d02dd8-bd20-4c94-aa78-e7714d36ab2a"); + +/// Struct containing the 3 possible SEV hashes +pub struct SevHashes { + /// Kernel hash + kernel_hash: Sha256Hash, + /// Initrd hash + initrd_hash: Sha256Hash, + /// Cmdline append hash + cmdline_hash: Sha256Hash, +} + +impl SevHashes { + /// Generate hashes from the user provided kernel, initrd, and cmdline. + pub fn new( + kernel: PathBuf, + initrd: Option, + append: Option<&str>, + ) -> Result { + let mut kernel_file = File::open(kernel)?; + let mut kernel_data = Vec::new(); + kernel_file.read_to_end(&mut kernel_data)?; + + let kernel_hash = sha256(&kernel_data); + + let initrd_data = match initrd { + Some(path) => { + let mut initrd_file = File::open(path)?; + let mut data = Vec::new(); + initrd_file.read_to_end(&mut data)?; + data + } + None => Vec::new(), + }; + + let initrd_hash = sha256(&initrd_data); + + let cmdline_hash = match append { + Some(append_str) => { + let mut append_bytes = append_str.trim().as_bytes().to_vec(); + append_bytes.extend_from_slice(b"\x00"); + sha256(&append_bytes) + } + + None => sha256(b"\x00"), + }; + + Ok(SevHashes { + kernel_hash, + initrd_hash, + cmdline_hash, + }) + } + + ///Generate the SEV hashes area - this must be *identical* to the way QEMU + ///generates this info in order for the measurement to match. + pub fn construct_table(&self) -> Result, MeasurementError> { + let sev_hash_table = SevHashTable::new( + SEV_HASH_TABLE_HEADER_GUID.to_string().as_str(), + SevHashTableEntry::new(&SEV_CMDLINE_ENTRY_GUID, self.cmdline_hash)?, + SevHashTableEntry::new(&SEV_INITRD_ENTRY_GUID, self.initrd_hash)?, + SevHashTableEntry::new(&SEV_KERNEL_ENTRY_GUID, self.kernel_hash)?, + )?; + + let padded_hash_table = PaddedSevHashTable::new(sev_hash_table); + + bincode::serialize(&padded_hash_table).map_err(|e| MeasurementError::BincodeError(*e)) + } + + /// Construct an SEV Hash page using hash table. + pub fn construct_page(&self, offset: usize) -> Result, MeasurementError> { + if offset >= 4096 { + return Err(MeasurementError::SevHashError(SevHashError::InvalidOffset( + offset, 4096, + ))); + } + + let hashes_table = self.construct_table()?; + let mut page = Vec::with_capacity(4096); + page.resize(offset, 0); + page.extend_from_slice(&hashes_table[..]); + page.resize(4096, 0); + if page.len() != 4096 { + return Err(MeasurementError::SevHashError(SevHashError::InvalidSize( + page.len(), + 4096, + ))); + } + Ok(page) + } +} diff --git a/src/measurement/vcpu_types.rs b/src/measurement/vcpu_types.rs new file mode 100644 index 00000000..bc0d08bb --- /dev/null +++ b/src/measurement/vcpu_types.rs @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Enum of different exisitng AMD EPYC VCPUs +use std::{convert::TryFrom, fmt, str::FromStr}; + +use crate::error::MeasurementError; + +/// All currently available QEMU VCPU types +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CpuType { + /// EPYC + Epyc = 0, + /// EPYC V1 + EpycV1 = 1, + /// EPYC V2 + EpycV2 = 2, + /// EPYC Indirect Branch Predictor Barrier + EpycIBPB = 3, + /// EPYC V3 + EpycV3 = 4, + /// EPYC V4 + EpycV4 = 5, + /// EPYC ROME + EpycRome = 6, + /// EPYC ROME V1 + EpycRomeV1 = 7, + ///EPYC ROME V2 + EpycRomeV2 = 8, + /// EPYC ROME V3 + EpycRomeV3 = 9, + /// EPYC MILAN + EpycMilan = 10, + /// EPYC MILAN V1 + EpycMilanV1 = 11, + /// EPYC MILAN V2 + EpycMilanV2 = 12, + /// EPYC GENOA + EpycGenoa = 13, + /// EPYC GENOA V1 + EpycGenoaV1 = 14, +} + +impl TryFrom for CpuType { + type Error = MeasurementError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(CpuType::Epyc), + 1 => Ok(CpuType::EpycV1), + 3 => Ok(CpuType::EpycIBPB), + 4 => Ok(CpuType::EpycV3), + 5 => Ok(CpuType::EpycV4), + 6 => Ok(CpuType::EpycRome), + 7 => Ok(CpuType::EpycRomeV1), + 8 => Ok(CpuType::EpycRomeV2), + 9 => Ok(CpuType::EpycRomeV3), + 10 => Ok(CpuType::EpycMilan), + 11 => Ok(CpuType::EpycMilanV1), + 12 => Ok(CpuType::EpycMilanV2), + 13 => Ok(CpuType::EpycGenoa), + 14 => Ok(CpuType::EpycGenoaV1), + _ => Err(MeasurementError::InvalidVcpuTypeError(value.to_string())), + } + } +} + +impl CpuType { + /// Matching CPU-Type with its CPU signature + pub fn sig(&self) -> i32 { + match self { + CpuType::Epyc => cpu_sig(23, 1, 2), + CpuType::EpycV1 => cpu_sig(23, 1, 2), + CpuType::EpycV2 => cpu_sig(23, 1, 2), + CpuType::EpycIBPB => cpu_sig(23, 1, 2), + CpuType::EpycV3 => cpu_sig(23, 1, 2), + CpuType::EpycV4 => cpu_sig(23, 1, 2), + CpuType::EpycRome => cpu_sig(23, 49, 0), + CpuType::EpycRomeV1 => cpu_sig(23, 49, 0), + CpuType::EpycRomeV2 => cpu_sig(23, 49, 0), + CpuType::EpycRomeV3 => cpu_sig(23, 49, 0), + CpuType::EpycMilan => cpu_sig(25, 1, 1), + CpuType::EpycMilanV1 => cpu_sig(25, 1, 1), + CpuType::EpycMilanV2 => cpu_sig(25, 1, 1), + CpuType::EpycGenoa => cpu_sig(25, 17, 0), + CpuType::EpycGenoaV1 => cpu_sig(25, 17, 0), + } + } +} + +impl fmt::Display for CpuType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + CpuType::Epyc => write!(f, "EPYC"), + CpuType::EpycV1 => write!(f, "EPYC-v1"), + CpuType::EpycV2 => write!(f, "EPYC-v2"), + CpuType::EpycIBPB => write!(f, "EPYC-IBPB"), + CpuType::EpycV3 => write!(f, "EPYC-v3"), + CpuType::EpycV4 => write!(f, "EPYC-v4"), + CpuType::EpycRome => write!(f, "EPYC-Rome"), + CpuType::EpycRomeV1 => write!(f, "EPYC-Rome-v1"), + CpuType::EpycRomeV2 => write!(f, "EPYC-Rome-v2"), + CpuType::EpycRomeV3 => write!(f, "EPYC-Rome-v3"), + CpuType::EpycMilan => write!(f, "EPYC-Milan"), + CpuType::EpycMilanV1 => write!(f, "EPYC-Milan-v1"), + CpuType::EpycMilanV2 => write!(f, "EPYC-Milan-v2"), + CpuType::EpycGenoa => write!(f, "EPYC-Genoa"), + CpuType::EpycGenoaV1 => write!(f, "EPYC-Genoa-v1"), + } + } +} + +impl FromStr for CpuType { + type Err = MeasurementError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "epyc" => Ok(CpuType::Epyc), + "epyc-v1" => Ok(CpuType::EpycV1), + "epyc-v2" => Ok(CpuType::EpycV2), + "epyc-ibpb" => Ok(CpuType::EpycIBPB), + "epyc-v3" => Ok(CpuType::EpycV3), + "epyc-v4" => Ok(CpuType::EpycV4), + "epyc-rome" => Ok(CpuType::EpycRome), + "epyc-rome-v1" => Ok(CpuType::EpycRomeV1), + "epyc-rome-v2" => Ok(CpuType::EpycRomeV2), + "epyc-rome-v3" => Ok(CpuType::EpycRomeV3), + "epyc-milan" => Ok(CpuType::EpycMilan), + "epyc-milan-v1" => Ok(CpuType::EpycMilanV1), + "epyc-milan-v2" => Ok(CpuType::EpycMilanV2), + "epyc-genoa" => Ok(CpuType::EpycGenoa), + "epyc-genoa-v1" => Ok(CpuType::EpycGenoaV1), + _ => Err(MeasurementError::InvalidVcpuTypeError(s.to_string())), + } + } +} + +/// Compute the 32-bit CPUID signature from family, model, and stepping. +/// +/// This computation is described in AMD's CPUID Specification, publication #25481 +/// https://www.amd.com/system/files/TechDocs/25481.pdf +/// See section: CPUID Fn0000_0001_EAX Family, Model, Stepping Identifiers +pub fn cpu_sig(family: i32, model: i32, stepping: i32) -> i32 { + let family_low; + let family_high; + + if family > 0xf { + family_low = 0xf; + family_high = (family - 0x0f) & 0xff; + } else { + family_low = family; + family_high = 0; + } + + let model_low = model & 0xf; + let model_high = (model >> 4) & 0xf; + + let stepping_low = stepping & 0xf; + + (family_high << 20) | (model_high << 16) | (family_low << 8) | (model_low << 4) | stepping_low +} diff --git a/src/measurement/vmsa.rs b/src/measurement/vmsa.rs new file mode 100644 index 00000000..be140d58 --- /dev/null +++ b/src/measurement/vmsa.rs @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! Operations to build and interact with an SEV-ES VMSA +use crate::error::MeasurementError; +// use crate::measurement::{measurement_functions::SevMode, vcpu_types::CpuType}; +use crate::measurement::vcpu_types::CpuType; +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; +use std::{convert::TryFrom, fmt, str::FromStr}; + +/// Different Possible SEV modes +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SevMode { + /// SEV + Sev, + /// SEV-ES + SevEs, + /// SEV-SNP + SevSnp, +} + +impl FromStr for SevMode { + type Err = MeasurementError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "sev" => Ok(SevMode::Sev), + "sev-es" | "seves" => Ok(SevMode::SevEs), + "sev-snp" | "sevsnp" => Ok(SevMode::SevSnp), + _ => Err(MeasurementError::InvalidSevModeError(s.to_string())), + } + } +} + +/// VmmTypes +#[derive(Clone, Copy, PartialEq)] +pub enum VMMType { + /// QEMU + QEMU = 1, + /// EC2 + EC2 = 2, + /// KRUN + KRUN = 3, +} + +impl TryFrom for VMMType { + type Error = MeasurementError; + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(VMMType::QEMU), + 2 => Ok(VMMType::EC2), + 3 => Ok(VMMType::KRUN), + _ => Err(MeasurementError::InvalidVmmError(value.to_string())), + } + } +} + +impl fmt::Debug for VMMType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VMMType::QEMU => write!(f, "qemu"), + VMMType::EC2 => write!(f, "ec2"), + VMMType::KRUN => write!(f, "krun"), + } + } +} + +/// Virtual Machine Control Block +/// The layout of a VMCB struct is documented in Table B-1 of the +/// AMD64 Architecture Programmer’s Manual, Volume 2: System Programming +#[repr(C)] +#[derive(Debug, Clone, Copy, Serialize, Default)] +struct VmcbSeg { + /// Segment selector: documented in Figure 4-3 of the + /// AMD64 Architecture Programmer’s Manual, Volume 2: System Programming + selector: u16, + + /// Segment attributes. + attrib: u16, + + /// Segment limit: used in comparisons with pointer offsets to prevent + /// segment limit violations. + limit: u32, + + /// Segment base address. + base: u64, +} + +impl VmcbSeg { + fn new(selector: u16, attrib: u16, limit: u32, base: u64) -> Self { + Self { + selector, + attrib, + limit, + base, + } + } +} + +/// Large array structure to serialize and default arrays larger than 32 bytes. +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[repr(C)] +pub(crate) struct LargeArray(#[serde(with = "BigArray")] [T; N]) +where + T: for<'a> Deserialize<'a> + Serialize; + +impl Default for LargeArray +where + T: std::marker::Copy + std::default::Default + for<'a> Deserialize<'a> + Serialize, +{ + fn default() -> Self { + Self([T::default(); N]) + } +} + +/// SEV-ES VMSA page +/// The names of the fields are taken from struct sev_es_work_area in the linux kernel: +/// https://github.com/AMDESE/linux/blob/sev-snp-v12/arch/x86/include/asm/svm.h#L318 +/// (following the definitions in AMD APM Vol 2 Table B-4) +#[repr(C)] +#[derive(Debug, Clone, Copy, Serialize, Default)] +struct SevEsSaveArea { + es: VmcbSeg, + cs: VmcbSeg, + ss: VmcbSeg, + ds: VmcbSeg, + fs: VmcbSeg, + gs: VmcbSeg, + gdtr: VmcbSeg, + ldtr: VmcbSeg, + idtr: VmcbSeg, + tr: VmcbSeg, + vmpl0_ssp: u64, + vmpl1_ssp: u64, + vmpl2_ssp: u64, + vmpl3_ssp: u64, + u_cet: u64, + reserved_1: [u8; 2], + vmpl: u8, + cpl: u8, + reserved_2: [u8; 4], + efer: u64, + reserved_3: LargeArray, + xss: u64, + cr4: u64, + cr3: u64, + cr0: u64, + dr7: u64, + dr6: u64, + rflags: u64, + rip: u64, + dr0: u64, + dr1: u64, + dr2: u64, + dr3: u64, + dr0_addr_mask: u64, + dr1_addr_mask: u64, + dr2_addr_mask: u64, + dr3_addr_mask: u64, + reserved_4: [u8; 24], + rsp: u64, + s_cet: u64, + ssp: u64, + isst_addr: u64, + rax: u64, + star: u64, + lstar: u64, + cstar: u64, + sfmask: u64, + kernel_gs_base: u64, + sysenter_cs: u64, + sysenter_esp: u64, + sysenter_eip: u64, + cr2: u64, + reserved_5: [u8; 32], + g_pat: u64, + dbgctrl: u64, + br_from: u64, + br_to: u64, + last_excp_from: u64, + last_excp_to: u64, + reserved_7: LargeArray, + pkru: u32, + reserved_8: [u8; 20], + reserved_9: u64, + rcx: u64, + rdx: u64, + rbx: u64, + reserved_10: u64, + rbp: u64, + rsi: u64, + rdi: u64, + r8: u64, + r9: u64, + r10: u64, + r11: u64, + r12: u64, + r13: u64, + r14: u64, + r15: u64, + reserved_11: [u8; 16], + guest_exit_info_1: u64, + guest_exit_info_2: u64, + guest_exit_int_info: u64, + guest_nrip: u64, + sev_features: u64, + vintr_ctrl: u64, + guest_exit_code: u64, + virtual_tom: u64, + tlb_id: u64, + pcpu_id: u64, + event_inj: u64, + xcr0: u64, + reserved_12: [u8; 16], + x87_dp: u64, + mxcsr: u32, + x87_ftw: u16, + x87_fsw: u16, + x87_fcw: u16, + x87_fop: u16, + x87_ds: u16, + x87_cs: u16, + x87_rip: u64, + fpreg_x87: LargeArray, + fpreg_xmm: LargeArray, + fpreg_ymm: LargeArray, + unused: LargeArray, +} + +const BSP_EIP: u64 = 0xffff_fff0; + +/// VMSA Structure +pub struct VMSA { + /// Bootstrap Processor + bsp_save_area: SevEsSaveArea, + /// Auxiliary Processor + ap_save_area: Option, +} + +impl VMSA { + /// Generate a new SEV-ES VMSA + /// One Bootstrap and an auxiliary save area if needed + pub fn new( + sev_mode: SevMode, + ap_eip: u64, + vcpu_type: CpuType, + vmm_type: VMMType, + cpu_num: Option, + ) -> Self { + let sev_features: u64 = match sev_mode { + SevMode::SevSnp => 0x1, + SevMode::Sev | SevMode::SevEs => 0x0, + }; + + let bsp_save_area = + Self::build_save_area(BSP_EIP, sev_features, vcpu_type, vmm_type, cpu_num); + + let ap_save_area = if ap_eip > 0 { + Some(Self::build_save_area( + ap_eip, + sev_features, + vcpu_type, + vmm_type, + cpu_num, + )) + } else { + None + }; + + VMSA { + bsp_save_area, + ap_save_area, + } + } + + /// Generate a save area + fn build_save_area( + eip: u64, + sev_features: u64, + vcpu_type: CpuType, + vmm_type: VMMType, + cpu_num: Option, + ) -> SevEsSaveArea { + let mut area = SevEsSaveArea::default(); + + let (cs_flags, ss_flags, tr_flags, rdx) = match vmm_type { + VMMType::QEMU => (0x9b, 0x93, 0x8b, vcpu_type.sig() as u64), + VMMType::EC2 => { + if eip == 0xfffffff0 { + (0x9a, 0x92, 0x83, 0) + } else { + (0x9b, 0x92, 0x83, 0) + } + } + VMMType::KRUN => { + match cpu_num { + Some(num) => { + if num > 0 { + area.rip = 0; + area.rsp = 0; + area.rbp = 0; + area.rsi = 0; + + area.cs.selector = 0x9100; + area.cs.base = 0x91000; + } else { + area.rsi = 0x7000; + area.rbp = 0x8ff0; + area.rsp = 0x8ff0; + } + } + None => { + area.rsi = 0x7000; + area.rbp = 0x8ff0; + area.rsp = 0x8ff0; + } + }; + (0x9a, 0x92, 0x83, 0) + } + }; + + area.es = VmcbSeg::new(0, 0x93, 0xffff, 0); + area.cs = VmcbSeg::new(0xf000, cs_flags, 0xffff, eip & 0xffff0000); + area.ss = VmcbSeg::new(0, ss_flags, 0xffff, 0); + area.ds = VmcbSeg::new(0, 0x93, 0xffff, 0); + area.fs = VmcbSeg::new(0, 0x93, 0xffff, 0); + area.gs = VmcbSeg::new(0, 0x93, 0xffff, 0); + area.gdtr = VmcbSeg::new(0, 0, 0xffff, 0); + area.idtr = VmcbSeg::new(0, 0, 0xffff, 0); + area.ldtr = VmcbSeg::new(0, 0x82, 0xffff, 0); + area.tr = VmcbSeg::new(0, tr_flags, 0xffff, 0); + area.efer = 0x1000; + area.cr4 = 0x40; + area.cr0 = 0x10; + area.dr7 = 0x400; + area.dr6 = 0xffff0ff0; + area.rflags = 0x2; + area.rip = eip & 0xffff; + area.g_pat = 0x7040600070406; + area.rdx = rdx; + area.sev_features = sev_features; + area.xcr0 = 0x1; + + area + } + + /// Return a vector containing the save area pages + pub fn pages(&self, vcpus: usize) -> Result>, MeasurementError> { + let bsp_page = bincode::serialize(&self.bsp_save_area) + .map_err(|e| MeasurementError::BincodeError(*e))?; + let ap_save_area_bytes: Option> = + match self.ap_save_area.map(|v| bincode::serialize(&v)) { + Some(value) => Some(value.map_err(|e| MeasurementError::BincodeError(*e))?), + None => None, + }; + + let mut pages = Vec::new(); + + for i in 0..vcpus { + if i == 0 { + pages.push(bsp_page.to_vec()) + } else if let Some(v) = ap_save_area_bytes.as_ref() { + pages.push(v.clone()); + } + } + Ok(pages) + } +} diff --git a/tests/certs.rs b/tests/certs.rs index a5e60cf0..f193a334 100644 --- a/tests/certs.rs +++ b/tests/certs.rs @@ -22,18 +22,14 @@ mod sev { } } -#[cfg(feature = "snp")] +#[cfg(all(feature = "snp", any(feature = "openssl", feature = "crypto_nossl")))] mod snp { - #[cfg(feature = "openssl")] use sev::certs::snp::{builtin::milan, ca, Certificate, Chain, Verifiable}; - #[cfg(feature = "openssl")] const TEST_MILAN_VCEK_DER: &[u8] = include_bytes!("certs_data/vcek_milan.der"); - #[cfg(feature = "openssl")] const TEST_MILAN_ATTESTATION_REPORT: &[u8] = include_bytes!("certs_data/report_milan.hex"); - #[cfg(feature = "openssl")] #[test] fn milan_chain() { let ark = milan::ark().unwrap(); @@ -42,12 +38,31 @@ mod snp { let ca = ca::Chain { ark, ask }; + let chain = Chain { + ca, + vcek: vcek.clone(), + }; + + assert_eq!(chain.verify().ok(), Some(&vcek)); + } + + #[test] + fn milan_chain_invalid() { + let ark = milan::ark().unwrap(); + let ask = milan::ask().unwrap(); + let vcek = { + let mut buf = TEST_MILAN_VCEK_DER.to_vec(); + buf[40] ^= 0xff; + Certificate::from_der(&buf).unwrap() + }; + + let ca = ca::Chain { ark, ask }; + let chain = Chain { ca, vcek }; - chain.verify().unwrap(); + assert_eq!(chain.verify().ok(), None); } - #[cfg(feature = "openssl")] #[test] fn milan_report() { use sev::firmware::guest::AttestationReport; @@ -64,6 +79,26 @@ mod snp { let report: AttestationReport = unsafe { std::ptr::read(report_bytes.as_ptr() as *const _) }; - (&chain, &report).verify().unwrap(); + assert_eq!((&chain, &report).verify().ok(), Some(())); + } + + #[test] + fn milan_report_invalid() { + use sev::firmware::guest::AttestationReport; + + let ark = milan::ark().unwrap(); + let ask = milan::ask().unwrap(); + let vcek = Certificate::from_der(TEST_MILAN_VCEK_DER).unwrap(); + + let ca = ca::Chain { ark, ask }; + + let chain = Chain { ca, vcek }; + + let mut report_bytes = hex::decode(TEST_MILAN_ATTESTATION_REPORT).unwrap(); + report_bytes[0] ^= 0x80; + let report: AttestationReport = + unsafe { std::ptr::read(report_bytes.as_ptr() as *const _) }; + + assert_eq!((&chain, &report).verify().ok(), None); } } diff --git a/tests/measurement.rs b/tests/measurement.rs new file mode 100644 index 00000000..4f860bb2 --- /dev/null +++ b/tests/measurement.rs @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(feature = "openssl")] + +#[cfg(feature = "snp")] +mod snp_tests { + use sev::measurement::{measurement_functions::*, vmsa::VMMType}; + // Test of we can generate a good OVMF hash + #[test] + fn test_snp_ovmf_hash_gen() { + let ovmf_hash = "cab7e085874b3acfdbe2d96dcaa3125111f00c35c6fc9708464c2ae74bfdb048a198cb9a9ccae0b3e5e1a33f5f249819"; + + let ld = snp_calc_launch_digest( + 1, + "EPYC-v4".into(), + "./tests/measurement/ovmf_AmdSev_suffix.bin".into(), + Some("/dev/null".into()), + Some("/dev/null".into()), + None, + Some(ovmf_hash), + Some(VMMType::QEMU), + ) + .unwrap(); + + let ld_hex = hex::encode(ld); + + let exp_result = "db06fb267824b1ccb56edbe2a9c2ce88841bca5090dc6dac91d9cd30f3c2c0bf42daccb30d55d6625bfbf0dae5c50c6d"; + + assert_eq!(ld_hex.as_str(), exp_result); + } + + // Test of we can a full LD from the OVMF hash + #[test] + fn test_snp_ovmf_hash_full() { + let ovmf_hash = hex::encode( + calc_snp_ovmf_hash("./tests/measurement/ovmf_AmdSev_suffix.bin".into()).unwrap(), + ); + + let exp_hash = "edcf6d1c57ce868a167c990f58c8667c698269ef9e0803246419eea914186343054d557e1f17acd93b032c106bc70d25"; + + assert_eq!(ovmf_hash.as_str(), exp_hash); + + let ld = snp_calc_launch_digest( + 1, + "EPYC-v4".into(), + "./tests/measurement/ovmf_AmdSev_suffix.bin".into(), + Some("/dev/null".into()), + Some("/dev/null".into()), + Some("console=ttyS0 loglevel=7"), + Some(ovmf_hash.as_str()), + None, + ) + .unwrap(); + + let ld_hex = hex::encode(ld); + + let exp_result = "f07864303ad8243132029e8110b92805c78d1135a15da75f67abb9a711d78740347f24ee76f603e650ec4adf3611cc1e"; + + assert_eq!(ld_hex.as_str(), exp_result); + } + + // Test EC2 vmm type + #[test] + fn test_snp_ec2() { + let ld = snp_calc_launch_digest( + 1, + "EPYC-v4".into(), + "./tests/measurement/ovmf_AmdSev_suffix.bin".into(), + Some("/dev/null".into()), + Some("/dev/null".into()), + None, + None, + Some(VMMType::EC2), + ) + .unwrap(); + + let ld_hex = hex::encode(ld); + + let exp_result = "760b6e51039d2d6c1fc6d38ca5c387967d158e0294883e4522c36f89bd61bfc9cdb975cd1ceedffbe1b23b1daf4e3f42"; + + assert_eq!(ld_hex.as_str(), exp_result); + } + + // Test a regular snp type + #[test] + fn test_snp() { + let ld = snp_calc_launch_digest( + 1, + "EPYC-v4".into(), + "./tests/measurement/ovmf_AmdSev_suffix.bin".into(), + Some("/dev/null".into()), + Some("/dev/null".into()), + Some("console=ttyS0 loglevel=7"), + None, + None, + ) + .unwrap(); + + let ld_hex = hex::encode(ld); + + let exp_result = "f07864303ad8243132029e8110b92805c78d1135a15da75f67abb9a711d78740347f24ee76f603e650ec4adf3611cc1e"; + + assert_eq!(ld_hex.as_str(), exp_result); + } + + // Test a regular snp without specified kernel + #[test] + fn test_snp_without_kernel() { + let ld = snp_calc_launch_digest( + 1, + "EPYC-v4".into(), + "./tests/measurement/ovmf_AmdSev_suffix.bin".into(), + None, + None, + None, + None, + None, + ) + .unwrap(); + + let ld_hex = hex::encode(ld); + + let exp_result = "e5e6be5a8fa6256f0245666bb237e2d028b7928148ce78d51b8a64dc9506c377709a5b5d7ab75554593bced304fcff93"; + + assert_eq!(ld_hex.as_str(), exp_result); + } + + // Test snp with multiple cpus + #[test] + fn test_snp_with_multiple_vcpus() { + let ld = snp_calc_launch_digest( + 4, + "EPYC-v4".into(), + "./tests/measurement/ovmf_AmdSev_suffix.bin".into(), + Some("/dev/null".into()), + Some("/dev/null".into()), + None, + None, + None, + ) + .unwrap(); + + let ld_hex = hex::encode(ld); + + let exp_result = "1c784beb8c49aa604b7fd57fbc73b36ec53a3f5fb48a2b895ad6cc2ea15d18ee7cc15e3e57c792766b45f944c3e81cfe"; + + assert_eq!(ld_hex.as_str(), exp_result); + } + + // Test snp with with ovmf64 and no kernel + #[test] + fn test_snp_with_ovmfx64_without_kernel() { + let ld = snp_calc_launch_digest( + 1, + "EPYC-v4".into(), + "./tests/measurement/ovmf_OvmfX64_suffix.bin".into(), + None, + None, + None, + None, + None, + ) + .unwrap(); + + let ld_hex = hex::encode(ld); + + let exp_result = "7ef631fa7f659f7250de96c456a0eb7354bd3b9461982f386a41c6a6aede87870ad020552a5a0716672d5d6e5b86b8f9"; + + assert_eq!(ld_hex.as_str(), exp_result); + } + + // Test non-SNP OVMF and SNP measure should fail + #[test] + #[should_panic( + expected = "Kernel specified but OVMF metadata doesn't include SNP_KERNEL_HASHES section" + )] + fn test_snp_with_ovmfx64_and_kernel_should_fail() { + panic!( + "{}", + snp_calc_launch_digest( + 1, + "EPYC-v4".into(), + "./tests/measurement/ovmf_OvmfX64_suffix.bin".into(), + Some("/dev/null".into()), + Some("/dev/null".into()), + None, + None, + None + ) + .unwrap_err() + .to_string() + ); + } +} + +#[cfg(feature = "sev")] +mod sev_tests { + use sev::measurement::measurement_functions::*; + // test regular sev-es + #[test] + fn test_seves() { + let ld = seves_calc_launch_digest( + 1, + "EPYC-v4".into(), + "./tests/measurement/ovmf_AmdSev_suffix.bin".into(), + Some("/dev/null".into()), + Some("/dev/null".into()), + None, + None, + ) + .unwrap(); + + let ld_hex = hex::encode(ld); + + let exp_result = "2e91d54814445ad178180af09f881efe4079fc54bfddd0ec1179ecd3cdbdf772"; + + assert_eq!(ld_hex.as_str(), exp_result); + } + + // test sev-es with multiple vcpus + #[test] + fn test_seves_with_multiple_vcpus() { + let ld = seves_calc_launch_digest( + 4, + "EPYC-v4".into(), + "./tests/measurement/ovmf_AmdSev_suffix.bin".into(), + Some("/dev/null".into()), + Some("/dev/null".into()), + None, + None, + ) + .unwrap(); + + let ld_hex = hex::encode(ld); + + let exp_result = "c05d37600072dc5ff24bafc49410f0369ba3a37c130a7bb7055ac6878be300f7"; + + assert_eq!(ld_hex.as_str(), exp_result); + } + + // Test that kernel specified doesn't work with OVMF + #[test] + #[should_panic( + expected = "Kernel specified but OVMF doesn't support kernel/initrd/cmdline measurement" + )] + fn test_seves_with_ovmfx64_and_kernel_should_fail() { + panic!( + "{}", + seves_calc_launch_digest( + 1, + "EPYC-v4".into(), + "./tests/measurement/ovmf_OvmfX64_suffix.bin".into(), + Some("/dev/null".into()), + Some("/dev/null".into()), + None, + None + ) + .unwrap_err() + .to_string() + ); + } + + // test regular sev + #[test] + fn test_sev() { + let ld = sev_calc_launch_digest( + "./tests/measurement/ovmf_AmdSev_suffix.bin".into(), + Some("/dev/null".into()), + Some("/dev/null".into()), + Some("console=ttyS0 loglevel=7"), + ) + .unwrap(); + + let ld_hex = hex::encode(ld); + + let exp_result = "f0d92a1fda00249e008820bd40def6abbed2ee65fea8a8bc47e532863ca0cc6a"; + + assert_eq!(ld_hex.as_str(), exp_result); + } + + // test sev kernel with no initrd or append + #[test] + fn test_sev_with_kernel_without_initrd_and_append() { + let ld = sev_calc_launch_digest( + "./tests/measurement/ovmf_AmdSev_suffix.bin".into(), + Some("/dev/null".into()), + None, + None, + ) + .unwrap(); + + let ld_hex = hex::encode(ld); + + let exp_result = "7332f6ef294f79919b46302e4541900a2dfc96714e2b7b4b5ccdc1899b78a195"; + + assert_eq!(ld_hex.as_str(), exp_result); + } + + // test sev with ovmfx64 + #[test] + fn test_sev_with_ovmfx64_without_kernel() { + let ld = sev_calc_launch_digest( + "./tests/measurement/ovmf_OvmfX64_suffix.bin".into(), + None, + None, + None, + ) + .unwrap(); + + let ld_hex = hex::encode(ld); + + let exp_result = "af9d6c674b1ff04937084c98c99ca106b25c37b2c9541ac313e6e0c54426314f"; + + assert_eq!(ld_hex.as_str(), exp_result); + } + + // Test that kernel specified doesn't work with OVMF + #[test] + #[should_panic( + expected = "Kernel specified but OVMF doesn't support kernel/initrd/cmdline measurement" + )] + fn test_sev_with_ovmfx64_and_kernel_should_fail() { + panic!( + "{}", + sev_calc_launch_digest( + "./tests/measurement/ovmf_OvmfX64_suffix.bin".into(), + Some("/dev/null".into()), + Some("/dev/null".into()), + None + ) + .unwrap_err() + .to_string() + ); + } +} diff --git a/tests/measurement/ovmf_AmdSev_suffix.bin b/tests/measurement/ovmf_AmdSev_suffix.bin new file mode 100644 index 00000000..4ecfc95a Binary files /dev/null and b/tests/measurement/ovmf_AmdSev_suffix.bin differ diff --git a/tests/measurement/ovmf_OvmfX64_suffix.bin b/tests/measurement/ovmf_OvmfX64_suffix.bin new file mode 100644 index 00000000..43a56981 Binary files /dev/null and b/tests/measurement/ovmf_OvmfX64_suffix.bin differ