Skip to content

Commit

Permalink
feat: add self_signed parameter in the pluginConfig
Browse files Browse the repository at this point in the history
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
  • Loading branch information
JeyJeyGao committed Jul 14, 2023
1 parent eded4d1 commit e54868a
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO;
using System.Security.Cryptography.X509Certificates;
using Notation.Plugin.Protocol;
using Xunit;

namespace Notation.Plugin.AzureKeyVault.Certificate.Tests
Expand All @@ -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<PluginException>(() => CertificateBundle.Create(pemPath));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ public async Task RunAsync_SelfSigned_ReturnsValidGenerateSignatureResponseAsync
var mockSignature = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var mockKeyVaultClient = new Mock<IKeyVaultClient>();

// 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<SignatureAlgorithm>(), It.IsAny<byte[]>()))
Expand All @@ -35,7 +35,10 @@ public async Task RunAsync_SelfSigned_ReturnsValidGenerateSignatureResponseAsync
var request = new GenerateSignatureRequest(
contractVersion: "1.0",
keyId: keyId,
pluginConfig: null,
pluginConfig: new Dictionary<string, string>()
{
["self_signed"] = "true"
},
keySpec: expectedKeySpec,
hashAlgorithm: "SHA-256",
payload: Encoding.UTF8.GetBytes("Cg=="));
Expand All @@ -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]
Expand Down Expand Up @@ -218,5 +221,32 @@ public void RunAsync_OtherRequestFailedException()

Assert.Throws<Azure.RequestFailedException>(() => 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<IKeyVaultClient>();

// 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<string, string>() {
{ "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<PluginException>(() => generateSignatureCommand.RunAsync().GetAwaiter().GetResult());
}
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Security.Cryptography.X509Certificates;
using Notation.Plugin.Protocol;

namespace Notation.Plugin.AzureKeyVault.Certificate
{
Expand All @@ -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;
}
}
Expand Down
65 changes: 41 additions & 24 deletions Notation.Plugin.AzureKeyVault/Command/GenerateSignature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,41 +43,58 @@ public async Task<IPluginResponse> 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
Expand Down
2 changes: 1 addition & 1 deletion docs/ca-signed-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions docs/self-signed-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
```
Expand Down

0 comments on commit e54868a

Please sign in to comment.