From e1afe4d7ea073627d1e0171f4806353ffb42fa70 Mon Sep 17 00:00:00 2001 From: Ahson Khan Date: Fri, 13 Sep 2024 19:35:40 -0700 Subject: [PATCH] Add support for sending an x5c parameter in the JWT token header for `ClientCertificateCredential`. (#5988) * Add public surface area to support sending a chain of certs. * Add x5c param to the JWT token. * Simplify test. * Address PR feedback and fix typo. * Fix typo. --- sdk/identity/azure-identity/CHANGELOG.md | 1 + .../client_certificate_credential.hpp | 17 +++- .../src/client_certificate_credential.cpp | 48 +++++++++- .../ut/client_certificate_credential_test.cpp | 94 +++++++++++++++++-- 4 files changed, 151 insertions(+), 9 deletions(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 89773d52a..afbc61f42 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features Added - Added support for providing an object ID to `ManagedIdentityCredential`. +- Added support for sending an x5c parameter in `ClientCertificateCredential`. ### Breaking Changes diff --git a/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp b/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp index f3e1bd506..54d680964 100644 --- a/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp +++ b/sdk/identity/azure-identity/inc/azure/identity/client_certificate_credential.hpp @@ -60,6 +60,16 @@ namespace Azure { namespace Identity { * for any tenant in which the application is installed. */ std::vector AdditionallyAllowedTenants; + + /** + * @brief SendCertificateChain controls whether the credential sends the public certificate + * chain in the x5c header of each token request's JWT. This is required for Subject Name/Issuer + * (SNI) authentication. + * + * @note Defaults to false. + * + */ + bool SendCertificateChain = false; }; /** @@ -83,6 +93,7 @@ namespace Azure { namespace Identity { std::string const& clientCertificatePath, std::string const& authorityHost, std::vector additionallyAllowedTenants, + bool sendCertificateChain, Core::Credentials::TokenCredentialOptions const& options); public: @@ -91,7 +102,8 @@ namespace Azure { namespace Identity { * * @param tenantId Tenant ID. * @param clientId Client ID. - * @param clientCertificatePath Client certificate path. + * @param clientCertificatePath The path to a Privacy Enhanced Mail (PEM) file containing + * exactly one certificate which is used for signing along with its corresponding private key. * @param options Options for token retrieval. */ explicit ClientCertificateCredential( @@ -106,7 +118,8 @@ namespace Azure { namespace Identity { * * @param tenantId Tenant ID. * @param clientId Client ID. - * @param clientCertificatePath Client certificate path. + * @param clientCertificatePath The path to a Privacy Enhanced Mail (PEM) file containing + * exactly one certificate which is used for signing along with its corresponding private key. * @param options Options for token retrieval. */ explicit ClientCertificateCredential( diff --git a/sdk/identity/azure-identity/src/client_certificate_credential.cpp b/sdk/identity/azure-identity/src/client_certificate_credential.cpp index 00ef03f34..19d94bb05 100644 --- a/sdk/identity/azure-identity/src/client_certificate_credential.cpp +++ b/sdk/identity/azure-identity/src/client_certificate_credential.cpp @@ -77,6 +77,38 @@ template std::vector ToUInt8Vector(T const& in) return outVec; } +std::string FindPemCertificateContent(std::string const& path) +{ + auto pemContent{FileBodyStream(path).ReadToEnd()}; + std::string pem{pemContent.begin(), pemContent.end()}; + pemContent = {}; + + const std::string beginHeader = std::string("-----BEGIN CERTIFICATE-----"); + auto headerStart = pem.find(beginHeader); + if (headerStart == std::string::npos) + { + throw AuthenticationException("PEM file does not contain certificate."); + } + + auto footerStart = pem.find("-----END CERTIFICATE-----", headerStart); + if (footerStart == std::string::npos) + { + throw AuthenticationException("PEM file does not contain a valid end certificate marker."); + } + + // Move past the begin marker + headerStart += beginHeader.length(); + + // Extract the certificate without the end marker + std::string certificate = pem.substr(headerStart, footerStart - headerStart); + + // Remove all new lines + certificate.erase(std::remove(certificate.begin(), certificate.end(), '\n'), certificate.end()); + certificate.erase(std::remove(certificate.begin(), certificate.end(), '\r'), certificate.end()); + + return certificate; +} + using CertificateThumbprint = std::vector; using UniquePrivateKey = Azure::Identity::_detail::UniquePrivateKey; using PrivateKey = decltype(std::declval().get()); @@ -383,6 +415,7 @@ ClientCertificateCredential::ClientCertificateCredential( std::string const& clientCertificatePath, std::string const& authorityHost, std::vector additionallyAllowedTenants, + bool sendCertificateChain, Core::Credentials::TokenCredentialOptions const& options) : TokenCredential("ClientCertificateCredential"), m_clientCredentialCore(tenantId, authorityHost, additionallyAllowedTenants), @@ -448,9 +481,20 @@ ClientCertificateCredential::ClientCertificateCredential( thumbprintBase64Str = Base64Url::Base64UrlEncode(ToUInt8Vector(mdVec)); } + std::string x5cHeaderParam{}; + if (sendCertificateChain) + { + // Since there is only one base64 encoded cert string, it can be written as a JSON string rather + // than a JSON array of strings. + x5cHeaderParam = ",\"x5c\":\""; + std::string certContent = FindPemCertificateContent(clientCertificatePath); + x5cHeaderParam += certContent; + x5cHeaderParam += "\""; + } + // Form a JWT token: const auto tokenHeader = std::string("{\"x5t\":\"") + thumbprintBase64Str + "\",\"kid\":\"" - + thumbprintHexStr + "\",\"alg\":\"RS256\",\"typ\":\"JWT\"}"; + + thumbprintHexStr + "\",\"alg\":\"RS256\",\"typ\":\"JWT\"" + x5cHeaderParam + "}"; const auto tokenHeaderVec = std::vector(tokenHeader.begin(), tokenHeader.end()); @@ -469,6 +513,7 @@ ClientCertificateCredential::ClientCertificateCredential( clientCertificatePath, options.AuthorityHost, options.AdditionallyAllowedTenants, + options.SendCertificateChain, options) { } @@ -484,6 +529,7 @@ ClientCertificateCredential::ClientCertificateCredential( clientCertificatePath, ClientCertificateCredentialOptions{}.AuthorityHost, ClientCertificateCredentialOptions{}.AdditionallyAllowedTenants, + false, // By default, we don't send the x5c property options) { } diff --git a/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp b/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp index 511b73393..3dc93dc52 100644 --- a/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp +++ b/sdk/identity/azure-identity/test/ut/client_certificate_credential_test.cpp @@ -20,7 +20,8 @@ namespace { enum CertFormat { RsaPkcs, - RsaRaw + RsaRaw, + RsaRawReverse }; enum TestType @@ -46,12 +47,14 @@ class GetCredentialName : public ::testing::TestWithParam { TempCertFile m_certFile{GetParam()}; }; -class GetToken : public ::testing::TestWithParam> { +class GetToken : public ::testing::TestWithParam> { public: TestType GetTestType() { return std::get<0>(GetParam()); } CertFormat GetCertFormat() { return std::get<1>(GetParam()); } + bool GetSendCertChain() { return std::get<2>(GetParam()); } + std::string GetTenantId() { return GetTestType() == TestType::AzureStack ? "adfs" : "01234567-89ab-cdef-fedc-ba8976543210"; @@ -105,9 +108,36 @@ public: } std::string GetHeader() - { // cspell:disable - return "{\"x5t\":\"V0pIIQwSzNn6vfSTPv-1f7Vt_Pw\",\"kid\":" - "\"574A48210C12CCD9FABDF4933EFFB57FB56DFCFC\",\"alg\":\"RS256\",\"typ\":\"JWT\"}"; + { + // cspell:disable + std::string x5t = "\"V0pIIQwSzNn6vfSTPv-1f7Vt_Pw\""; + std::string kid = "\"574A48210C12CCD9FABDF4933EFFB57FB56DFCFC\""; + std::string x5c + = "\"MIIDODCCAiCgAwIBAgIQNqa9U3MBxqBF7ksWk+" + "XRkzANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0MCAXDTIyMDQyMjE1MDYw" + "NloYDzIyMjIwMTAxMDcwMDAwWjAeMRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0MIIBIjANBgkqhkiG9w" + "0BAQEFAAOCAQ8AMIIBCgKCAQEAz3ZuKbpDu7oBMfMF65qOFSBKInKe8N0LBCRgNmzMfZxzL8LoBueLdeEKX6gU" + "GEFi3i9R5qXA3or1Q/teWV3hiwj1WQR4aGPGVhom34QAM6kND/" + "QmtZMnY7weLiXBJxf0WLUL+p+jsJnTtcCdtiTXEZTLWapp2/" + "0NCJ9n41xG3ZfOfxmZWMzEEXcnyNMq4kkQXGFdpINM3lwsX5grwd62+iNSqaFBR5ZHh7ZHg8JtFR1BLeB8/" + "IIXAdNLSOXktnx9qz5CDUCnOvtEFAtiiAkAvhsybGA28EDmqOVYZPNB+S0bjPTXc7/n1N5S55LWAoF4C/QF+C/" + "0fWeD87bmqP6m0QIDAQABo3AwbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFB" + "wMBMB4GA1UdEQQXMBWCE2F6dXJlLWlkZW50aXR5LXRlc3QwHQYDVR0OBBYEFCoJ5tInmafyNuR0tGxZOz522jl" + "WMA0GCSqGSIb3DQEBCwUAA4IBAQBzLXpwXmrg1sQTmzMnS24mREKxj9B3YILmgsdBMrHkH07QUROee7IbQ8gfB" + "Keln0dEcfYiJyh42jn+fmg9AR17RP80wPthD2eKOt4WYNkNM3H8U4JEo+0ML0jZyswynpR48h/Em96sm/" + "NUeKUViD5iVTb1uHL4j8mQAN1IbXcunXvrrek1CzFVn5Rpah0Tn+" + "6cYVKdJg531i53udzusgZtV1NPZ82tzYkPQG1vxB//D9vd0LzmcfCvT50MKhz0r/" + "c5yJYki9q94DBuzMhe+O9j+Ob2pVQt5akVFJVtIVSfBZzRBAd66u9JeADlT4sxwS4QAUHiRrCsEpJsnJXkx/" + "6O\""; + + if (GetSendCertChain()) + { + return "{\"x5t\":" + x5t + ",\"kid\":" + kid + + ",\"alg\":\"RS256\",\"typ\":\"JWT\"," + "\"x5c\":" + + x5c + "}"; + } + return "{\"x5t\":" + x5t + ",\"kid\":" + kid + ",\"alg\":\"RS256\",\"typ\":\"JWT\"}"; } std::string GetPayloadStart() @@ -249,6 +279,7 @@ TEST_P(GetToken, ) options.AuthorityHost = "https://microsoft.com/"; } options.Transport.Transport = transport; + options.SendCertificateChain = GetSendCertChain(); return std::make_unique( GetTenantId(), "fedcba98-7654-3210-0123-456789abcdef", TempCertFile::Path, options); @@ -363,7 +394,8 @@ INSTANTIATE_TEST_SUITE_P( GetToken, testing::Combine( testing::Values(Regular, AzureStack, Authority), - testing::Values(RsaPkcs, RsaRaw))); + testing::Values(RsaPkcs, RsaRaw, RsaRawReverse), + testing::Values(true, false))); namespace { const char* const TempCertFile::Path = "azure-identity-test.pem"; @@ -490,6 +522,56 @@ TempCertFile::TempCertFile(CertFormat format) "RrCsEpJsnJXkx/6O\n" "-----END CERTIFICATE-----"; // cspell:enable + else if (format == RsaRawReverse) + cert << // cspell:disable + "-----BEGIN CERTIFICATE-----\n" + "MIIDODCCAiCgAwIBAgIQNqa9U3MBxqBF7ksWk+XRkzANBgkqhkiG9w0BAQsFADAe\n" + "MRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0MCAXDTIyMDQyMjE1MDYwNloY\n" + "DzIyMjIwMTAxMDcwMDAwWjAeMRwwGgYDVQQDDBNhenVyZS1pZGVudGl0eS10ZXN0\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz3ZuKbpDu7oBMfMF65qO\n" + "FSBKInKe8N0LBCRgNmzMfZxzL8LoBueLdeEKX6gUGEFi3i9R5qXA3or1Q/teWV3h\n" + "iwj1WQR4aGPGVhom34QAM6kND/QmtZMnY7weLiXBJxf0WLUL+p+jsJnTtcCdtiTX\n" + "EZTLWapp2/0NCJ9n41xG3ZfOfxmZWMzEEXcnyNMq4kkQXGFdpINM3lwsX5grwd62\n" + "+iNSqaFBR5ZHh7ZHg8JtFR1BLeB8/IIXAdNLSOXktnx9qz5CDUCnOvtEFAtiiAkA\n" + "vhsybGA28EDmqOVYZPNB+S0bjPTXc7/n1N5S55LWAoF4C/QF+C/0fWeD87bmqP6m\n" + "0QIDAQABo3AwbjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwIG\n" + "CCsGAQUFBwMBMB4GA1UdEQQXMBWCE2F6dXJlLWlkZW50aXR5LXRlc3QwHQYDVR0O\n" + "BBYEFCoJ5tInmafyNuR0tGxZOz522jlWMA0GCSqGSIb3DQEBCwUAA4IBAQBzLXpw\n" + "Xmrg1sQTmzMnS24mREKxj9B3YILmgsdBMrHkH07QUROee7IbQ8gfBKeln0dEcfYi\n" + "Jyh42jn+fmg9AR17RP80wPthD2eKOt4WYNkNM3H8U4JEo+0ML0jZyswynpR48h/E\n" + "m96sm/NUeKUViD5iVTb1uHL4j8mQAN1IbXcunXvrrek1CzFVn5Rpah0Tn+6cYVKd\n" + "Jg531i53udzusgZtV1NPZ82tzYkPQG1vxB//D9vd0LzmcfCvT50MKhz0r/c5yJYk\n" + "i9q94DBuzMhe+O9j+Ob2pVQt5akVFJVtIVSfBZzRBAd66u9JeADlT4sxwS4QAUHi\n" + "RrCsEpJsnJXkx/6O\n" + "-----END CERTIFICATE-----\n" + "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEpAIBAAKCAQEAz3ZuKbpDu7oBMfMF65qOFSBKInKe8N0LBCRgNmzMfZxzL8Lo\n" + "BueLdeEKX6gUGEFi3i9R5qXA3or1Q/teWV3hiwj1WQR4aGPGVhom34QAM6kND/Qm\n" + "tZMnY7weLiXBJxf0WLUL+p+jsJnTtcCdtiTXEZTLWapp2/0NCJ9n41xG3ZfOfxmZ\n" + "WMzEEXcnyNMq4kkQXGFdpINM3lwsX5grwd62+iNSqaFBR5ZHh7ZHg8JtFR1BLeB8\n" + "/IIXAdNLSOXktnx9qz5CDUCnOvtEFAtiiAkAvhsybGA28EDmqOVYZPNB+S0bjPTX\n" + "c7/n1N5S55LWAoF4C/QF+C/0fWeD87bmqP6m0QIDAQABAoIBAQDEGSK6KIk7me7l\n" + "QtyWvemNSI8qjoN0EswF50hWSXLlTIuIWsgtNpIZI1VF477SyoNklv/ob0amVFzP\n" + "HHwrJtU5MYeP0+zoZ18jJecWoVP7gNCLAvHP8b9qw3cXkbJIfJkHfGJNTLZSCKUY\n" + "CHBKqfnscWPhZnZXbZLzUpHFVATcEJ14vqFj4RNoLqNoNQT5NoGxdPtxb0q0PEMB\n" + "h4PrkCcK0KSfkgfU8rkBWrhkef8Eqh/d3BR+WAv/r+SO6lumUHtH+6xCkA8mxlc5\n" + "AZSichglWJj5+12v8Ca4sLPhWSHx8395tJCYoMSXfx8E65ykoPh/KAYJ4O5WS3QW\n" + "FhzBiYQNAoGBAOPJqFu7M3oL3y7lBWtLB38irjcrzr+1rneLGtJcHSjx0vmrcC+k\n" + "zVFggBpKJmAAxHt6omIDFw1/VN4ZVus5LWBY9N7Z0YOIgY6fJ3ISwVS391neUz0c\n" + "NVSruGVuN8vAUYWFlft2eLNZ8jBAwDRWykZi+ywwdOaFh3STIxSvy+mHAoGBAOko\n" + "VeL9kUIl85Fuhh0gWQyFRwnlsLyJXTpRHxu8M2VuHvMDQ4X0jLV8ia832xMlwbVS\n" + "qBEnT+jZ5vVu37XMp1veuUveEx7su/qH7x6OiQJvIP9Ll+9MGdui1PKoZCTE1prD\n" + "jQTSi8FM5BU+1RrHWgZYmptUS743k1EXUIJ37SLnAoGBAOBWGpk9JNVuG7/zjgK9\n" + "QgTUAwATBOuJ4umY9jF2xsEsaLu7PCGwDQW4JHG/1Ut3dgqmHIaqxGlmng6ephvD\n" + "lAzvjzprCwyfw/jSheay0fS9ub2oWBI3Vc6t0E0U356rKZ52kd+2Lel1DDC5lJH3\n" + "Z/8qPHSoxHjDyUPmJQaanBjBAoGAWa5iGsVdsgvW/AF/JITku6QoBu6KZHqRmXTK\n" + "emiRfFo3HVIMDuJZnRUiAHuDkIHdWFlKvA5a9j2aUJ0s/0iQtw2cSEpLIIH+bAcN\n" + "Oruoh38nOgthjXHAIHMpZYzPuDTeNvkwrMIvb1KcCG/6mCpFvlsmXMi3uZq212IY\n" + "XZazZ9ECgYA3vGkRvjDklE014wFbLGw2NFLPeNxTfdagZmoDag8qMygAKg6Cr3Uc\n" + "TNCJSA5zqbY+AH26SdSU4TTiQ2AaVPgM6PFKHnQDYJ3bWdp9dUUo5pUOkxP1hpbI\n" + "qxxMaq+sv5e9c56EJtctxNnAK27JsoadD+b+NjysZgMeKUdBIzSrHQ==\n" + "-----END RSA PRIVATE KEY-----"; + // cspell:enable } std::vector SplitString(const std::string& s, char separator)