├── src └── customer-key-store │ ├── Protocols │ ├── Decrypt.Response.json │ ├── Decrypt.Request.json │ └── PublicKey.Response.json │ ├── Models │ ├── Key.cs │ ├── KeyStore.cs │ ├── Authorizer.cs │ ├── extensions.cs │ ├── KeyAccessException.cs │ ├── DecryptedData.cs │ ├── EncryptedData.cs │ ├── KeyData.cs │ ├── EmailAuthorizer.cs │ ├── KeyManager.cs │ ├── TestKey.cs │ ├── PublicKey.cs │ ├── RoleAuthorizer.cs │ └── TestStore.cs │ ├── appsettings.Development.json │ ├── Properties │ └── launchSettings.json │ ├── Program.cs │ ├── customerkeystore.csproj │ ├── appsettings.json │ ├── CodeAnalysisRuleSet.ruleset │ ├── Controllers │ └── KeysController.cs │ ├── Startup.cs │ └── scripts │ └── key_store_tester.ps1 ├── CODE_OF_CONDUCT.md ├── LICENSE ├── SECURITY.md ├── README.md └── .gitignore /src/customer-key-store/Protocols/Decrypt.Response.json: -------------------------------------------------------------------------------- 1 | { 2 | "value":"LAD//yAAEAACEAAAC7xFqqyoJQGy7DEKv/vTWMrNA8bwZuA4JwXaXfnbF3Y=" 3 | } -------------------------------------------------------------------------------- /src/customer-key-store/Models/Key.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Models 4 | { 5 | public interface IKey 6 | { 7 | PublicKey GetPublicKey(); 8 | byte[] Decrypt(byte[] encryptedData); 9 | } 10 | } -------------------------------------------------------------------------------- /src/customer-key-store/Models/KeyStore.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Models 4 | { 5 | public interface IKeyStore 6 | { 7 | KeyStoreData GetActiveKey(string keyName); 8 | KeyStoreData GetKey(string keyName, string keyId); 9 | } 10 | } -------------------------------------------------------------------------------- /src/customer-key-store/Models/Authorizer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | 4 | namespace Microsoft.InformationProtection.Web.Models 5 | { 6 | using System.Security.Claims; 7 | public interface IAuthorizer 8 | { 9 | void CanUserAccessKey(ClaimsPrincipal user, KeyStoreData key); 10 | } 11 | } -------------------------------------------------------------------------------- /src/customer-key-store/Protocols/Decrypt.Request.json: -------------------------------------------------------------------------------- 1 | { 2 | "alg":"RSA-OAEP-256", 3 | "value":"rga6Ky1ddzkj0ujL7gLSSlO6fNByLrGdvPPp1Vh+KryzEZCzUIsbLRIvKUXzB+u6WuPl5JqJj0CZpc7bAFr9rpXaQJEkyPeg7xcfm17n4K2lIppUlBH6pfqVNwJnv+N5jbAmYh6kxV41VB4GJNVS7xhmYOX2ggH+OybQtNQTg6RNMMSDl91n2Tj+gws2Qx0jRGo0WksmRDGgTYEUn5ebl3Kog8jkchvVDDJWfPZ9AsFSigRxMTM3A2KlXfb7AgW9InILGgVZNeQxVKhD8fNHrGzuzN0CupLYgqUA/wtgGrhd9UihybeMbhDALIOonDHz/BL1xryLLzD+7cyd1gryqw==" 4 | } -------------------------------------------------------------------------------- /src/customer-key-store/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "AzureAd": { 3 | "Instance": "https://login.microsoftonline.com/", 4 | "ClientId": "[Client_id-of-web-api-eg-2ec40e65-ba09-4853-bcde-bcb60029e596]", 5 | "TenantId": "common" 6 | }, 7 | "Logging": { 8 | "LogLevel": { 9 | "Default": "Debug", 10 | "System": "Information", 11 | "Microsoft": "Information" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /src/customer-key-store/Models/extensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Models.Extensions 4 | { 5 | public static class ExceptionExtensions 6 | { 7 | public static void ThrowIfNull(this T argument, string name) 8 | { 9 | if(argument == null) 10 | { 11 | throw new System.ArgumentNullException(name); 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/customer-key-store/Models/KeyAccessException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace CustomerKeyStore.Models 4 | { 5 | using System; 6 | public class KeyAccessException : Exception 7 | { 8 | public KeyAccessException(string message) 9 | : base(message) 10 | { 11 | } 12 | 13 | public KeyAccessException(string message, Exception inner) 14 | : base(message, inner) 15 | { 16 | } 17 | 18 | private KeyAccessException() 19 | { 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/customer-key-store/Protocols/PublicKey.Response.json: -------------------------------------------------------------------------------- 1 | { 2 | "key":{ 3 | "kty":"RSA", 4 | "n":"wP4ir0aynve6Cpv3ZcBo5+HDue7OA6ogQetNkql1ptfKXilQ2N6x+wDTszcrJlb672l+ckUV5Gjn+ohhFUh0hx6B3rTNKVyxJiGq8S+MRXrTl0UGjWjFED7fYZ2nYZPigu1VHdm3HgBVZdeR8TMr1uIjDHxhWgen2utnTvacn5r8X079ImwpbhilrYBUvt9q42r/CxRp+axsMY3ozkGYsSZ/vXsgjSN0Nbn+9cwHi+XeE2PcjAOnaxUTKVcxjcZvRE+y2FcwgT+nVfJub4ZvRjz9lAbhdDNUS2ZrisAtHVRWJx1ArAMHH7OYg41LoA9+wmBoB04cEzi3JkJkqNCwtw==", 5 | "e":65537, 6 | "alg":"RS256", 7 | "kid":"https://localhost:5001/TestKey1/F2DBF8CB-7B06-4ABA-B93A-CACAD13CD7BB" 8 | }, 9 | "cache":{ 10 | "exp":"2020-11-21T21:15:55" 11 | } 12 | } -------------------------------------------------------------------------------- /src/customer-key-store/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:53456", 7 | "sslPort": 44373 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "aspnetapp": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/customer-key-store/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace CustomerKeyStore 4 | { 5 | using Microsoft.AspNetCore; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | public static class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | CreateWebHostBuilder(args).Build().Run(); 13 | } 14 | 15 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 16 | WebHost.CreateDefaultBuilder(args) 17 | .UseStartup() 18 | .ConfigureLogging((context, logging) => 19 | { 20 | logging.AddEventLog(eventLogSettings => 21 | { 22 | }); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/customer-key-store/Models/DecryptedData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Models 4 | { 5 | //This class implements the format of data returned from the /decrypt API 6 | //Changes in the returned format will break consuming clients 7 | //See src\customer-key-store\Protocols\Decrypt.Response.json 8 | public class DecryptedData 9 | { 10 | public DecryptedData(string value) 11 | { 12 | this.Value = value; 13 | } 14 | 15 | /// 16 | /// Gets the value 17 | /// 18 | /// 19 | /// The decrypted data in base 64 format 20 | /// Required. 21 | /// 22 | [Newtonsoft.Json.JsonProperty("value")] 23 | public string Value { get; private set; } 24 | } 25 | } -------------------------------------------------------------------------------- /src/customer-key-store/customerkeystore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | 5 | 6 | true 7 | AllEnabledByDefault 8 | $(MSBuildProjectDirectory)\CodeAnalysisRuleSet.ruleset 9 | $(BaseIntermediateOutputPath)\$(MSBuildThisFileName).xml 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /src/customer-key-store/Models/EncryptedData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Models 4 | { 5 | //This class implements the format of data accepted in the /decrypt API 6 | //Changes in the format will break consuming clients 7 | //See src\customer-key-store\Protocols\Decrypt.Request.json 8 | public class EncryptedData 9 | { 10 | /// 11 | /// Gets or sets the algorithm 12 | /// 13 | /// 14 | /// The algorithm used to encrypt the data. Currently only RSA-OAEP-256 is supported 15 | /// Required. 16 | /// Valid values: 17 | /// - RSA-OAEP-256 - RSA OAEP encoding with SHA-256 18 | /// 19 | [Newtonsoft.Json.JsonProperty("alg")] 20 | public string Algorithm { get; set; } 21 | 22 | /// 23 | /// Gets or sets the value 24 | /// 25 | /// 26 | /// The encrypted data in base 64 format 27 | /// Required. 28 | /// 29 | [Newtonsoft.Json.JsonProperty("value")] 30 | public string Value { get; set; } 31 | } 32 | } -------------------------------------------------------------------------------- /src/customer-key-store/Models/KeyData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Models 4 | { 5 | public class KeyStoreData 6 | { 7 | private IKey key; 8 | private string keyId; 9 | private string keyType; 10 | private string supportedAlgorithm; 11 | private IAuthorizer keyAuth; 12 | private int? expirationTimeInDays; 13 | 14 | public KeyStoreData(IKey key, string keyId, string keyType, string supportedAlgorithm, IAuthorizer keyAuth, int? expirationTimeInDays) 15 | { 16 | this.key = key; 17 | this.keyId = keyId; 18 | this.keyType = keyType; 19 | this.supportedAlgorithm = supportedAlgorithm; 20 | this.keyAuth = keyAuth; 21 | this.expirationTimeInDays = expirationTimeInDays; 22 | } 23 | 24 | public IKey Key { get { return key; } } 25 | public string KeyId { get { return keyId; } } 26 | public string KeyType { get { return keyType; } } 27 | public string SupportedAlgorithm { get { return supportedAlgorithm; } } 28 | public IAuthorizer KeyAuth { get { return keyAuth; } } 29 | public int? ExpirationTimeInDays { get { return expirationTimeInDays; } } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/customer-key-store/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AzureAd": { 3 | "Instance": "https://login.microsoftonline.com/", 4 | "ClientId": "[Client_id-of-web-api-eg-2ec40e65-ba09-4853-bcde-bcb60029e596]", 5 | "TenantId": "common", 6 | "Authority": "https://login.microsoftonline.com/common/v2.0", 7 | "TokenValidationParameters": { 8 | "ValidIssuers": [ 9 | "https://sts.windows.net//" 10 | ] 11 | } 12 | }, 13 | "Logging": { 14 | "LogLevel": { 15 | "Default": "Information" 16 | }, 17 | "EventLog": { 18 | "LogLevel": { 19 | "Default": "Information" 20 | } 21 | } 22 | }, 23 | "AllowedHosts": "*", 24 | "JwtAudience": "https://aadrm.com", 25 | "JwtAuthorization": "https://login.windows.net/common/oauth2/authorize", 26 | "RoleAuthorizer": { 27 | "LDAPPath": "If you use role authorization (AuthorizedRoles) then this is the LDAP path." 28 | }, 29 | "TestKeys": [ 30 | { 31 | "Name": "YourTestKeyName", 32 | "Id": "GUID", 33 | "AuthorizedRoles": ["On premises Active Directory groups that you want to have access to this key. If you provide a value for AuthorizedRoles, then remove the line that starts with AuthorizedEmailAddress."], 34 | "AuthorizedEmailAddress": ["Email addresses of users that have access to this key. If you provide a value for AuthorizedEmailAddress, then remove the line that starts with AuthorizedRoles."], 35 | "PublicPem" : "The public key in PEM format. Do not include the BEGIN and END lines", 36 | "PrivatePem": "The private key in PEM format. Do not include the BEGIN and END lines" 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/customer-key-store/CodeAnalysisRuleSet.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/customer-key-store/Models/EmailAuthorizer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Models 4 | { 5 | using System.Collections.Generic; 6 | using System.Security.Claims; 7 | 8 | using Microsoft.InformationProtection.Web.Models.Extensions; 9 | public class EmailAuthorizer : IAuthorizer 10 | { 11 | private const string EmailClaim = ClaimTypes.Email; 12 | private const string UpnClaim = ClaimTypes.Upn; 13 | private HashSet validEmails = new HashSet(System.StringComparer.OrdinalIgnoreCase); 14 | 15 | public void AddEmail(string email) 16 | { 17 | email.ThrowIfNull(nameof(email)); 18 | 19 | validEmails.Add(email.Trim()); 20 | } 21 | 22 | public void CanUserAccessKey(ClaimsPrincipal user, KeyStoreData key) 23 | { 24 | string email = null; 25 | 26 | user.ThrowIfNull(nameof(user)); 27 | 28 | foreach(var claim in user.Claims) 29 | { 30 | if(claim.Type == EmailClaim) 31 | { 32 | email = claim.Value; 33 | break; 34 | } 35 | else if(claim.Type == UpnClaim) 36 | { 37 | email = claim.Value; 38 | break; 39 | } 40 | } 41 | 42 | if(email == null) 43 | { 44 | throw new System.ArgumentException("The email or upn claim is required"); 45 | } 46 | 47 | if(!validEmails.Contains(email.Trim())) 48 | { 49 | throw new CustomerKeyStore.Models.KeyAccessException("User does not have access to the key"); 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/customer-key-store/Controllers/KeysController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Controllers 4 | { 5 | using System; 6 | using Microsoft.AspNetCore.Authentication.JwtBearer; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.AspNetCore.Http.Extensions; 9 | using Microsoft.AspNetCore.Mvc; 10 | using ippw = Microsoft.InformationProtection.Web.Models; 11 | //https://docs.microsoft.com/azure/active-directory/develop/scenario-protected-web-api-app-configuration 12 | public class KeysController : Controller 13 | { 14 | private readonly ippw.KeyManager keyManager; 15 | 16 | public KeysController(ippw.KeyManager keyManager) 17 | { 18 | this.keyManager = keyManager; 19 | } 20 | 21 | [HttpGet] 22 | public IActionResult GetKey(string keyName) 23 | { 24 | try 25 | { 26 | var publicKey = keyManager.GetPublicKey(GetRequestUri(Request), keyName); 27 | 28 | return Ok(publicKey); 29 | } 30 | catch(CustomerKeyStore.Models.KeyAccessException) 31 | { 32 | return StatusCode(403); 33 | } 34 | catch(ArgumentException e) 35 | { 36 | return BadRequest(e); 37 | } 38 | } 39 | 40 | [HttpPost] 41 | [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] 42 | public IActionResult Decrypt(string keyName, string keyId, [FromBody] ippw.EncryptedData encryptedData) 43 | { 44 | try 45 | { 46 | var decryptedData = keyManager.Decrypt(HttpContext.User, keyName, keyId, encryptedData); 47 | 48 | return Ok(decryptedData); 49 | } 50 | catch(CustomerKeyStore.Models.KeyAccessException) 51 | { 52 | return StatusCode(403); 53 | } 54 | catch(ArgumentException e) 55 | { 56 | return BadRequest(e); 57 | } 58 | } 59 | 60 | private static Uri GetRequestUri(AspNetCore.Http.HttpRequest request) 61 | { 62 | return new Uri(request.GetDisplayUrl()); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/customer-key-store/Models/KeyManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Models 4 | { 5 | using System; 6 | using System.Security.Claims; 7 | using Microsoft.InformationProtection.Web.Models.Extensions; 8 | using sg = System.Globalization; 9 | 10 | public class KeyManager 11 | { 12 | private readonly IKeyStore keyStore; 13 | 14 | public KeyManager(IKeyStore keyStore) 15 | { 16 | this.keyStore = keyStore; 17 | } 18 | 19 | public KeyData GetPublicKey(Uri requestUri, string keyName) 20 | { 21 | requestUri.ThrowIfNull(nameof(requestUri)); 22 | keyName.ThrowIfNull(nameof(keyName)); 23 | 24 | var key = keyStore.GetActiveKey(keyName); 25 | var publicKey = key.Key.GetPublicKey(); 26 | 27 | publicKey.KeyId = requestUri.GetLeftPart(UriPartial.Path) + "/" + key.KeyId; 28 | publicKey.KeyType = key.KeyType; 29 | publicKey.Algorithm = key.SupportedAlgorithm; 30 | PublicKeyCache cache = null; 31 | 32 | if(key.ExpirationTimeInDays.HasValue) 33 | { 34 | cache = new PublicKeyCache( 35 | DateTime.UtcNow.AddDays( 36 | key.ExpirationTimeInDays.Value).ToString("yyyy-MM-ddTHH:mm:ss", sg.CultureInfo.InvariantCulture)); 37 | } 38 | 39 | return new KeyData(publicKey, cache); 40 | } 41 | 42 | public DecryptedData Decrypt(ClaimsPrincipal user, string keyName, string keyId, EncryptedData encryptedData) 43 | { 44 | user.ThrowIfNull(nameof(user)); 45 | keyName.ThrowIfNull(nameof(keyName)); 46 | keyId.ThrowIfNull(nameof(keyId)); 47 | encryptedData.ThrowIfNull(nameof(encryptedData)); 48 | 49 | var keyData = keyStore.GetKey(keyName, keyId); 50 | 51 | keyData.KeyAuth.CanUserAccessKey(user, keyData); 52 | 53 | if (encryptedData.Algorithm != "RSA-OAEP-256") 54 | { 55 | throw new ArgumentException(encryptedData.Algorithm + " is not supported"); 56 | } 57 | 58 | var decryptedData = keyData.Key.Decrypt(Convert.FromBase64String(encryptedData.Value)); 59 | return new DecryptedData(Convert.ToBase64String(decryptedData)); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/customer-key-store/Models/TestKey.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Models 4 | { 5 | using System; 6 | using Microsoft.InformationProtection.Web.Models.Extensions; 7 | using sg = System.Globalization; 8 | public class TestKey : IKey 9 | { 10 | private string privateKeyPem; 11 | private string publicKeyPem; 12 | private PublicKey storedPublicKey = null; 13 | private System.Security.Cryptography.RSA cryptoEngine = null; 14 | 15 | public TestKey(string publicKey, string privateKey) 16 | { 17 | publicKeyPem = publicKey; 18 | privateKeyPem = privateKey; 19 | } 20 | 21 | public PublicKey GetPublicKey() 22 | { 23 | IntializeCrypto(); 24 | 25 | return storedPublicKey; 26 | } 27 | 28 | public byte[] Decrypt(byte[] encryptedData) 29 | { 30 | IntializeCrypto(); 31 | 32 | return cryptoEngine.Decrypt(encryptedData, System.Security.Cryptography.RSAEncryptionPadding.OaepSHA256); 33 | } 34 | 35 | private static uint ByteArrayToUInt(byte[] array) 36 | { 37 | uint retVal = 0; 38 | 39 | checked 40 | { 41 | if (BitConverter.IsLittleEndian) 42 | { 43 | for (int index = array.Length - 1; index >= 0; index--) 44 | { 45 | retVal = (retVal << 8) + array[index]; 46 | } 47 | } 48 | else 49 | { 50 | for (int index = 0; index < array.Length; index++) 51 | { 52 | retVal = (retVal << 8) + array[index]; 53 | } 54 | } 55 | } 56 | 57 | return retVal; 58 | } 59 | 60 | private void IntializeCrypto() 61 | { 62 | if(cryptoEngine == null) 63 | { 64 | var tempCryptoEngine = System.Security.Cryptography.RSA.Create(); 65 | byte[] privateKeyBytes = System.Convert.FromBase64String(privateKeyPem); 66 | tempCryptoEngine.ImportRSAPrivateKey(privateKeyBytes, out int bytesRead); 67 | 68 | var rsaKeyInfo = tempCryptoEngine.ExportParameters(false); 69 | var exponent = ByteArrayToUInt(rsaKeyInfo.Exponent); 70 | var modulus = Convert.ToBase64String(rsaKeyInfo.Modulus); 71 | storedPublicKey = new PublicKey(modulus, exponent); 72 | 73 | cryptoEngine = tempCryptoEngine; 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; you can download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [https://www.microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | - Type of issue, for example, buffer overflow, SQL injection, cross-site scripting, and so on. 22 | - Full paths of source files related to the manifestation of the issue. 23 | - The location of the affected source code (tag/branch/commit or direct URL). 24 | - Any special configuration required to reproduce the issue. 25 | - Step-by-step instructions to reproduce the issue. 26 | - Proof-of-concept or exploit code (if possible). 27 | - Impact of the issue, including how an attacker might exploit the issue. 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - csharp 5 | products: 6 | - dotnet 7 | description: "Key store for Double Key Encryption" 8 | urlFragment: "" 9 | --- 10 | 11 | # Source code repository for the Double Key Encryption (DKE) service for Microsoft 365 12 | 13 | Use this repository to download the DKE service. Once you download, install, and set up the DKE service, you keep your keys under your control. This way, your keys are never exposed to Microsoft. Follow the instructions at https://aka.ms/dke to get started. 14 | 15 | ## Warning 16 | 17 | IMPORTANT NOTICE: This project includes code for encryption libraries. You are responsible for complying with all applicable international and national laws that apply to this software, including the U.S. Export Administration Regulations, as well as end-user, end use and destination restrictions by U.S. and other governments. 18 | 19 | ## Contents 20 | 21 | | File or folder | Description | 22 | |----------------------|-----------------------------------------------------------------------------------------| 23 | | `src` | Contains the DKE service source code. | 24 | | `.gitignore` | Identifies what to ignore at commit time. | 25 | | `CODE_OF_CONDUCT.md` | Outlines expectations for participation in Microsoft-managed open source communities. | 26 | | `README.md` | This README file. | 27 | | `LICENSE` | The license for the DKE service software. | 28 | | `SECURITY.md` | Describes how to contact Microsoft to report a security vulnerability. | 29 | 30 | ## Contributing 31 | 32 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 33 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 34 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 35 | 36 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 37 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 38 | provided by the bot. You will only need to do this once across all repos using our CLA. 39 | 40 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 41 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 42 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 43 | 44 | [Coding Guidelines](https://blogs.msdn.microsoft.com/brada/2005/01/26/internal-coding-guidelines/) 45 | [Capitalization Conventions](https://docs.microsoft.com/dotnet/standard/design-guidelines/capitalization-conventions) 46 | -------------------------------------------------------------------------------- /src/customer-key-store/Startup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace CustomerKeyStore 4 | { 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Authentication.Cookies; 8 | using Microsoft.AspNetCore.Authentication.JwtBearer; 9 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Hosting; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.AspNetCore.Mvc; 14 | using Microsoft.Extensions.Configuration; 15 | using Microsoft.Extensions.DependencyInjection; 16 | using Microsoft.Extensions.Hosting; 17 | using ippw = Microsoft.InformationProtection.Web.Models; 18 | 19 | public class Startup 20 | { 21 | public Startup(IConfiguration configuration) 22 | { 23 | Configuration = configuration; 24 | } 25 | 26 | public IConfiguration Configuration { get; } 27 | 28 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 29 | public static void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IWebHostEnvironment env) 30 | { 31 | if (env.IsDevelopment()) 32 | { 33 | app.UseDeveloperExceptionPage(); 34 | } 35 | else 36 | { 37 | app.UseExceptionHandler("/Home/Error"); 38 | app.UseHsts(); 39 | } 40 | 41 | app.UseHttpsRedirection(); 42 | app.UseStaticFiles(); 43 | app.UseCookiePolicy(); 44 | 45 | app.UseMvc(routes => 46 | { 47 | routes.MapRoute( 48 | name: "DecryptRoute", 49 | template: "{keyName}/{keyId}/Decrypt", 50 | defaults: new { controller = "Keys", action = "Decrypt" }); 51 | 52 | routes.MapRoute( 53 | name: "GetKeyRoute", 54 | template: "{keyName}", 55 | defaults: new { controller = "Keys", action = "GetKey" }); 56 | }); 57 | } 58 | 59 | // This method gets called by the runtime. Use this method to add services to the container. 60 | public void ConfigureServices(IServiceCollection services) 61 | { 62 | services.Configure(options => 63 | { 64 | // This lambda determines whether user consent for non-essential cookies is needed for a given request. 65 | options.CheckConsentNeeded = context => true; 66 | options.MinimumSameSitePolicy = SameSiteMode.Strict; 67 | }); 68 | 69 | #if USE_TEST_KEYS 70 | #error !!!!!!!!!!!!!!!!!!!!!! Use of test keys is only supported for testing, DO NOT USE FOR PRODUCTION !!!!!!!!!!!!!!!!!!!!!!!!!!!!! 71 | services.AddSingleton(); 72 | #endif 73 | services.AddTransient(); 74 | services.AddMvc(options => options.EnableEndpointRouting = false); 75 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0); 76 | services.AddControllers().AddNewtonsoftJson(); 77 | 78 | services.AddAuthentication(sharedOptions => 79 | { 80 | sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 81 | sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; 82 | }) 83 | .AddJwtBearer(options => 84 | { 85 | Configuration.Bind("AzureAd", options); 86 | options.Audience = Configuration["JwtAudience"]; 87 | options.TokenValidationParameters.ValidateIssuerSigningKey = true; 88 | options.Challenge = "Bearer resource=\"" + Configuration["JwtAudience"] + "\", authorization=\"" + Configuration["JwtAuthorization"] + "\", realm=\"" + Configuration["JwtAudience"] + "\""; 89 | options.Events = new JwtBearerEvents 90 | { 91 | OnChallenge = context => 92 | { 93 | context.Response.Headers.Add("resource", options.Audience); 94 | context.Response.Headers.Add("authorization", Configuration["JwtAuthorization"]); 95 | 96 | return Task.CompletedTask; 97 | }, 98 | }; 99 | }); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/customer-key-store/Models/PublicKey.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Models 4 | { 5 | //The classes in this file implement the format of public key data returned for a key in json format 6 | //Changing the returned data can break consuming clients 7 | //See src\customer-key-store\Protocols\PublicKey.Response.json 8 | public class KeyData 9 | { 10 | public KeyData(PublicKey key, PublicKeyCache cache) 11 | { 12 | this.Key = key; 13 | this.Cache = cache; 14 | } 15 | 16 | /// 17 | /// Gets the key. 18 | /// 19 | /// 20 | /// The key information. 21 | /// Required. 22 | /// See PublicKey documentation below. 23 | /// 24 | [Newtonsoft.Json.JsonProperty("key")] 25 | public PublicKey Key { get; private set; } 26 | 27 | /// 28 | /// Gets the Cache. 29 | /// 30 | /// 31 | /// Details how the public key is cached locally 32 | /// Optional. 33 | /// If omitted then caching of the public key is disabled and encryption of content will always require a call to the key store 34 | /// 35 | [Newtonsoft.Json.JsonProperty("cache", NullValueHandling=Newtonsoft.Json.NullValueHandling.Ignore)] 36 | public PublicKeyCache Cache { get; private set; } 37 | } 38 | 39 | public class PublicKey 40 | { 41 | public PublicKey(string modulus, uint exponent) 42 | { 43 | this.KeyType = string.Empty; 44 | this.Modulus = modulus; 45 | this.Exponent = exponent; 46 | this.Algorithm = string.Empty; 47 | this.KeyId = string.Empty; 48 | } 49 | 50 | /// 51 | /// Gets or sets the key type. 52 | /// 53 | /// 54 | /// The key type. 55 | /// Required. 56 | /// The only supported value is 'RSA'. 57 | /// 58 | [Newtonsoft.Json.JsonProperty("kty")] 59 | public string KeyType { get; set; } 60 | 61 | /// 62 | /// Gets the public key modulus. 63 | /// 64 | /// 65 | /// The public key modulus in base 64 format. 66 | /// Required. 67 | /// 68 | [Newtonsoft.Json.JsonProperty("n")] 69 | public string Modulus { get; private set; } 70 | 71 | /// 72 | /// Gets the key exponent. 73 | /// 74 | /// 75 | /// The public key exponent in base 10 numeric format. 76 | /// Required. 77 | /// 78 | [Newtonsoft.Json.JsonProperty("e")] 79 | public uint Exponent { get; private set; } 80 | 81 | /// 82 | /// Gets or sets the algorithm. 83 | /// 84 | /// 85 | /// The supported algorithm that can be used to encrypt the data. 86 | /// Required. 87 | /// The only supported value is 'RS256'. 88 | /// 89 | [Newtonsoft.Json.JsonProperty("alg")] 90 | public string Algorithm { get; set; } 91 | 92 | /// 93 | /// Gets or sets the key id. 94 | /// 95 | /// 96 | /// The key Id. 97 | /// Required. 98 | /// A URI that identifies the key that is in use for the key name. The format is {URI}/{KeyName}/{KeyVersion-Guid} 99 | /// This URI will be called by the client to decrypt the data by appending /decrypt to the end. 100 | /// Ex. https://hostname/KeyName/2BE4E378-1317-4D64-AC44-D75f638F7B29 101 | /// 102 | [Newtonsoft.Json.JsonProperty("kid")] 103 | public string KeyId { get; set; } 104 | } 105 | 106 | public class PublicKeyCache 107 | { 108 | public PublicKeyCache(string expiration) 109 | { 110 | this.Expiration = expiration; 111 | } 112 | 113 | /// 114 | /// Gets the expiration. 115 | /// 116 | /// 117 | /// This member specifies the expiration date and time in format yyyy-MM-ddTHH:mm:ss - after which a locally stored public key will expire and require a call to 118 | /// the customer key store to obtain a newer version. 119 | /// Required. 120 | /// 121 | [Newtonsoft.Json.JsonProperty("exp")] 122 | public string Expiration { get; private set; } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/customer-key-store/Models/RoleAuthorizer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Models 4 | { 5 | using System.Collections.Generic; 6 | using System.DirectoryServices; 7 | using System.Security.Claims; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.InformationProtection.Web.Models.Extensions; 10 | public class RoleAuthorizer : IAuthorizer 11 | { 12 | private const string SidClaim = "onprem_sid"; 13 | private const string RoleProperty = "memberof"; 14 | 15 | private string ldapPath; 16 | private HashSet roles = new HashSet(); 17 | 18 | public RoleAuthorizer(IConfiguration configuration) 19 | { 20 | configuration.ThrowIfNull(nameof(configuration)); 21 | 22 | ldapPath = configuration["RoleAuthorizer:LDAPPath"]; 23 | } 24 | 25 | public static string GetRole(string memberOf) 26 | { 27 | memberOf.ThrowIfNull(nameof(memberOf)); 28 | return ParseCN(memberOf); 29 | } 30 | 31 | public void AddRole(string role) 32 | { 33 | roles.Add(role); 34 | } 35 | 36 | public void CanUserAccessKey(string sid) 37 | { 38 | sid.ThrowIfNull(nameof(sid)); 39 | 40 | using(DirectoryEntry entry = new DirectoryEntry("LDAP://" + ldapPath)) 41 | { 42 | using(DirectorySearcher dSearch = new DirectorySearcher(entry)) 43 | { 44 | dSearch.Filter = "(objectSid=" + sid + ")"; 45 | 46 | var result = dSearch.FindOne(); 47 | 48 | if(result == null) 49 | { 50 | throw new System.ArgumentException("User not found"); 51 | } 52 | 53 | var memberof = result.Properties[RoleProperty]; 54 | bool success = false; 55 | foreach(var member in memberof) 56 | { 57 | //Split out the first part of the role to the comma 58 | var role = GetRole(member.ToString()); 59 | if(!string.IsNullOrEmpty(role) && roles.Contains(role)) 60 | { 61 | success = true; 62 | break; 63 | } 64 | } 65 | 66 | if(!success) 67 | { 68 | throw new CustomerKeyStore.Models.KeyAccessException("User does not have access to the key"); 69 | } 70 | } 71 | } 72 | } 73 | 74 | public void CanUserAccessKey(ClaimsPrincipal user, KeyStoreData key) 75 | { 76 | user.ThrowIfNull(nameof(user)); 77 | 78 | string sid = null; 79 | 80 | foreach(var claim in user.Claims) 81 | { 82 | if(claim.Type == SidClaim) 83 | { 84 | sid = claim.Value; 85 | break; 86 | } 87 | } 88 | 89 | if(sid == null) 90 | { 91 | throw new System.ArgumentException(SidClaim + " claim not found"); 92 | } 93 | 94 | CanUserAccessKey(sid); 95 | } 96 | 97 | private static string ParseCN(string distinguishedName) 98 | { 99 | distinguishedName.ThrowIfNull(nameof(distinguishedName)); 100 | 101 | //The CN is terminated by a comma 102 | //A comma can be part of the CN if it is escaped by \ in which case continue searching, adding the comma without the \ 103 | int commaIndex = distinguishedName.IndexOf("CN=", System.StringComparison.InvariantCulture); 104 | 105 | if(commaIndex == -1) 106 | { 107 | return string.Empty; 108 | } 109 | 110 | System.Text.StringBuilder role = new System.Text.StringBuilder(); 111 | commaIndex += 3; //Skip over CN= 112 | do 113 | { 114 | var newCommaIndex = distinguishedName.IndexOf(",", commaIndex, System.StringComparison.InvariantCulture); 115 | 116 | if(newCommaIndex != -1) 117 | { 118 | if(distinguishedName[newCommaIndex - 1] == '\\') 119 | { 120 | //Found a delimited comma, skip over, add it to the resulting string, and continue searching 121 | role.Append(distinguishedName.Substring(commaIndex, newCommaIndex - commaIndex - 1)).Append(","); 122 | newCommaIndex++; 123 | } 124 | else 125 | { 126 | role.Append(distinguishedName.Substring(commaIndex, newCommaIndex - commaIndex)); 127 | break; 128 | } 129 | } 130 | else 131 | { 132 | role.Append(distinguishedName.Substring(commaIndex)); 133 | break; 134 | } 135 | 136 | commaIndex = newCommaIndex; 137 | } 138 | while(commaIndex > 0 && commaIndex < distinguishedName.Length); 139 | 140 | return role.ToString(); 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /src/customer-key-store/scripts/key_store_tester.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Corporation. 2 | # Licensed under the MIT License. 3 | 4 | function ParseAuthResponse 5 | { 6 | param( 7 | [parameter(Mandatory = $true)] 8 | [string] $AuthHeader 9 | ) 10 | if (($AuthHeader -match "^\s*bearer") -eq $False) { 11 | throw "Auth header does not contain bearer" 12 | } 13 | $authFields = $AuthHeader.Split(',') -replace "bearer", ""; 14 | $returnedAuthKeyValues = @{} 15 | foreach ($item in $authFields) { 16 | $matchedAuthLine = [regex]::Match($item, "\s*(?[^=]+)\=\s*\`"(?[^\`"]*)\`"") 17 | if ($matchedAuthLine.Success -eq $False) { 18 | throw "Invalid auth value: $item" 19 | } 20 | 21 | $returnedAuthKeyValues.Add($matchedAuthLine.Groups["name"].value.Trim(), $matchedAuthLine.Groups["value"].value.Trim()) 22 | } 23 | return $returnedAuthKeyValues; 24 | } 25 | 26 | if ($args.Count -ne 1) { 27 | Write-Host " 28 | cmdlet to test a double key customer key store 29 | Please enter the url with key name to test 30 | Ex. .\key_store_tester.ps1 https://mykeystoreurl.com/mykey1 31 | " 32 | exit 33 | } 34 | 35 | Write-Host "Validation request started: $($args[0])" 36 | 37 | if (-Not $args[0].Trim().StartsWith("https")) { 38 | Write-Host -ForegroundColor red "Validation failure: Url must be begin with 'https'" 39 | exit 40 | } 41 | 42 | try { 43 | $publicKeyResponse = Invoke-WebRequest -uri $args[0] -Method 'GET' 44 | } catch [System.Net.WebException] { 45 | Write-Host -ForegroundColor red "Validation failure: Unable to access the provided url $($_.Exception.Response.StatusDescription)" 46 | exit 47 | } catch { 48 | Write-Host -ForegroundColor red "Unexpected error $($_.Exception)" 49 | exit 50 | } 51 | 52 | Write-Host "Received content from url: $($publicKeyResponse.Content)" 53 | 54 | $jsonResponse = ConvertFrom-Json -InputObject $publicKeyResponse.Content 55 | 56 | if (-Not ([bool]($jsonResponse -match "key"))) { 57 | Write-Host -ForegroundColor red "Validation failure: Response does not contain the key" 58 | exit 59 | } 60 | 61 | $jsonKeyResponse = $jsonResponse.key 62 | 63 | if (-Not ([bool]($jsonKeyResponse -match "kty"))) { 64 | Write-Host -ForegroundColor red "Validation failure: Response does not contain the key type" 65 | exit 66 | } 67 | 68 | if (-Not ([bool]($jsonKeyResponse -match "n"))) { 69 | Write-Host -ForegroundColor red "Validation failure: Response does not contain the modulus" 70 | exit 71 | } 72 | 73 | if (-Not ([bool]($jsonKeyResponse -match "e"))) { 74 | Write-Host -ForegroundColor red "Validation failure: Response does not contain the exponent" 75 | exit 76 | } 77 | 78 | if (-Not ([bool]($jsonKeyResponse -match "alg"))) { 79 | Write-Host -ForegroundColor red "Validation failure: Response does not contain the algorithm" 80 | exit 81 | } 82 | 83 | if (-Not ([bool]($jsonKeyResponse -match "kid"))) { 84 | Write-Host -ForegroundColor red "Validation failure: Response does not contain the key id" 85 | exit 86 | } 87 | 88 | if ($jsonKeyResponse.kty -ne "RSA") { 89 | Write-Host -ForegroundColor red "Validation failure: Invalid key type" 90 | } 91 | 92 | if ($jsonKeyResponse.alg -ne "RS256") { 93 | Write-Host -ForegroundColor red "Validation failure: Invalid key algorithm" 94 | } 95 | 96 | Write-Host "Public key API validation complete" 97 | 98 | try { 99 | $decryptUrl = "$($jsonKeyResponse.kid)/decrypt" 100 | Write-Host "Attempting to access url: $($decryptUrl)" 101 | $decryptResponse = Invoke-WebRequest -uri $decryptUrl -Method 'POST' 102 | Write-Host -ForegroundColor red "Validation failure: Decryption unexpected response" 103 | exit 104 | } catch [System.Net.WebException] { 105 | # it is expected that the call will throw an exception because auth failed 106 | if($_.Exception.Response.StatusCode -ne 401) { 107 | Write-Host -ForegroundColor red "Validation failure: Decryption unexpected response - $($_.Exception.Response.StatusDescription)" 108 | exit 109 | } 110 | 111 | $headerCount = $_.Exception.Response.Headers.Count 112 | $authFound = $False 113 | for ($index = 0; $index -lt $headerCount; $index++) { 114 | if ( $_.Exception.Response.Headers.Keys[$index] -eq "WWW-Authenticate") { 115 | Write-Host "Found auth header - $($_.Exception.Response.Headers[$index])" 116 | $authFound = $True; 117 | 118 | $responeFields = ParseAuthResponse $_.Exception.Response.Headers[$index] 119 | if (-Not ($responeFields.ContainsKey("resource"))) { 120 | Write-Host -ForegroundColor red "resource auth field not found: $authResourceUri" 121 | exit 122 | } 123 | 124 | if (-Not ($responeFields.ContainsKey("authorization"))) { 125 | Write-Host -ForegroundColor red "authorization auth field not found: $authResourceUri" 126 | exit 127 | } 128 | 129 | if (-Not ($responeFields.ContainsKey("realm"))) { 130 | Write-Host -ForegroundColor red "realm auth field not found: $authResourceUri" 131 | exit 132 | } 133 | 134 | $resourceAuthField = $responeFields["resource"] 135 | 136 | if (-Not ($resourceAuthField.StartsWith("https://"))) { 137 | Write-Host -ForegroundColor red "Resource auth field ($($resourceAuthField)) must contains 'https://'. Ensure that the `"JwtAudience`" value in appsettings.json contains 'https://'" 138 | exit 139 | } 140 | 141 | $authResourceUri = [System.Uri]$resourceAuthField 142 | $decryptUrlUri = [System.Uri]$decryptUrl 143 | 144 | Write-Host "Validated parsed resource: $($authResourceUri)" 145 | 146 | if ($authResourceUri.host -ne $decryptUrlUri.host) { 147 | Write-Host -ForegroundColor red "Hostname mismatch between auth resource ($($authResourceUri.host)) and key url ($($decryptUrlUri.host)). Ensure that the `"JwtAudience`" value in appsettings.json matches the host where the key store has been published" 148 | exit 149 | } 150 | } 151 | } 152 | 153 | if($authFound -eq $False) { 154 | Write-Host -ForegroundColor red "Validation failure: WWW-Authenticate header not found" 155 | exit 156 | } 157 | } catch { 158 | Write-Host -ForegroundColor red "Unexpected error $($_.Exception)" 159 | exit 160 | } 161 | 162 | Write-Host -ForegroundColor green "Validation successful!" 163 | -------------------------------------------------------------------------------- /src/customer-key-store/Models/TestStore.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT License. 3 | namespace Microsoft.InformationProtection.Web.Models 4 | { 5 | using System; 6 | using System.Collections.Generic; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.InformationProtection.Web.Models.Extensions; 9 | using sg = System.Globalization; 10 | public class TestKeyStore : IKeyStore 11 | { 12 | private const string KeyType = "RSA"; 13 | private const string Algorithm = "RS256"; 14 | private Dictionary> keys = new Dictionary>(); 15 | private Dictionary activeKeys = new Dictionary(); 16 | 17 | public TestKeyStore(IConfiguration configuration) 18 | { 19 | configuration.ThrowIfNull(nameof(configuration)); 20 | 21 | var testKeysSection = configuration.GetSection("TestKeys"); 22 | IAuthorizer keyAuth = null; 23 | 24 | if(!testKeysSection.Exists()) 25 | { 26 | throw new System.ArgumentException("TestKeys section does not exist"); 27 | } 28 | 29 | foreach(var testKey in testKeysSection.GetChildren()) 30 | { 31 | List roles = new List(); 32 | var validRoles = testKey.GetSection("AuthorizedRoles"); 33 | var validEmails = testKey.GetSection("AuthorizedEmailAddress"); 34 | 35 | if(validRoles != null && validRoles.Exists() && 36 | validEmails != null && validEmails.Exists()) 37 | { 38 | throw new System.ArgumentException("Both role and email authorizers cannot be used on the same test key"); 39 | } 40 | 41 | if(validRoles != null && validRoles.Exists()) 42 | { 43 | RoleAuthorizer roleAuth = new RoleAuthorizer(configuration); 44 | keyAuth = roleAuth; 45 | foreach(var role in validRoles.GetChildren()) 46 | { 47 | roleAuth.AddRole(role.Value); 48 | } 49 | } 50 | else if(validEmails != null && validEmails.Exists()) 51 | { 52 | EmailAuthorizer emailAuth = new EmailAuthorizer(); 53 | keyAuth = emailAuth; 54 | foreach(var email in validEmails.GetChildren()) 55 | { 56 | emailAuth.AddEmail(email.Value); 57 | } 58 | } 59 | 60 | int? expirationTimeInDays = null; 61 | var cacheTime = testKey["CacheExpirationInDays"]; 62 | if(cacheTime != null) 63 | { 64 | expirationTimeInDays = Convert.ToInt32(cacheTime, sg.CultureInfo.InvariantCulture); 65 | } 66 | 67 | var name = testKey["Name"]; 68 | var id = testKey["Id"]; 69 | var publicPem = testKey["PublicPem"]; 70 | var privatePem = testKey["PrivatePem"]; 71 | 72 | if(name == null) 73 | { 74 | throw new System.ArgumentException("The key must have a name"); 75 | } 76 | 77 | if(id == null) 78 | { 79 | throw new System.ArgumentException("The key must have an id"); 80 | } 81 | 82 | if(publicPem == null) 83 | { 84 | throw new System.ArgumentException("The key must have a publicPem"); 85 | } 86 | 87 | if(privatePem == null) 88 | { 89 | throw new System.ArgumentException("The key must have a privatePem"); 90 | } 91 | 92 | CreateTestKey( 93 | name, 94 | id, 95 | publicPem, 96 | privatePem, 97 | KeyType, 98 | Algorithm, 99 | keyAuth, 100 | expirationTimeInDays); 101 | } 102 | } 103 | 104 | public KeyStoreData GetActiveKey(string keyName) 105 | { 106 | Dictionary keys; 107 | string activeKey; 108 | KeyStoreData foundKey; 109 | if(!this.keys.TryGetValue(keyName, out keys) || !activeKeys.TryGetValue(keyName, out activeKey) || 110 | !keys.TryGetValue(activeKey, out foundKey)) 111 | { 112 | throw new ArgumentException("Key " + keyName + " not found"); 113 | } 114 | 115 | return foundKey; 116 | } 117 | 118 | public KeyStoreData GetKey(string keyName, string keyId) 119 | { 120 | Dictionary keys; 121 | KeyStoreData foundKey; 122 | if(!this.keys.TryGetValue(keyName, out keys) || 123 | !keys.TryGetValue(keyId, out foundKey)) 124 | { 125 | throw new ArgumentException("Key " + keyName + "-" + keyId + " not found"); 126 | } 127 | 128 | return foundKey; 129 | } 130 | 131 | private void CreateTestKey( 132 | string keyName, 133 | string keyId, 134 | string publicKey, 135 | string privateKey, 136 | string keyType, 137 | string algorithm, 138 | IAuthorizer keyAuth, 139 | int? expirationTimeInDays) 140 | { 141 | keyAuth.ThrowIfNull(nameof(keyAuth)); 142 | 143 | if(!keys.ContainsKey(keyName)) 144 | { 145 | keys.Add(keyName, new Dictionary()); 146 | } 147 | 148 | keys[keyName][keyId] = new KeyStoreData( 149 | new TestKey(publicKey, privateKey), 150 | keyId, 151 | keyType, 152 | algorithm, 153 | keyAuth, 154 | expirationTimeInDays); 155 | //Multiple keys with the same name can be in the app settings, the first one for the current name is active, the rest have been rolled 156 | if(!activeKeys.ContainsKey(keyName)) 157 | { 158 | activeKeys[keyName] = keyId; 159 | } 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | --------------------------------------------------------------------------------