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.
This commit is contained in:
Ahson Khan 2024-09-13 19:35:40 -07:00 committed by GitHub
parent 382efbd7fd
commit e1afe4d7ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 151 additions and 9 deletions

View File

@ -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

View File

@ -60,6 +60,16 @@ namespace Azure { namespace Identity {
* for any tenant in which the application is installed.
*/
std::vector<std::string> 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<std::string> 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(

View File

@ -77,6 +77,38 @@ template <typename T> std::vector<uint8_t> 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<unsigned char>;
using UniquePrivateKey = Azure::Identity::_detail::UniquePrivateKey;
using PrivateKey = decltype(std::declval<UniquePrivateKey>().get());
@ -383,6 +415,7 @@ ClientCertificateCredential::ClientCertificateCredential(
std::string const& clientCertificatePath,
std::string const& authorityHost,
std::vector<std::string> 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<std::string::value_type>(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)
{
}

View File

@ -20,7 +20,8 @@ namespace {
enum CertFormat
{
RsaPkcs,
RsaRaw
RsaRaw,
RsaRawReverse
};
enum TestType
@ -46,12 +47,14 @@ class GetCredentialName : public ::testing::TestWithParam<CertFormat> {
TempCertFile m_certFile{GetParam()};
};
class GetToken : public ::testing::TestWithParam<std::tuple<TestType, CertFormat>> {
class GetToken : public ::testing::TestWithParam<std::tuple<TestType, CertFormat, bool>> {
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<ClientCertificateCredential>(
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<std::string> SplitString(const std::string& s, char separator)