Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create certificate support proposal.md #176

Closed
wants to merge 4 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
370 changes: 370 additions & 0 deletions docs/certificate support proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,370 @@
# Why?

## Client Certificates
Web apps and Web APIs are confidential client applications.

They can prove their identity to Azure AD by 3 means:
- client secrets
- client certificates
- client assertions
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you want to integrate with Wilson and expose configuring a SigningCredential here? I think this is similar, if not better, than client assertions option and would open up more scenarios (not that these 2 are mutually exclusive)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if SigningCredential also works with MSI?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not as far as I know, but @GeoK would know best

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SigningCredentials works with any SecurityKey, including ManagedKeyVaultSecurityKey that supports MSI.
SigningCredentials object has to be constructed with a KeyVaultCryptoProvider as CustomCryptoProvider, but that can be simplified by introducing a new class, e.g. ManagedKeyVaultSigningCredentials.

This would be a current setup:

var sc = new SigningCredentials(new ManagedKeyVaultSecurityKey([keyId]), [alg])
{
    CryptoProviderFactory = new CryptoProviderFactory
    {
        CustomCryptoProvider = new KeyVaultCryptoProvider()
    }
}

Copy link
Collaborator

@jmprieur jmprieur May 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @GeoK :

  • Does it use the new Azure SDK TokenCredential?
  • Is MSI working with the developer credentials in the shared token cache (between VS, Azure Rm PowerShell and Azure CLI) ?

also do customers need to provide the algorithm? do the understand what this means? (I don't)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with TokenCredential. ManagedKeyVaultSecurityKey leverages Microsoft.Azure.KeyVault and Microsoft.Azure.Services.AppAuthentication. Would these libraries have the business logic that you are looking for?

SigningCredentials is usually used in token signing scenarios where algorithm needs to be specified.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GeoK : I believe that the new Azure SDK uses Azure.Security.KeyVault (and Azure.Identity). Underneath, it uses MSAL.NET.


Today, Microsoft.Identity.Web enables developers to provide client secrets.
In addition to Client secret, we'd want Microsoft.Identity.Web to support client certificates. The constraints are the following:
- enable several ways of getting the certificate. You'd provide a description on how to get the certificate.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the developers programatically configure an X509Certificate object?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's a good idea;

- from the certificate store (Windows) and a thumbprint ("440A5BE6C4BE2FF02A0ADBED1AAA43D6CF12E269")
- from the certificate store (Windows) and a distinguished name ("CN=TestCert")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- from a path on the disk (probably only for debugging locally)
- directly from a base64 representation of the certificate
- from a KeyVault address.
- getting the certificate just in time, rather than paying the startup cost. For instance for a web app that signs in a user, do not load the certificate until an access token is needed to call a Web API.
- when the certificate is stored in KeyVault, leverage Managed identity (probably though the Azure SDK for .NET)
- help you rotating your certificates but letting you provide several (2) certificates
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How to you envision this scenario? I see that you provided support for configuring N certificates, but this scenario is not described. I'm wondering if an array of certificates is what is needed here? Perhaps 2 well known certificates should be used instead (e.g. "current_certificate", "next_certificate") ?


## Decrypt certificates

It's a different topic, but still touching on certificates: Web APIs can request token encryption (for privacy reasons). This is even compulsory for 1P Web APIs that access MSA identities. We also want to let you pass decryption certificates with the same flexibility as for client certificates.

## Proposal

The current proposal is to have the following options to specify both client certificates and decrypt certificates from the configuration file.

```Json
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "msidentitysamplestesting.onmicrosoft.com",
"TenantId": "7f58f645-c190-4ce5-9de4-e2b7acd2a6ab",
"ClientId": "86699d80-dd21-476a-bcd1-7c1a3d471f75",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath ": "/signout-callback-oidc",

// To call an API
"ClientSecret": "[Copy the client secret added to the app from the Azure portal]",

// Exclusive with "ClientSecret"
"ClientCertificates": [
{
"CertificateFromThumbprint": {
"StoreLocation": "CurrentUser", // Optional, default = CurrentUser
"StoreName": "My", // Optional, default = My
"certificateThumbprint": "440A5BE6C4BE2FF02A0ADBED1AAA43D6CF12E269"
}
},
{
"CertificateFromSubjectDistinguishedName": {
"StoreLocation": "CurrentUser", // Optional, default = CurrentUser
"StoreName": "My", // Optional, default = My
"certificateSubjectDistinguishedName": "CN=TestCert"
}
},
{
"CertificateFromPath": "C:\Users\myname\Documents\TestJWE.pfx",
"CertificatePassword": "TestJWE"
},
{
"CertificateFromBase64Encoded": "Base64Encoded"
},
{
"CertificateFromKeyVault": "https://vaultname.vault.azure.net/certificates/certificateid/3fb1c62f74b844b0a2d9f1a3d289648d"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the naming here is a bit misleading if you expect to use managed identity here? AFAIK, you can only use this if the code is running in an Azure VM or an Azure Function?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually no: Managed identities also work for App services, plenty of Azure services (increasing) and even locally on the dev box (using the shared token cache)

}
],
"TokenDecryptionCertificates": [
{
// Same possibilities as for the client certificates
}
],

"SingletonTokenAcquisition" : true

},
"TodoList": {
/*
TodoListScope is the scope of the Web API you want to call. This can be: "api://a4c2469b-cf84-4145-8f5f-cb7bacf814bc/access_as_user",
- a scope for a V2 application (for instance api://b3682cc7-8b30-4bd2-aaba-080c6bf0fd31/access_as_user)
- a scope corresponding to a V1 application (for instance <GUID>/user_impersonation, where <GUID> is the
clientId of a V1 application, created in the https://portal.azure.com portal.
*/
"TodoListScope": "api://a4c2469b-cf84-4145-8f5f-cb7bacf814bc/access_as_user",
"TodoListBaseAddress": "https://localhost:44351"

},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

```

## Todo:
- provide feedback on this proposal. Would we rather want to have a service to get certificates?
- Come-up with MicrosoftIdentityOptions to read this
- Read the certificate from its description.
- Apply the certificate to ConfidentialClientApplication.

See also [Sample code to load certiticates](https://github.com/AzureAD/microsoft-identity-web/wiki/Spec-certificates#sample-code-to-load-certificates) - which is not necessarily what we want to do, but gives an idea on manipulate the certs?

See also
- https://github.com/AzureAD/microsoft-identity-web/issues/72
- https://github.com/AzureAD/microsoft-identity-web/issues/12


## Proposal/Principles
Initially let's reduce the problem by saying that:
- we want to support both ClientCertificates and EncryptionCertificate collections in the configuration (in MicrosoftIdentityOptions)
- however initially we'll only consider the **first** certificate of each collection. We would not handle the rotation of certificate yet, as this would need to be pushed to MSAL.NET (for the ClientCertificate), and to Wilson (for the EncyrptionCertificate). We just add the collection so that we don't break the public API later.
- we would start by only supporting certificates obtained from KeyVault, as this is the use case that most customers are after. But we'd propose a design that would work with other source of certificates (local file, base64 encoded are the ones I've seen the most)

Proposed sequence:
1. Experiment with the KeyVault SDK part of the Azure SDK. This has the advantage of getting the secrets from KeyVault both locally, and when the Web app or Web API is deployed, as it leverages Managed identity. See https://docs.microsoft.com/en-us/azure/key-vault/general/tutorial-net-create-vault-azure-web-app#update-the-code (this one is for client secrets, we need to find the equivalent with certificates, for instance by looking at https://docs.microsoft.com/en-us/dotnet/api/overview/azure/security.keyvault.certificates-readme?view=azure-dotnet#retrieve-a-certificate). We could use, for the client the DefaultAzureCredentials (which also work well in the dev environment)

This means we would now leverage the following NuGet packages: **Azure.Identity** and **Azure.Security.KeyVault.Certificates**. This seems fine but we need to check if there is a negative impact.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As part of the experiment, we should also understand if there is significant logic related to MSI in those assemblies. If not, perhaps we can avoid the dependencies.


1. Create a new class `CertificateDescription` which would direct Microsoft.Identity.Web on how to get the certificate. It could be:
- SourceType: an enumeration (KeyVault, Base64Encoded, FromPath, FromStoreWithThumbprint, FromStoreWithDistinguishedName,)
- Container a string which would contain the container (for instance the keyVaultUrl in the case of the enum being KeyVault, or the Store Key in the case of the Windows cert Store [CurrentUser/My], the path in the case of a certificate from its path)
- ReferenceOrValue: a string which would contain the secret identifier or value. This would be the name of the certificate in the case of keyvault, the password in the case of a path on disk, the base64 encoded value in the case of Base64 encoded.
1. Modify the `MicrosoftIdentityOptions` to add the 2 collections of `CertificateDescription` `ClientCertificates` and `EncryptionCertificate`
2. Add a new class to load the certificates as a function of their type. We probably want to have a kind of base class (CertificateLoader), and derived classes for each SourceType. We'd start only by the KeyVault one.

Let's start simple, increment based on feedback

Possible design to discuss:

```CSharp
/// <summary>
/// Source for a certificate.
/// </summary>
public enum CertificateSource
{
/// <summary>
/// KeyVault
/// </summary>
KeyVault,

/// <summary>
/// Base 64 encoded directly in the configuration.
/// </summary>
Base64Encoded,

/// <summary>
/// Local path on disk
/// </summary>
Path,

/// <summary>
/// From the certificate store, described by its thumprint.
/// </summary>
StoreWithThumbprint,

/// <summary>
/// From the certificate store, described by its Distinguished name.
/// </summary>
StoreWithDistinguihedName,
}

/// <summary>
/// Description of a certificate.
/// </summary>
public class CertificateDescription
{
/// <summary>
/// Creates a certificate description from a certificate (by code).
/// </summary>
/// <param name="certificate2">Certificate.</param>
/// <returns>A certificate description.</returns>
public static CertificateDescription FromCertificate(X509Certificate2 certificate2)
{
return new CertificateDescription
{
Certificate = certificate2,
};
}

/// <summary>
/// Creates a Certificate Description from KeyVault.
/// </summary>
/// <param name="keyVaultUrl"></param>
/// <param name="certificateName"></param>
/// <param name="version"></param>
/// <returns>A certificate description.</returns>
public static CertificateDescription FromKeyVault(string keyVaultUrl, string certificateName, string version)
{
return new CertificateDescription
{
SourceType = CertificateSource.KeyVault,
Container = keyVaultUrl,
ReferenceOrValue = certificateName,
};
// todo support values?
}

/// <summary>
/// Create a certificate description from a base 64 encoded value.
/// </summary>
/// <param name="base64EncodedValue">base 64 encoded value.</param>
/// <returns>A certificate description.</returns>
public static CertificateDescription FromBase64Encoded(string base64EncodedValue)
{
return new CertificateDescription
{
SourceType = CertificateSource.Base64Encoded,
Container = string.Empty,
ReferenceOrValue = base64EncodedValue,
};
}

/// <summary>
/// Create a certificate description from path on disk.
/// </summary>
/// <param name="path">Path were to find the certificate file.</param>
/// <param name="password">certificate password.</param>
/// <returns>A certificate description.</returns>
public static CertificateDescription FromPath(string path, string password = null)
{
return new CertificateDescription
{
SourceType = CertificateSource.Path,
Container = path,
ReferenceOrValue = password,
};
}

// Todo: do the other ones

/// <summary>
/// Type of the source of the certificate.
/// </summary>
public CertificateSource SourceType { get; private set; }

/// <summary>
/// Container in which to find the certificate.
/// <list type="bullet">
/// <item>If <see cref="SourceType"/> equals <see cref="CertificateSource.KeyVault"/>, then
/// the container is the KeyVault base URL</item>
/// <item>If <see cref="SourceType"/> equals <see cref="CertificateSource.Base64Encoded"/>, then
/// this value is not used</item>
/// <item>If <see cref="SourceType"/> equals <see cref="CertificateSource.Path"/>, then
/// this value is the path on disk where to find the certificate</item>
/// <item>If <see cref="SourceType"/> equals <see cref="CertificateSource.StoreWithDistinguihedName"/>,
/// or <see cref="CertificateSource.StoreWithThumbprint"/>, then
/// this value is the path to the certificate in the cert store, for instance <c>CurrentUser/My</c></item>
/// </list>
/// </summary>
public string Container { get; private set; }

/// <summary>
/// Reference to the certificate or value.
/// </summary>
/// <list type="bullet">
/// <item>If <see cref="SourceType"/> equals <see cref="CertificateSource.KeyVault"/>, then
/// the reference is the name of the certificate in KeyVault (maybe the version?)</item>
/// <item>If <see cref="SourceType"/> equals <see cref="CertificateSource.Base64Encoded"/>, then
/// this value is the base 64 encoded certificate itself</item>
/// <item>If <see cref="SourceType"/> equals <see cref="CertificateSource.Path"/>, then
/// this value is the password to access the certificate (if needed)</item>
/// <item>If <see cref="SourceType"/> equals <see cref="CertificateSource.StoreWithDistinguihedName"/>,
/// this value is the path to the certificate in the cert store, for instance <c>CurrentUser/My</c></item>
/// <item>If <see cref="SourceType"/> equals <see cref="CertificateSource.StoreWithThumbprint"/>,
/// this value is the path to the certificate in the cert store, for instance <c>CurrentUser/My</c></item>
/// </list>
public string ReferenceOrValue { get; private set; }

/// <summary>
/// The certificate, either provided directly in code by the
/// or loaded from the description.
/// </summary>
public X509Certificate2 Certificate { get; internal set; }
}

public class MicrosoftIdentityOptions : OpenIdConnectOptions
{
// Other properties here

/// <summary>
/// Description of the certificates used to prove the identity of the Web app or Web API.
/// </summary>
public CertificateDescription[] ClientCertificates { get; set; }

/// <summary>
/// Description of the certificates used to decrypt an encrypted token in a Web API.
/// </summary>
public CertificateDescription[] DecryptCertificates { get; set; }
}

```

Then `TokenAcquisition` will require a `ICertificateLoader` (by dependency injection)

```CSharp
/// <summary>
/// Interface to implement load a certificate.
/// </summary>
public interface ICertificateLoader
{
/// <summary>
/// Load the certificate from the description if needed.
/// </summary>
/// <param name="certificateDescription">Description of the certificate.</param>
void LoadIfNeeded(CertificateDescription certificateDescription);
}
```

We would propose a default implementation for this interface

```CSharp
/// <summary>
/// Certificate Loader.
/// </summary>
public class DefaultCertificateLoader : ICertificateLoader
{
/// <summary>
/// Load the certificate from the description if needed.
/// </summary>
/// <param name="certificateDescription">Description of the certificate.</param>
public void LoadIfNeeded(CertificateDescription certificateDescription)
{
if (certificateDescription.Certificate == null)
{
switch (certificateDescription.SourceType)
{
case CertificateSource.KeyVault:
certificateDescription.Certificate = LoadFromKeyVault(certificateDescription.Container, certificateDescription.ReferenceOrValue);
break;
case CertificateSource.Base64Encoded:
certificateDescription.Certificate = LoadFromBase64Encoded(certificateDescription.ReferenceOrValue);
break;
case CertificateSource.Path:
// TODO
break;
case CertificateSource.StoreWithThumbprint:
// TODO
break;
case CertificateSource.StoreWithDistinguihedName:
// TODO
break;
default:
break;
}
}
}

private static X509Certificate2 LoadFromBase64Encoded(string certificateBase64)
{
byte[] decoded = Convert.FromBase64String(certificateBase64);
X509Certificate2 cert = new X509Certificate2(decoded);
return cert;
}

private static X509Certificate2 LoadFromKeyVault(string keyVaultUrl, string certificateName)
{
throw new NotImplementedException();
}
}
```

See also https://github.com/AzureAD/microsoft-identity-web/wiki/Spec-certificates#sample-code-to-load-certificates for possible implementations