Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add self_signed pluginConfig #119

Merged
merged 7 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
}
}
}
105 changes: 93 additions & 12 deletions Notation.Plugin.AzureKeyVault.Tests/Command/GenerateSignatureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ public async Task RunAsync_SelfSigned_ReturnsValidGenerateSignatureResponseAsync
// Arrange
var keyId = "https://testvault.vault.azure.net/keys/testkey/123";
var expectedKeySpec = "RSA-2048";
var mockCert = new X509Certificate2(Path.Combine(Directory.GetCurrentDirectory(), "TestData", "rsa_2048.crt"));
var mockSignature = new byte[] { 0x01, 0x02, 0x03, 0x04 };

var mockKeyVaultClient = new Mock<IKeyVaultClient>();

// mock GetCertificateAsync
var mockCert = new X509Certificate2(Path.Combine(Directory.GetCurrentDirectory(), "TestData", "leaf.crt"));
mockKeyVaultClient.Setup(client => client.GetCertificateAsync())
.ReturnsAsync(mockCert);

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 Down Expand Up @@ -106,16 +109,16 @@ public async Task RunAsync_ca_certs_ReturnsValidGenerateSignatureResponseAsync()
}

[Fact]
public async Task RunAsync_as_secret_ReturnsValidGenerateSignatureResponseAsync()
public async Task RunAsync_default_ReturnsValidGenerateSignatureResponseAsync()
{
// Arrange
var keyId = "https://testvault.vault.azure.net/keys/testkey/123";
var expectedKeySpec = "RSA-2048";
var mockSignature = new byte[] { 0x01, 0x02, 0x03, 0x04 };
var mockCertChain = CertificateBundle.Create(Path.Combine(Directory.GetCurrentDirectory(), "TestData", "cert_chain.pem"));

var mockKeyVaultClient = new Mock<IKeyVaultClient>();
// mock GetCertificateAsync

// mock GetCertificateChainAsync
var mockCertChain = CertificateBundle.Create(Path.Combine(Directory.GetCurrentDirectory(), "TestData", "cert_chain.pem"));
mockKeyVaultClient.Setup(client => client.GetCertificateChainAsync())
.ReturnsAsync(mockCertChain);

Expand All @@ -126,10 +129,7 @@ public async Task RunAsync_as_secret_ReturnsValidGenerateSignatureResponseAsync(
var request = new GenerateSignatureRequest(
contractVersion: "1.0",
keyId: keyId,
pluginConfig: new Dictionary<string, string>()
{
["as_secret"] = "true"
},
pluginConfig: new Dictionary<string, string>() { },
keySpec: expectedKeySpec,
hashAlgorithm: "SHA-256",
payload: Encoding.UTF8.GetBytes("Cg=="));
Expand Down Expand Up @@ -167,5 +167,86 @@ public void Constructor_Invalid()

Assert.Throws<ValidationException>(() => new GenerateSignature(InvalidInputJson));
}

[Fact]
public void RunAsync_NoSecertsGetPermission()
{
// 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"));
mockKeyVaultClient.Setup(client => client.GetCertificateChainAsync())
.ThrowsAsync(new Azure.RequestFailedException("does not have secrets get permission"));

var request = new GenerateSignatureRequest(
contractVersion: "1.0",
keyId: keyId,
pluginConfig: new Dictionary<string, string>() { },
keySpec: expectedKeySpec,
hashAlgorithm: "SHA-256",
payload: Encoding.UTF8.GetBytes("Cg=="));

var generateSignatureCommand = new GenerateSignature(request, mockKeyVaultClient.Object);

Assert.Throws<PluginException>(() => generateSignatureCommand.RunAsync().GetAwaiter().GetResult());
}

[Fact]
public void RunAsync_OtherRequestFailedException()
{
// 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"));
mockKeyVaultClient.Setup(client => client.GetCertificateChainAsync())
.ThrowsAsync(new Azure.RequestFailedException("RequestFailedException"));

var request = new GenerateSignatureRequest(
contractVersion: "1.0",
keyId: keyId,
pluginConfig: new Dictionary<string, string>() { },
keySpec: expectedKeySpec,
hashAlgorithm: "SHA-256",
payload: Encoding.UTF8.GetBytes("Cg=="));

var generateSignatureCommand = new GenerateSignature(request, mockKeyVaultClient.Object);

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;
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved

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
67 changes: 45 additions & 22 deletions Notation.Plugin.AzureKeyVault/Command/GenerateSignature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,35 +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 if (_request.PluginConfig?.TryGetValue("as_secret", out var asSecret) == true && asSecret.Equals("true", StringComparison.OrdinalIgnoreCase))
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
var certificateChain = await _keyVaultClient.GetCertificateChainAsync();
if (string.IsNullOrEmpty(certBundlePath))
{
// 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
{
certificateChain = await _keyVaultClient.GetCertificateChainAsync();
}
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
{
// only have the leaf certificate
certBundle = new X509Certificate2Collection();
leafCert = await _keyVaultClient.GetCertificateAsync();
// 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);

// obtain the leaf certificate from Azure Key Vault
leafCert = await _keyVaultClient.GetCertificateAsync();
}
}

// Extract KeySpec from the certificate
Expand Down
8 changes: 4 additions & 4 deletions docs/self-signed-workflow.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Sign and verify an artifact with a self-signed Azure Key Vault certificate
> **Warning** Using self-signed certificates are intended for development and testing. Outside of development and testing, a certificate from a trusted CA is recommended.

>
> **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 `GetCertificate` and `Sign` permission have been granted to your role:
Expand Down Expand Up @@ -75,12 +75,12 @@
```
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
```

The following example output shows the artifact is successfully signed.
```
```text
Warning: Always sign the artifact using digest(@sha256:...) rather than a tag(:v1) because tags are mutable and a tag reference can point to a different artifact than the one signed.
Successfully signed notation.azurecr.io/hello-world@sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4
```
Expand Down Expand Up @@ -123,7 +123,7 @@
notation verify $server/hello-world:v1
```
The following output shows the artifact is successfully verified.
```
```text
Warning: Always verify the artifact using digest(@sha256:...) rather than a tag(:v1) because resolved digest may not point to the same signed artifact, as tags are mutable.
Successfully verified signature for notation.azurecr.io/hello-world@sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4
```