diff --git a/.snpcc_canary b/.snpcc_canary index a363e415e587..1e94f06b5368 100644 --- a/.snpcc_canary +++ b/.snpcc_canary @@ -3,4 +3,4 @@ O \ o | / /-xXx--//-----x=x--/-xXx--/---x---->>>--/ ... -.. \ No newline at end of file +... \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ede37832dff4..9c69516cccaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added - Added TypeScript `TypedKvSet` and `ccfapp.typedKv` to facilitate set handling from application code. +- Added support for UVM endorsements signed with EC keys (#6231). ### Removed diff --git a/CMakeLists.txt b/CMakeLists.txt index 4388baddd988..3d0eb11951ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -936,6 +936,18 @@ if(BUILD_TESTS) http_parser.host ) + add_unit_test( + endorsements_test + ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/endorsements.cpp + ) + set_property( + TEST endorsements_test + APPEND + PROPERTY + ENVIRONMENT + "TEST_ENDORSEMENTS_PATH=${CMAKE_CURRENT_SOURCE_DIR}/tests/uvm_endorsements" + ) + add_unit_test( historical_queries_test ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/historical_queries.cpp diff --git a/include/ccf/crypto/cose_verifier.h b/include/ccf/crypto/cose_verifier.h index ff4742377301..0d30c270a413 100644 --- a/include/ccf/crypto/cose_verifier.h +++ b/include/ccf/crypto/cose_verifier.h @@ -22,6 +22,7 @@ namespace crypto using COSEVerifierUniquePtr = std::unique_ptr; - COSEVerifierUniquePtr make_cose_verifier(const std::vector& cert); - COSEVerifierUniquePtr make_cose_verifier(const RSAPublicKeyPtr& pubk_ptr); + COSEVerifierUniquePtr make_cose_verifier_from_cert( + const std::vector& cert); + COSEVerifierUniquePtr make_cose_verifier_from_key(const Pem& public_key); } diff --git a/src/crypto/openssl/cose_verifier.cpp b/src/crypto/openssl/cose_verifier.cpp index 5c5666655f3e..f2fe746c6f87 100644 --- a/src/crypto/openssl/cose_verifier.cpp +++ b/src/crypto/openssl/cose_verifier.cpp @@ -19,7 +19,7 @@ namespace crypto { using namespace OpenSSL; - COSEVerifier_OpenSSL::COSEVerifier_OpenSSL( + COSECertVerifier_OpenSSL::COSECertVerifier_OpenSSL( const std::vector& certificate) { Unique_BIO certbio(certificate); @@ -53,10 +53,9 @@ namespace crypto } } - COSEVerifier_OpenSSL::COSEVerifier_OpenSSL(const RSAPublicKeyPtr& pubk_ptr) + COSEKeyVerifier_OpenSSL::COSEKeyVerifier_OpenSSL(const Pem& public_key_) { - public_key = - std::make_shared(pubk_ptr->public_key_pem()); + public_key = std::make_shared(public_key_); } COSEVerifier_OpenSSL::~COSEVerifier_OpenSSL() = default; @@ -92,13 +91,14 @@ namespace crypto return false; } - COSEVerifierUniquePtr make_cose_verifier(const std::vector& cert) + COSEVerifierUniquePtr make_cose_verifier_from_cert( + const std::vector& cert) { - return std::make_unique(cert); + return std::make_unique(cert); } - COSEVerifierUniquePtr make_cose_verifier(const RSAPublicKeyPtr& pubk_ptr) + COSEVerifierUniquePtr make_cose_verifier_from_key(const Pem& public_key) { - return std::make_unique(pubk_ptr); + return std::make_unique(public_key); } } diff --git a/src/crypto/openssl/cose_verifier.h b/src/crypto/openssl/cose_verifier.h index 34a4f28610fb..a906f4860dec 100644 --- a/src/crypto/openssl/cose_verifier.h +++ b/src/crypto/openssl/cose_verifier.h @@ -15,15 +15,25 @@ namespace crypto { class COSEVerifier_OpenSSL : public COSEVerifier { - private: + protected: std::shared_ptr public_key; public: - COSEVerifier_OpenSSL(const std::vector& certificate); - COSEVerifier_OpenSSL(const RSAPublicKeyPtr& pubk_ptr); virtual ~COSEVerifier_OpenSSL() override; virtual bool verify( const std::span& buf, std::span& authned_content) const override; }; + + class COSECertVerifier_OpenSSL : public COSEVerifier_OpenSSL + { + public: + COSECertVerifier_OpenSSL(const std::vector& certificate); + }; + + class COSEKeyVerifier_OpenSSL : public COSEVerifier_OpenSSL + { + public: + COSEKeyVerifier_OpenSSL(const Pem& public_key); + }; } diff --git a/src/endpoints/authentication/cose_auth.cpp b/src/endpoints/authentication/cose_auth.cpp index 2a81b0da756f..1574982a3ae4 100644 --- a/src/endpoints/authentication/cose_auth.cpp +++ b/src/endpoints/authentication/cose_auth.cpp @@ -302,7 +302,7 @@ namespace ccf auto member_cert = member_certs->get(phdr.kid); if (member_cert.has_value()) { - auto verifier = crypto::make_cose_verifier(member_cert->raw()); + auto verifier = crypto::make_cose_verifier_from_cert(member_cert->raw()); std::span body = { ctx->get_request_body().data(), ctx->get_request_body().size()}; @@ -441,7 +441,7 @@ namespace ccf auto user_cert = user_certs->get(phdr.kid); if (user_cert.has_value()) { - auto verifier = crypto::make_cose_verifier(user_cert->raw()); + auto verifier = crypto::make_cose_verifier_from_cert(user_cert->raw()); std::span body = { ctx->get_request_body().data(), ctx->get_request_body().size()}; diff --git a/src/node/did.h b/src/node/did.h index eb45f2ae3c91..929c504e835a 100644 --- a/src/node/did.h +++ b/src/node/did.h @@ -18,8 +18,7 @@ namespace ccf::did std::string id; std::string type; std::string controller; - std::optional public_key_jwk = - std::nullopt; // Note: Only supports RSA for now + std::optional public_key_jwk = std::nullopt; bool operator==(const DIDDocumentVerificationMethod&) const = default; }; diff --git a/src/node/test/endorsements.cpp b/src/node/test/endorsements.cpp new file mode 100644 index 000000000000..de4f3524d94b --- /dev/null +++ b/src/node/test/endorsements.cpp @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. + +#include "ccf/pal/measurement.h" +#include "crypto/openssl/hash.h" +#include "ds/files.h" +#include "node/uvm_endorsements.h" + +#define DOCTEST_CONFIG_IMPLEMENT +#include +#include + +TEST_CASE("Check RSA Production endorsement") +{ + char* end_path = std::getenv("TEST_ENDORSEMENTS_PATH"); + REQUIRE(end_path != nullptr); + + auto endorsement = files::slurp(fmt::format("{}/rsa_test1.cose", end_path)); + REQUIRE(!endorsement.empty()); + + ccf::pal::SnpAttestationMeasurement measurement( + "02c3b0d5bf1d256fa4e3b5deefc07b55ff2f7029085ed350f60959140a1a51f1310753ba5a" + "b2c03a0536b1c0c193af47"); + ccf::pal::PlatformAttestationMeasurement uvm_measurement(measurement); + auto endorsements = + ccf::verify_uvm_endorsements(endorsement, uvm_measurement); + REQUIRE( + endorsements == + ccf::UVMEndorsements{ + "did:x509:0:sha256:I__iuL25oXEVFdTP_aBLx_eT1RPHbCQ_ECBQfYZpt9s::eku:1.3." + "6.1.4.1.311.76.59.1.2", + "ContainerPlat-AMD-UVM", + "100"}); +} + +TEST_CASE("Check ECDSA Test endorsement") +{ + char* end_path = std::getenv("TEST_ENDORSEMENTS_PATH"); + REQUIRE(end_path != nullptr); + + auto endorsement = files::slurp(fmt::format("{}/ecdsa_test1.cose", end_path)); + REQUIRE(!endorsement.empty()); + + ccf::pal::SnpAttestationMeasurement measurement( + "5a84c66e9c8dd1a991e6d8b43a8aaae488940f87ce25ef6a62ad180cc3c73554ed7e4ccd10" + "13456602758778d9d65c48"); + ccf::pal::PlatformAttestationMeasurement uvm_measurement(measurement); + REQUIRE_THROWS_WITH_AS( + ccf::verify_uvm_endorsements(endorsement, uvm_measurement), + "UVM endorsements did " + "did:x509:0:sha256:VFsRLNBh5Zy1HRtVl2IIXAl0lUs-xobEbskZ3XRDpCY::subject:CN:" + "Test%20Leaf%20%28DO%20NOT%20TRUST%29, feed ConfAKS-AMD-UVM-Test, svn 0 do " + "not match any of the known UVM roots of trust", + std::logic_error); + + std::vector custom_roots_of_trust = { + ccf::UVMEndorsements{ + "did:x509:0:sha256:VFsRLNBh5Zy1HRtVl2IIXAl0lUs-xobEbskZ3XRDpCY::subject:" + "CN:Test%20Leaf%20%28DO%20NOT%20TRUST%29", + "ConfAKS-AMD-UVM-Test", + "0"}}; + + auto endorsements = ccf::verify_uvm_endorsements( + endorsement, uvm_measurement, custom_roots_of_trust); + REQUIRE(endorsements == custom_roots_of_trust[0]); +} + +int main(int argc, char** argv) +{ + logger::config::default_init(); + crypto::openssl_sha256_init(); + doctest::Context context; + context.applyCommandLine(argc, argv); + int res = context.run(); + crypto::openssl_sha256_shutdown(); + if (context.shouldExit()) + return res; + return res; +} \ No newline at end of file diff --git a/src/node/uvm_endorsements.h b/src/node/uvm_endorsements.h index 7cd23077bd52..6e94a95dce0c 100644 --- a/src/node/uvm_endorsements.h +++ b/src/node/uvm_endorsements.h @@ -4,6 +4,7 @@ #include "ccf/crypto/base64.h" #include "ccf/ds/json.h" +#include "ccf/pal/measurement.h" #include "ccf/service/tables/uvm_endorsements.h" #include "crypto/openssl/cose_verifier.h" #include "node/cose_common.h" @@ -52,19 +53,21 @@ namespace ccf }; // Roots of trust for UVM endorsements/measurement in AMD SEV-SNP attestations - static std::vector uvm_roots_of_trust = { + static std::vector default_uvm_roots_of_trust = { // Confidential Azure Kubertnetes Service (AKS) {"did:x509:0:sha256:I__iuL25oXEVFdTP_aBLx_eT1RPHbCQ_ECBQfYZpt9s::eku:1.3.6." "1.4.1.311.76.59.1.2", "ContainerPlat-AMD-UVM", - "0"}, + "100"}, // Confidential Azure Container Instances (ACI) {"did:x509:0:sha256:I__iuL25oXEVFdTP_aBLx_eT1RPHbCQ_ECBQfYZpt9s::eku:1.3.6." "1.4.1.311.76.59.1.5", "ConfAKS-AMD-UVM", "0"}}; - bool inline matches_uvm_roots_of_trust(const UVMEndorsements& endorsements) + bool inline matches_uvm_roots_of_trust( + const UVMEndorsements& endorsements, + const std::vector& uvm_roots_of_trust) { for (const auto& uvm_root_of_trust : uvm_roots_of_trust) { @@ -248,10 +251,10 @@ namespace ccf } static std::span verify_uvm_endorsements_signature( - const crypto::RSAPublicKeyPtr& leef_cert_pub_key, + const crypto::Pem& leaf_cert_pub_key, const std::vector& uvm_endorsements_raw) { - auto verifier = crypto::make_cose_verifier(leef_cert_pub_key); + auto verifier = crypto::make_cose_verifier_from_key(leaf_cert_pub_key); std::span payload; if (!verifier->verify(uvm_endorsements_raw, payload)) @@ -264,14 +267,16 @@ namespace ccf static UVMEndorsements verify_uvm_endorsements( const std::vector& uvm_endorsements_raw, - const pal::PlatformAttestationMeasurement& uvm_measurement) + const pal::PlatformAttestationMeasurement& uvm_measurement, + const std::vector& uvm_roots_of_trust = + default_uvm_roots_of_trust) { auto phdr = cose::decode_protected_header(uvm_endorsements_raw); - if (!cose::is_rsa_alg(phdr.alg)) + if (!(cose::is_rsa_alg(phdr.alg) || cose::is_ecdsa_alg(phdr.alg))) { - throw std::logic_error( - fmt::format("Signature algorithm {} is not expected RSA", phdr.alg)); + throw std::logic_error(fmt::format( + "Signature algorithm {} is not one of expected: RSA, ECDSA", phdr.alg)); } std::string pem_chain; @@ -292,17 +297,37 @@ namespace ccf did_document_str)); } - crypto::RSAPublicKeyPtr pubk = nullptr; + crypto::Pem pubk; for (auto const& vm : did_document.verification_method) { if (vm.controller == did && vm.public_key_jwk.has_value()) { - pubk = crypto::make_rsa_public_key(vm.public_key_jwk.value()); - break; + auto jwk = vm.public_key_jwk.value().get(); + switch (jwk.kty) + { + case crypto::JsonWebKeyType::RSA: + { + auto rsa_jwk = + vm.public_key_jwk->get(); + pubk = crypto::make_rsa_public_key(rsa_jwk)->public_key_pem(); + break; + } + case crypto::JsonWebKeyType::EC: + { + auto ec_jwk = vm.public_key_jwk->get(); + pubk = crypto::make_public_key(ec_jwk)->public_key_pem(); + break; + } + default: + { + throw std::logic_error(fmt::format( + "Unsupported public key type ({}) for DID {}", jwk.kty, did)); + } + } } } - if (pubk == nullptr) + if (pubk.empty()) { throw std::logic_error(fmt::format( "Could not find matching public key for DID {} in {}", @@ -341,7 +366,7 @@ namespace ccf UVMEndorsements end{did, phdr.feed, payload.sevsnpvm_guest_svn}; - if (!matches_uvm_roots_of_trust(end)) + if (!matches_uvm_roots_of_trust(end, uvm_roots_of_trust)) { throw std::logic_error(fmt::format( "UVM endorsements did {}, feed {}, svn {} " diff --git a/tests/uvm_endorsements/ecdsa_test1.cose b/tests/uvm_endorsements/ecdsa_test1.cose new file mode 100644 index 000000000000..e167c8c80a8a Binary files /dev/null and b/tests/uvm_endorsements/ecdsa_test1.cose differ diff --git a/tests/uvm_endorsements/rsa_test1.cose b/tests/uvm_endorsements/rsa_test1.cose new file mode 100644 index 000000000000..ffc1a2439c29 Binary files /dev/null and b/tests/uvm_endorsements/rsa_test1.cose differ