From cd4dcc88803dee8f06097c63cd3f7520bd79db0b Mon Sep 17 00:00:00 2001 From: Omer Levi Hevroni Date: Tue, 8 Jan 2019 14:44:16 +0200 Subject: [PATCH] [WIP] Add support for GCP KMS (#66) * encryption working * now deceryption is working + add support for protection level * added tests for google KMS * added support for KMS in the API * push images temporary from branch * fix image tag --- ci/codefresh.yml | 12 +-- src/decrypt-api/Startup.cs | 40 +++++++- .../Controllers/EncryptController.cs | 3 - src/encrypt-api/Startup.cs | 40 +++++++- src/key-managment/GoogleCloudKeyManagment.cs | 94 +++++++++++++++++++ src/key-managment/key-managment.csproj | 1 + tests/integration/GoogleCloudKeyManagment.cs | 71 ++++++++++++++ tests/integration/integration.csproj | 1 + 8 files changed, 247 insertions(+), 15 deletions(-) create mode 100644 src/key-managment/GoogleCloudKeyManagment.cs create mode 100644 tests/integration/GoogleCloudKeyManagment.cs diff --git a/ci/codefresh.yml b/ci/codefresh.yml index 9856094dc..c40786421 100644 --- a/ci/codefresh.yml +++ b/ci/codefresh.yml @@ -67,24 +67,16 @@ steps: title: Push Encryptor Api - lastest candidate: ${{BuildingEncryptorDockerImage}} tags: - - encryptor-latest + - encryptor-${{CF_BRANCH_TAG_NORMALIZED}} credentials: username: ${{DOCKERHUB_REGISTRY_USERNAME}} password: ${{DOCKERHUB_REGISTRY_PASSWORD}} - when: - branch: - only: - - master PushDecryptorImage: type: push title: Push Decryptor Api - lastest - when: - branch: - only: - - master candidate: ${{BuildingDecryptorDockerImage}} tags: - - decryptor-latest + - decryptor-${{CF_BRANCH_TAG_NORMALIZED}} credentials: username: ${{DOCKERHUB_REGISTRY_USERNAME}} password: ${{DOCKERHUB_REGISTRY_PASSWORD}} \ No newline at end of file diff --git a/src/decrypt-api/Startup.cs b/src/decrypt-api/Startup.cs index ade23ac5d..ac305eaf9 100644 --- a/src/decrypt-api/Startup.cs +++ b/src/decrypt-api/Startup.cs @@ -16,7 +16,11 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel; using Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption; - +using Google.Apis.Auth.OAuth2; +using Google.Apis.CloudKMS.v1; +using Google.Apis.Services; +using System.IO; + namespace Kamus { public class Startup { @@ -67,6 +71,8 @@ public void ConfigureServices (IServiceCollection services) { var provider = Configuration.GetValue("KeyManagement:Provider"); switch (provider) { + case "GoogleKms": + return GetGoogleCloudKeyManagment(); case "AzureKeyVault": return new EnvelopeEncryptionDecorator( new AzureKeyVaultKeyManagement(s.GetService(), Configuration), @@ -139,6 +145,38 @@ public void Configure (IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication(); app.UseMvc (); + } + + private IKeyManagement GetGoogleCloudKeyManagment() + { + var location = Configuration.GetValue("KeyManagement:GoogleKms:Location"); + var keyRingName = Configuration.GetValue("KeyManagement:GoogleKms:KeyRingName"); + var protectionLevel = Configuration.GetValue("KeyManagement:GoogleKms:ProtectionLevel"); + var credentialsPath = Configuration.GetValue("KeyManagement:GoogleKms:CredentialsPath"); + + var serviceAccountCredential = ServiceAccountCredential.FromServiceAccountData(File.OpenRead(credentialsPath)); + var credentials = GoogleCredential.FromServiceAccountCredential(serviceAccountCredential); + if (credentials.IsCreateScopedRequired) + { + credentials = credentials.CreateScoped(new[] + { + CloudKMSService.Scope.CloudPlatform + }); + } + + var kmsService = new CloudKMSService(new BaseClientService.Initializer + { + HttpClientInitializer = credentials, + GZipEnabled = true + }); + + + return new GoogleCloudKeyManagment( + kmsService, + serviceAccountCredential.ProjectId, + keyRingName, + location, + protectionLevel); } } } \ No newline at end of file diff --git a/src/encrypt-api/Controllers/EncryptController.cs b/src/encrypt-api/Controllers/EncryptController.cs index fa41a2051..a87f49294 100644 --- a/src/encrypt-api/Controllers/EncryptController.cs +++ b/src/encrypt-api/Controllers/EncryptController.cs @@ -18,9 +18,6 @@ public class EncryptController : Controller private readonly IKeyManagement mKeyManagement; private readonly ILogger mAuditLogger = Log.ForContext().AsAudit(); private readonly ILogger mLogger = Log.ForContext(); - - //see: https://github.com/kubernetes/kubernetes/blob/d5803e596fc8aba17aa8c74a96aff9c73bb0f1da/staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount/util.go#L27 - private const string ServiceAccountUsernamePrefix = "system:serviceaccount:"; public EncryptController(IKeyManagement keyManagement) { diff --git a/src/encrypt-api/Startup.cs b/src/encrypt-api/Startup.cs index eea3a4a29..59702d6ac 100644 --- a/src/encrypt-api/Startup.cs +++ b/src/encrypt-api/Startup.cs @@ -1,5 +1,9 @@ using System; +using System.IO; using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2; +using Google.Apis.CloudKMS.v1; +using Google.Apis.Services; using Kamus.KeyManagement; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -51,7 +55,9 @@ public void ConfigureServices (IServiceCollection services) { { var provider = Configuration.GetValue("KeyManagement:Provider"); switch (provider) - { + { + case "GoogleKms": + return GetGoogleCloudKeyManagment(); case "AzureKeyVault": return new EnvelopeEncryptionDecorator( new AzureKeyVaultKeyManagement(s.GetService(), Configuration), @@ -117,5 +123,37 @@ public void Configure (IApplicationBuilder app, IHostingEnvironment env) { app.UseMvc (); } + + private IKeyManagement GetGoogleCloudKeyManagment() + { + var location = Configuration.GetValue("KeyManagement:GoogleKms:Location"); + var keyRingName = Configuration.GetValue("KeyManagement:GoogleKms:KeyRingName"); + var protectionLevel = Configuration.GetValue("KeyManagement:GoogleKms:ProtectionLevel"); + var credentialsPath = Configuration.GetValue("KeyManagement:GoogleKms:CredentialsPath"); + + var serviceAccountCredential = ServiceAccountCredential.FromServiceAccountData(File.OpenRead(credentialsPath)); + var credentials = GoogleCredential.FromServiceAccountCredential(serviceAccountCredential); + if (credentials.IsCreateScopedRequired) + { + credentials = credentials.CreateScoped(new[] + { + CloudKMSService.Scope.CloudPlatform + }); + } + + var kmsService = new CloudKMSService(new BaseClientService.Initializer + { + HttpClientInitializer = credentials, + GZipEnabled = true + }); + + + return new GoogleCloudKeyManagment( + kmsService, + serviceAccountCredential.ProjectId, + keyRingName, + location, + protectionLevel); + } } } \ No newline at end of file diff --git a/src/key-managment/GoogleCloudKeyManagment.cs b/src/key-managment/GoogleCloudKeyManagment.cs new file mode 100644 index 000000000..f7d7da6a8 --- /dev/null +++ b/src/key-managment/GoogleCloudKeyManagment.cs @@ -0,0 +1,94 @@ +using System; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Google; +using Google.Apis.CloudKMS.v1; +using Google.Apis.CloudKMS.v1.Data; +using Microsoft.AspNetCore.WebUtilities; + +namespace Kamus.KeyManagement +{ + public class GoogleCloudKeyManagment : IKeyManagement + { + private readonly CloudKMSService mKmsService; + private readonly string mProjectName; + private readonly string mKeyringName; + private readonly string mKeyringLocation; + private readonly string mProtectionLevel; + + public GoogleCloudKeyManagment( + CloudKMSService kmsService, + string projectName, + string keyringName, + string keyringLocation, + string protectionLevel) + { + mKmsService = kmsService; + mProjectName = projectName; + mKeyringName = keyringName; + mKeyringLocation = keyringLocation; + mProtectionLevel = protectionLevel; + } + + + public async Task Decrypt(string encryptedData, string serviceAccountId) + { + var safeId = ComputeKeyId(serviceAccountId); + var cryptoKeys = mKmsService.Projects.Locations.KeyRings.CryptoKeys; + var keyringId = $"projects/{mProjectName}/locations/{mKeyringLocation}/keyRings/{mKeyringName}"; + var keyId = $"{keyringId}/cryptoKeys/{safeId}"; + + var result = await cryptoKeys.Decrypt(new DecryptRequest + { + Ciphertext = encryptedData + }, keyId).ExecuteAsync(); + + return result.Plaintext; + } + + public async Task Encrypt(string data, string serviceAccountId, bool createKeyIfMissing = true) + { + var safeId = ComputeKeyId(serviceAccountId); + var cryptoKeys = mKmsService.Projects.Locations.KeyRings.CryptoKeys; + var keyringId = $"projects/{mProjectName}/locations/{mKeyringLocation}/keyRings/{mKeyringName}"; + var keyId = $"{keyringId}/cryptoKeys/{safeId}"; + try + { + await cryptoKeys.Get(keyId).ExecuteAsync(); + } catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.NotFound && createKeyIfMissing) + { + //todo: handle key rotation - currently set to never expired + var key = new CryptoKey + { + Purpose = "ENCRYPT_DECRYPT", + VersionTemplate = new CryptoKeyVersionTemplate + { + ProtectionLevel = mProtectionLevel + } + }; + + var request = cryptoKeys.Create(key, keyringId); + request.CryptoKeyId = safeId; + await request.ExecuteAsync(); + } + + var encryted = await cryptoKeys.Encrypt(new EncryptRequest + { + Plaintext = data + }, keyId).ExecuteAsync(); + + return encryted.Ciphertext; + } + + private string ComputeKeyId(string serviceUserName) + { + return + WebEncoders.Base64UrlEncode( + SHA256.Create().ComputeHash( + Encoding.UTF8.GetBytes(serviceUserName))) + .Replace("_", "-"); + } + } +} diff --git a/src/key-managment/key-managment.csproj b/src/key-managment/key-managment.csproj index fa96103af..2ad6f9588 100644 --- a/src/key-managment/key-managment.csproj +++ b/src/key-managment/key-managment.csproj @@ -6,6 +6,7 @@ + diff --git a/tests/integration/GoogleCloudKeyManagment.cs b/tests/integration/GoogleCloudKeyManagment.cs new file mode 100644 index 000000000..c815a64ce --- /dev/null +++ b/tests/integration/GoogleCloudKeyManagment.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2; +using Google.Apis.CloudKMS.v1; +using Google.Apis.Services; +using Kamus.KeyManagement; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace integration +{ + public class GoogleCloudKeyManagmentTests + { + private readonly IKeyManagement mGoogleCloudKeyManagement; + private readonly CloudKMSService mCloudKmsService; + private readonly IConfiguration mConfiguration; + + public GoogleCloudKeyManagmentTests() + { + mConfiguration = new ConfigurationBuilder() + .AddJsonFile("settings.json") + .AddEnvironmentVariables().Build(); + + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(mConfiguration.GetValue("KeyManagment:GoogleKms:Credentials")); + writer.Flush(); + stream.Position = 0; + var serviceAccountCredential = ServiceAccountCredential.FromServiceAccountData(stream); + var credentials = GoogleCredential.FromServiceAccountCredential(serviceAccountCredential); + if (credentials.IsCreateScopedRequired) + { + credentials = credentials.CreateScoped(new[] + { + CloudKMSService.Scope.CloudPlatform + }); + } + + mCloudKmsService = new CloudKMSService(new BaseClientService.Initializer + { + HttpClientInitializer = credentials, + GZipEnabled = true + }); + var location = mConfiguration.GetValue("KeyManagment:GoogleKms:Location"); + var keyRingName = mConfiguration.GetValue("KeyManagment:GoogleKms:KeyRingName"); + var protectionLevel = mConfiguration.GetValue("KeyManagment:GoogleKms:ProtectionLevel"); + + mGoogleCloudKeyManagement = new GoogleCloudKeyManagment( + mCloudKmsService, + serviceAccountCredential.ProjectId, + keyRingName, + location, + protectionLevel); + } + + + + [Fact] + public async Task TestFullFlow() + { + var sa = "sa:namespace"; + var data = "data"; + var encrypted = await mGoogleCloudKeyManagement.Encrypt(data, sa); + var decrypted = await mGoogleCloudKeyManagement.Decrypt(encrypted, sa); + + Assert.Equal(data, decrypted); + + } + } +} diff --git a/tests/integration/integration.csproj b/tests/integration/integration.csproj index 0ae0ae1bb..3aaa89cb2 100644 --- a/tests/integration/integration.csproj +++ b/tests/integration/integration.csproj @@ -7,6 +7,7 @@ +