From e54868af014009499723331ca58bd5e00b70652e Mon Sep 17 00:00:00 2001 From: Junjie Gao Date: Fri, 14 Jul 2023 11:36:31 +0800 Subject: [PATCH] feat: add self_signed parameter in the pluginConfig Signed-off-by: Junjie Gao --- .../Certificate/CertificateBundleTests.cs | 11 ++++ .../Command/GenerateSignatureTests.cs | 42 ++++++++++-- .../TestData/empty.pem | 0 .../Certificate/CertificateBundle.cs | 5 ++ .../Command/GenerateSignature.cs | 65 ++++++++++++------- docs/ca-signed-workflow.md | 2 +- docs/self-signed-workflow.md | 4 +- 7 files changed, 96 insertions(+), 33 deletions(-) create mode 100644 Notation.Plugin.AzureKeyVault.Tests/TestData/empty.pem diff --git a/Notation.Plugin.AzureKeyVault.Tests/Certificate/CertificateBundleTests.cs b/Notation.Plugin.AzureKeyVault.Tests/Certificate/CertificateBundleTests.cs index d7605e90..9812c5f7 100644 --- a/Notation.Plugin.AzureKeyVault.Tests/Certificate/CertificateBundleTests.cs +++ b/Notation.Plugin.AzureKeyVault.Tests/Certificate/CertificateBundleTests.cs @@ -1,5 +1,6 @@ using System.IO; using System.Security.Cryptography.X509Certificates; +using Notation.Plugin.Protocol; using Xunit; namespace Notation.Plugin.AzureKeyVault.Certificate.Tests @@ -19,5 +20,15 @@ public void Create_WithValidPemFile_ReturnsCertificates() Assert.NotNull(certificates); Assert.True(certificates.Count > 0); } + + [Fact] + public void Create_ThrowsPluginException_WhenPemFileIsEmpty() + { + // Arrange + var pemPath = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "empty.pem"); + + // Act & Assert + Assert.Throws(() => CertificateBundle.Create(pemPath)); + } } } diff --git a/Notation.Plugin.AzureKeyVault.Tests/Command/GenerateSignatureTests.cs b/Notation.Plugin.AzureKeyVault.Tests/Command/GenerateSignatureTests.cs index b3aba494..3c1babf9 100644 --- a/Notation.Plugin.AzureKeyVault.Tests/Command/GenerateSignatureTests.cs +++ b/Notation.Plugin.AzureKeyVault.Tests/Command/GenerateSignatureTests.cs @@ -23,10 +23,10 @@ public async Task RunAsync_SelfSigned_ReturnsValidGenerateSignatureResponseAsync var mockSignature = new byte[] { 0x01, 0x02, 0x03, 0x04 }; var mockKeyVaultClient = new Mock(); - // mock GetCertificateChainAsync - var mockCertChain = CertificateBundle.Create(Path.Combine(Directory.GetCurrentDirectory(), "TestData", "rsa_2048.crt")); - mockKeyVaultClient.Setup(client => client.GetCertificateChainAsync()) - .ReturnsAsync(mockCertChain); + // mock GetCertificateAsync + var mockCert = new X509Certificate2(Path.Combine(Directory.GetCurrentDirectory(), "TestData", "leaf.crt")); + mockKeyVaultClient.Setup(client => client.GetCertificateAsync()) + .ReturnsAsync(mockCert); // mock SignAsync mockKeyVaultClient.Setup(client => client.SignAsync(It.IsAny(), It.IsAny())) @@ -35,7 +35,10 @@ public async Task RunAsync_SelfSigned_ReturnsValidGenerateSignatureResponseAsync var request = new GenerateSignatureRequest( contractVersion: "1.0", keyId: keyId, - pluginConfig: null, + pluginConfig: new Dictionary() + { + ["self_signed"] = "true" + }, keySpec: expectedKeySpec, hashAlgorithm: "SHA-256", payload: Encoding.UTF8.GetBytes("Cg==")); @@ -54,7 +57,7 @@ public async Task RunAsync_SelfSigned_ReturnsValidGenerateSignatureResponseAsync Assert.Equal("RSASSA-PSS-SHA-256", response.SigningAlgorithm); Assert.Equal(mockSignature, response.Signature); Assert.Single(response.CertificateChain); - Assert.Equal(mockCertChain[0].RawData, response.CertificateChain[0]); + Assert.Equal(mockCert.RawData, response.CertificateChain[0]); } [Fact] @@ -218,5 +221,32 @@ public void RunAsync_OtherRequestFailedException() Assert.Throws(() => generateSignatureCommand.RunAsync().GetAwaiter().GetResult()); } + + [Fact] + public void RunAsync_SelfSignedWithCaCerts() + { + // Arrange + var keyId = "https://testvault.vault.azure.net/keys/testkey/123"; + var expectedKeySpec = "RSA-2048"; + var mockSignature = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + var mockKeyVaultClient = new Mock(); + + // mock GetCertificateChainAsync + var mockCertChain = CertificateBundle.Create(Path.Combine(Directory.GetCurrentDirectory(), "TestData", "cert_chain.pem")); + var request = new GenerateSignatureRequest( + contractVersion: "1.0", + keyId: keyId, + pluginConfig: new Dictionary() { + { "self_signed", "true" }, + { "ca_certs", "/test/cert.pem" } + }, + keySpec: expectedKeySpec, + hashAlgorithm: "SHA-256", + payload: Encoding.UTF8.GetBytes("Cg==")); + + var generateSignatureCommand = new GenerateSignature(request, mockKeyVaultClient.Object); + + Assert.Throws(() => generateSignatureCommand.RunAsync().GetAwaiter().GetResult()); + } } } diff --git a/Notation.Plugin.AzureKeyVault.Tests/TestData/empty.pem b/Notation.Plugin.AzureKeyVault.Tests/TestData/empty.pem new file mode 100644 index 00000000..e69de29b diff --git a/Notation.Plugin.AzureKeyVault/Certificate/CertificateBundle.cs b/Notation.Plugin.AzureKeyVault/Certificate/CertificateBundle.cs index 68cd234b..a13fe607 100644 --- a/Notation.Plugin.AzureKeyVault/Certificate/CertificateBundle.cs +++ b/Notation.Plugin.AzureKeyVault/Certificate/CertificateBundle.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography.X509Certificates; +using Notation.Plugin.Protocol; namespace Notation.Plugin.AzureKeyVault.Certificate { @@ -14,6 +15,10 @@ public static X509Certificate2Collection Create(string pemFilePath) { var certificates = new X509Certificate2Collection(); certificates.ImportFromPemFile(pemFilePath); + if (certificates.Count == 0) + { + throw new PluginException($"No certificate found in {pemFilePath}"); + } return certificates; } } diff --git a/Notation.Plugin.AzureKeyVault/Command/GenerateSignature.cs b/Notation.Plugin.AzureKeyVault/Command/GenerateSignature.cs index 89ad37a7..82efcd05 100644 --- a/Notation.Plugin.AzureKeyVault/Command/GenerateSignature.cs +++ b/Notation.Plugin.AzureKeyVault/Command/GenerateSignature.cs @@ -43,41 +43,58 @@ public async Task RunAsync() // Obtain the certificate chain X509Certificate2Collection certBundle; X509Certificate2 leafCert; - if (_request.PluginConfig?.TryGetValue("ca_certs", out var certBundlePath) == true) + string? certBundlePath = _request.PluginConfig?.GetValueOrDefault("ca_certs", ""); + + if (_request.PluginConfig?.GetValueOrDefault("self_signed")?.ToLower() == "true") { - // Obtain the certificate bundle from file - // (including the intermediate and root certificates). - certBundle = CertificateBundle.Create(certBundlePath); + if (!string.IsNullOrEmpty(certBundlePath)) + { + throw new PluginException("Self-signed certificate is specified. Please do not specify the `ca_certs` parameter if it is a self-signed certificate."); + } + // Certificate bundle is empty + certBundle = new X509Certificate2Collection(); - // obtain the leaf certificate from Azure Key Vault + // Obtain self-signed leaf certificate from Azure Key Vault leafCert = await _keyVaultClient.GetCertificateAsync(); } else { - // Obtain the certificate chain from Azure Key Vault using - // GetSecret permission. Ensure intermediate and root - // certificates are merged into the Key Vault certificate to - // retrieve the full chain. - // reference: https://learn.microsoft.com//azure/key-vault/certificates/create-certificate-signing-request - X509Certificate2Collection? certificateChain; - try + if (string.IsNullOrEmpty(certBundlePath)) { - certificateChain = await _keyVaultClient.GetCertificateChainAsync(); - } - catch (Azure.RequestFailedException ex) - { - if (ex.Message.Contains("does not have secrets get permission")) + // Obtain the certificate chain from Azure Key Vault using + // GetSecret permission. Ensure intermediate and root + // certificates are merged into the Key Vault certificate to + // retrieve the full chain. + // reference: https://learn.microsoft.com//azure/key-vault/certificates/create-certificate-signing-request + X509Certificate2Collection? certificateChain; + try { - throw new PluginException("The plugin does not have secrets get permission. Please grant the permission to the credential associated with the plugin or specify the file path of the certificate chain bundle through the `ca_certs` parameter in the plugin config."); + certificateChain = await _keyVaultClient.GetCertificateChainAsync(); } - throw; - } + catch (Azure.RequestFailedException ex) + { + if (ex.Message.Contains("does not have secrets get permission")) + { + throw new PluginException("The plugin does not have secrets get permission. Please grant the permission to the credential associated with the plugin or specify the file path of the certificate chain bundle through the `ca_certs` parameter in the plugin config."); + } + throw; + } + + // the certBundle is the certificates start from the second one of certificateChain + certBundle = new X509Certificate2Collection(certificateChain.Skip(1).ToArray()); - // the certBundle is the certificates start from the second one of certificateChain - certBundle = new X509Certificate2Collection(certificateChain.Skip(1).ToArray()); + // the leafCert is the first certificate in the certBundle + leafCert = certificateChain[0]; + } + else + { + // Obtain the certificate bundle from file + // (including the intermediate and root certificates). + certBundle = CertificateBundle.Create(certBundlePath); - // the leafCert is the first certificate in the certBundle - leafCert = certificateChain[0]; + // obtain the leaf certificate from Azure Key Vault + leafCert = await _keyVaultClient.GetCertificateAsync(); + } } // Extract KeySpec from the certificate diff --git a/docs/ca-signed-workflow.md b/docs/ca-signed-workflow.md index 6de3ff0b..79c98075 100644 --- a/docs/ca-signed-workflow.md +++ b/docs/ca-signed-workflow.md @@ -114,7 +114,7 @@ > **Note** If you have generated the certificate with `openssl` according to the above steps, the certificate bundle is the root certificate `ca.crt`. ```sh notation key add --plugin azure-kv --id $keyID akv-key --default - notation sign $server/hello-world:v1 --plugin-config=ca_certs=$certBundlePath + notation sign $server/hello-world:v1 ``` The following example output shows the artifact is successfully signed. diff --git a/docs/self-signed-workflow.md b/docs/self-signed-workflow.md index c47fb256..2ed6abca 100644 --- a/docs/self-signed-workflow.md +++ b/docs/self-signed-workflow.md @@ -3,7 +3,7 @@ > > **Note** The following guide can be executed on Linux bash, macOS Zsh and Windows WSL. 1. [Install the Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) -2. Log in using the Azure CLI, set the subscription, and confirm the `GetCertificates`, `GetSecrets` and `Sign` permission for Azure Key Vault have been granted to your role: +2. Log in using the Azure CLI, set the subscription, and confirm the `GetCertificates` and `Sign` permission for Azure Key Vault have been granted to your role: ```sh az login az account set --subscription $subscriptionID @@ -75,7 +75,7 @@ ``` 7. Sign the container image with Notation: ```sh - notation key add --plugin azure-kv --id $keyID akv-key --default + notation key add --plugin azure-kv --id $keyID akv-key --default --plugin-config=self_signed=true notation sign $server/hello-world:v1 ```