Skip to content

Commit 1d06bd5

Browse files
author
Matthew John Cheetham
authored
Add support for sending X5C when using a service principal with certificate for authentication (#1666)
When using a service principal with certificate authentication, every time the certificate is renewed, the new certificate needs to be uploaded to the service principal's AAD app registration in order for authentication to continue to work. However, a technology called "X5C" has made this unnecessary by allowing _any_ certificate, with a _specific_ subject, issued by a known, trusted, predetermined CA, to be used. For this to work, the AAD app registration's manifest needs to be updated to reflect the subject name, and during authentication, the request for "X5C" authentication needs to be sent along with the certificate's signature. This change enables that to take place.
2 parents 38df606 + ab05752 commit 1d06bd5

File tree

6 files changed

+70
-2
lines changed

6 files changed

+70
-2
lines changed

docs/azrepos-misp.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ Type|Git Configuration|Environment Variable
108108
-|-|-
109109
Client Secret|[`credential.azreposServicePrincipalSecret`][gcm-sp-secret-config]|[`GCM_AZREPOS_SP_SECRET`][gcm-sp-secret-env]
110110
Certificate|[`credential.azreposServicePrincipalCertificateThumbprint`][gcm-sp-cert-config]|[`GCM_AZREPOS_SP_CERT_THUMBPRINT`][gcm-sp-cert-env]
111+
Send X5C|[`credential.azreposServicePrincipalCertificateSendX5C`][gcm-sp-cert-x5c-config]|[`GCM_AZREPOS_SP_CERT_SEND_X5C`][gcm-sp-cert-x5c-env]
111112

112113
The value for these options should be the client secret or the thumbrint of the
113114
certificate that is associated with the Service Principal.
@@ -126,4 +127,6 @@ current user or the local machine.
126127
[gcm-sp-secret-config]: https://gh.io/gcm/config#credentialazreposserviceprincipalsecret
127128
[gcm-sp-secret-env]: https://gh.io/gcm/env#GCM_AZREPOS_SP_SECRET
128129
[gcm-sp-cert-config]: https://gh.io/gcm/config#credentialazreposserviceprincipalcertificatethumbprint
130+
[gcm-sp-cert-x5c-config]: https://gh.io/gcm/config#credentialazreposserviceprincipalcertificatesendx5c
129131
[gcm-sp-cert-env]: https://gh.io/gcm/env#GCM_AZREPOS_SP_CERT_THUMBPRINT
132+
[gcm-sp-cert-x5c-env]: https://gh.io/gcm/env#GCM_AZREPOS_SP_CERT_SEND_X5C

docs/configuration.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,7 @@ You must also set at least one authentication mechanism if you set this value:
858858

859859
- [credential.azreposServicePrincipalSecret][credential-azrepos-sp-secret]
860860
- [credential.azreposServicePrincipalCertificateThumbprint][credential-azrepos-sp-cert-thumbprint]
861+
- [credential.azreposServicePrincipalCertificateSendX5C][credential-azrepos-sp-cert-x5c]
861862

862863
For more information about service principals, see the Azure DevOps
863864
[documentation][azrepos-sp-mid].
@@ -904,6 +905,25 @@ git config --global credential.azreposServicePrincipalCertificateThumbprint "9b6
904905

905906
---
906907

908+
### credential.azreposServicePrincipalCertificateSendX5C
909+
910+
When using a certificate for [service principal][service-principal] authentication, this configuration
911+
specifies whether the X5C claim should be should be sent to the STS. Sending the x5c
912+
enables application developers to achieve easy certificate rollover in Azure AD:
913+
this method will send the public certificate to Azure AD along with the token request,
914+
so that Azure AD can use it to validate the subject name based on a trusted issuer
915+
policy. This saves the application admin from the need to explicitly manage the
916+
certificate rollover. For details see [https://aka.ms/msal-net-sni](https://aka.ms/msal-net-sni).
917+
918+
#### Example
919+
920+
```shell
921+
git config --global credential.azreposServicePrincipalCertificateSendX5C true
922+
```
923+
**Also see: [GCM_AZREPOS_SP_CERT_SEND_X5C][gcm-azrepos-sp-cert-x5c]**
924+
925+
---
926+
907927
### trace2.normalTarget
908928

909929
Turns on Trace2 Normal Format tracing - see [Git's Trace2 Normal Format
@@ -1034,6 +1054,8 @@ Defaults to disabled.
10341054
[credential-azrepos-sp]: #credentialazreposserviceprincipal
10351055
[credential-azrepos-sp-secret]: #credentialazreposserviceprincipalsecret
10361056
[credential-azrepos-sp-cert-thumbprint]: #credentialazreposserviceprincipalcertificatethumbprint
1057+
[credential-azrepos-sp-cert-x5c]: #credentialazreposserviceprincipalcertificatesendx5c
10371058
[gcm-azrepos-service-principal]: environment.md#GCM_AZREPOS_SERVICE_PRINCIPAL
10381059
[gcm-azrepos-sp-secret]: environment.md#GCM_AZREPOS_SP_SECRET
10391060
[gcm-azrepos-sp-cert-thumbprint]: environment.md#GCM_AZREPOS_SP_CERT_THUMBPRINT
1061+
[gcm-azrepos-sp-cert-x5c]: environment.md#GCM_AZREPOS_SP_CERT_SEND_X5C

docs/environment.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,32 @@ export GCM_AZREPOS_SP_CERT_THUMBPRINT="9b6555292e4ea21cbc2ebd23e66e2f91ebbe92dc"
10391039

10401040
---
10411041

1042+
### GCM_AZREPOS_SP_CERT_SEND_X5C
1043+
1044+
When using a certificate for service principal authentication, this configuration
1045+
specifies whether the X5C claim should be should be sent to the STS. Sending the x5c
1046+
enables application developers to achieve easy certificate rollover in Azure AD:
1047+
this method will send the public certificate to Azure AD along with the token request,
1048+
so that Azure AD can use it to validate the subject name based on a trusted issuer
1049+
policy. This saves the application admin from the need to explicitly manage the
1050+
certificate rollover. For details see [https://aka.ms/msal-net-sni](https://aka.ms/msal-net-sni).
1051+
1052+
#### Windows
1053+
1054+
```batch
1055+
SET GCM_AZREPOS_SP_CERT_SEND_X5C="true"
1056+
```
1057+
1058+
#### macOS/Linux
1059+
1060+
```bash
1061+
export GCM_AZREPOS_SP_CERT_SEND_X5C="true"
1062+
```
1063+
1064+
**Also see: [credential.azreposServicePrincipalCertificateSendX5C][credential-azrepos-sp-cert-x5c]**
1065+
1066+
---
1067+
10421068
### GIT_TRACE2
10431069

10441070
Turns on Trace2 Normal Format tracing - see [Git's Trace2 Normal Format
@@ -1184,6 +1210,8 @@ Defaults to disabled.
11841210
[gcm-azrepos-sp]: #gcm_azrepos_service_principal
11851211
[gcm-azrepos-sp-secret]: #gcm_azrepos_sp_secret
11861212
[gcm-azrepos-sp-cert-thumbprint]: #gcm_azrepos_sp_cert_thumbprint
1213+
[gcm-azrepos-sp-cert-x5c]: #gcm_azrepos_sp_cert_send_x5c
11871214
[credential-azrepos-sp]: configuration.md#credentialazreposserviceprincipal
11881215
[credential-azrepos-sp-secret]: configuration.md#credentialazreposserviceprincipalsecret
11891216
[credential-azrepos-sp-cert-thumbprint]: configuration.md#credentialazreposserviceprincipalcertificatethumbprint
1217+
[credential-azrepos-sp-cert-x5c]: configuration.md#credentialazreposserviceprincipalcertificatesendx5c

src/shared/Core/Authentication/MicrosoftAuthentication.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ public class ServicePrincipalIdentity
9292
/// If both <see cref="Certificate"/> and <see cref="ClientSecret"/> are set, the certificate will be used.
9393
/// </remarks>
9494
public string ClientSecret { get; set; }
95+
96+
/// <summary>
97+
/// Whether the authentication should send X5C
98+
/// </summary>
99+
public bool SendX5C { get; set; }
95100
}
96101

97102
public interface IMicrosoftAuthenticationResult
@@ -269,12 +274,14 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenForServicePrincipalAsy
269274

270275
try
271276
{
272-
AuthenticationResult result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
277+
Context.Trace.WriteLine($"Sending with X5C: '{sp.SendX5C}'.");
278+
AuthenticationResult result = await app.AcquireTokenForClient(scopes).WithSendX5C(sp.SendX5C).ExecuteAsync();;
279+
273280
return new MsalResult(result);
274281
}
275282
catch (Exception ex)
276283
{
277-
Context.Trace.WriteLine($"Failed to acquire token for service principal '{sp.TenantId}/{sp.TenantId}'.");
284+
Context.Trace.WriteLine($"Failed to acquire token for service principal '{sp.TenantId}/{sp.Id}'.");
278285
Context.Trace.WriteException(ex);
279286
throw;
280287
}

src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public static class EnvironmentVariables
4444
public const string ServicePrincipalId = "GCM_AZREPOS_SERVICE_PRINCIPAL";
4545
public const string ServicePrincipalSecret = "GCM_AZREPOS_SP_SECRET";
4646
public const string ServicePrincipalCertificateThumbprint = "GCM_AZREPOS_SP_CERT_THUMBPRINT";
47+
public const string ServicePrincipalCertificateSendX5C = "GCM_AZREPOS_SP_CERT_SEND_X5C";
4748
public const string ManagedIdentity = "GCM_AZREPOS_MANAGEDIDENTITY";
4849
}
4950

@@ -59,6 +60,7 @@ public static class Credential
5960
public const string ServicePrincipal = "azreposServicePrincipal";
6061
public const string ServicePrincipalSecret = "azreposServicePrincipalSecret";
6162
public const string ServicePrincipalCertificateThumbprint = "azreposServicePrincipalCertificateThumbprint";
63+
public const string ServicePrincipalCertificateSendX5C = "azreposServicePrincipalCertificateSendX5C";
6264
public const string ManagedIdentity = "azreposManagedIdentity";
6365
}
6466
}

src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,12 @@ private bool UseServicePrincipal(out ServicePrincipalIdentity sp)
549549

550550
if (hasCertThumbprint)
551551
{
552+
sp.SendX5C = _context.Settings.TryGetSetting(
553+
AzureDevOpsConstants.EnvironmentVariables.ServicePrincipalCertificateSendX5C,
554+
Constants.GitConfiguration.Credential.SectionName,
555+
AzureDevOpsConstants.GitConfiguration.Credential.ServicePrincipalCertificateSendX5C,
556+
out string certHasX5CStr) && certHasX5CStr.ToBooleanyOrDefault(false);
557+
552558
X509Certificate2 cert = X509Utils.GetCertificateByThumbprint(certThumbprint);
553559
if (cert is null)
554560
{

0 commit comments

Comments
 (0)