diff --git a/Notation.Plugin.AzureKeyVault.Tests/Certificate/CertificateChainTests.cs b/Notation.Plugin.AzureKeyVault.Tests/Certificate/CertificateChainTests.cs index 930b1fb3..904a6024 100644 --- a/Notation.Plugin.AzureKeyVault.Tests/Certificate/CertificateChainTests.cs +++ b/Notation.Plugin.AzureKeyVault.Tests/Certificate/CertificateChainTests.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.IO; using System.Security.Cryptography.X509Certificates; -using Notation.Plugin.Protocol; using Xunit; namespace Notation.Plugin.AzureKeyVault.Certificate.Tests diff --git a/Notation.Plugin.AzureKeyVault.Tests/Certificate/Pkcs12Tests.cs b/Notation.Plugin.AzureKeyVault.Tests/Certificate/Pkcs12Tests.cs new file mode 100644 index 00000000..cb361797 --- /dev/null +++ b/Notation.Plugin.AzureKeyVault.Tests/Certificate/Pkcs12Tests.cs @@ -0,0 +1,50 @@ +using System.IO; +using System.Security.Cryptography.Pkcs; +using Notation.Plugin.Protocol; +using Xunit; + +namespace Notation.Plugin.AzureKeyVault.Certificate.Tests +{ + public class Pkcs12Tests + { + [Fact] + public void ReEncode() + { + // read the pfx file + byte[] data = File.ReadAllBytes(Path.Combine(Directory.GetCurrentDirectory(), "TestData", "cert_chain.pfx")); + Pkcs12Info originPfx = Pkcs12Info.Decode(data, out _); + Assert.True(originPfx.IntegrityMode == Pkcs12IntegrityMode.Password); + + // re-encode the pfx file + byte[] newData = Pkcs12.ReEncode(data); + Pkcs12Info pfxWithoutMac = Pkcs12Info.Decode(newData, out _); + Assert.True(pfxWithoutMac.IntegrityMode == Pkcs12IntegrityMode.None); + } + + [Fact] + public void ReEncode_WithInvalidMac() + { + // read the pfx file + byte[] data = File.ReadAllBytes(Path.Combine(Directory.GetCurrentDirectory(), "TestData", "cert_invalid_mac.pfx")); + Pkcs12Info originPfx = Pkcs12Info.Decode(data, out _); + Assert.True(originPfx.IntegrityMode == Pkcs12IntegrityMode.Password); + + // re-encode the pfx file + Assert.Throws(() => Pkcs12.ReEncode(data)); + } + + [Fact] + public void ReEncode_withoutMac(){ + // read the pfx file + byte[] data = File.ReadAllBytes(Path.Combine(Directory.GetCurrentDirectory(), "TestData", "cert_without_mac.pfx")); + Pkcs12Info originPfx = Pkcs12Info.Decode(data, out _); + Assert.True(originPfx.IntegrityMode == Pkcs12IntegrityMode.None); + + // re-encode the pfx file + byte[] newData = Pkcs12.ReEncode(data); + Pkcs12Info pfxWithoutMac = Pkcs12Info.Decode(newData, out _); + Assert.True(pfxWithoutMac.IntegrityMode == Pkcs12IntegrityMode.None); + + } + } +} diff --git a/Notation.Plugin.AzureKeyVault.Tests/Notation.Plugin.AzureKeyVault.Tests.csproj b/Notation.Plugin.AzureKeyVault.Tests/Notation.Plugin.AzureKeyVault.Tests.csproj index b7e396f2..081fb7f3 100644 --- a/Notation.Plugin.AzureKeyVault.Tests/Notation.Plugin.AzureKeyVault.Tests.csproj +++ b/Notation.Plugin.AzureKeyVault.Tests/Notation.Plugin.AzureKeyVault.Tests.csproj @@ -11,6 +11,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Notation.Plugin.AzureKeyVault.Tests/TestData/cert_invalid_mac.pfx b/Notation.Plugin.AzureKeyVault.Tests/TestData/cert_invalid_mac.pfx new file mode 100644 index 00000000..59e40403 Binary files /dev/null and b/Notation.Plugin.AzureKeyVault.Tests/TestData/cert_invalid_mac.pfx differ diff --git a/Notation.Plugin.AzureKeyVault.Tests/TestData/cert_without_mac.pfx b/Notation.Plugin.AzureKeyVault.Tests/TestData/cert_without_mac.pfx new file mode 100644 index 00000000..510dd00b Binary files /dev/null and b/Notation.Plugin.AzureKeyVault.Tests/TestData/cert_without_mac.pfx differ diff --git a/Notation.Plugin.AzureKeyVault/Certificate/CertificateChain.cs b/Notation.Plugin.AzureKeyVault/Certificate/CertificateChain.cs index 94b0231f..1fefc180 100644 --- a/Notation.Plugin.AzureKeyVault/Certificate/CertificateChain.cs +++ b/Notation.Plugin.AzureKeyVault/Certificate/CertificateChain.cs @@ -1,6 +1,4 @@ -using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using Notation.Plugin.Protocol; namespace Notation.Plugin.AzureKeyVault.Certificate { diff --git a/Notation.Plugin.AzureKeyVault/Certificate/Pkcs12.cs b/Notation.Plugin.AzureKeyVault/Certificate/Pkcs12.cs new file mode 100644 index 00000000..a6cc07cc --- /dev/null +++ b/Notation.Plugin.AzureKeyVault/Certificate/Pkcs12.cs @@ -0,0 +1,51 @@ +using System.Security.Cryptography.Pkcs; +using Notation.Plugin.Protocol; + +namespace Notation.Plugin.AzureKeyVault.Certificate +{ + static class Pkcs12 + { + /// + /// Re-encode the PKCS12 data to remove the MAC and keys. + /// The macOS doesn't support PKCS12 with non-encrypted MAC. + /// + /// + /// + /// + public static byte[] ReEncode(byte[] data) + { + Pkcs12Info pfx = Pkcs12Info.Decode(data, out _); + // only remove the MAC if it is password protected + if (pfx.IntegrityMode != Pkcs12IntegrityMode.Password) + { + return data; + } + // verify the MAC with null password + if (!pfx.VerifyMac(null)) + { + throw new ValidationException("Invalid MAC or the MAC password is not null"); + } + + // re-build PFX without MAC and keys + Pkcs12Builder pfxBuilder = new Pkcs12Builder(); + foreach (var safeContent in pfx.AuthenticatedSafe) + { + // decrypt with null password + safeContent.Decrypt((byte[]?)null); + + // create a newSafeContent and only contains the certificate bag + var newSafeContent = new Pkcs12SafeContents(); + foreach (var bag in safeContent.GetBags()) + { + if (bag is Pkcs12CertBag) + { + newSafeContent.AddSafeBag(bag); + } + } + pfxBuilder.AddSafeContentsUnencrypted(newSafeContent); + } + pfxBuilder.SealWithoutIntegrity(); + return pfxBuilder.Encode(); + } + } +} diff --git a/Notation.Plugin.AzureKeyVault/KeyVault/KeyVaultClient.cs b/Notation.Plugin.AzureKeyVault/KeyVault/KeyVaultClient.cs index 11821476..87dd8cad 100644 --- a/Notation.Plugin.AzureKeyVault/KeyVault/KeyVaultClient.cs +++ b/Notation.Plugin.AzureKeyVault/KeyVault/KeyVaultClient.cs @@ -1,9 +1,11 @@ using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; using Azure.Identity; using Azure.Security.KeyVault.Certificates; using Azure.Security.KeyVault.Keys.Cryptography; using Azure.Security.KeyVault.Secrets; +using Notation.Plugin.AzureKeyVault.Certificate; using Notation.Plugin.Protocol; [assembly: InternalsVisibleTo("Notation.Plugin.AzureKeyVault.Tests")] @@ -190,7 +192,22 @@ public async Task GetCertificateChainAsync() { case "application/x-pkcs12": // If the secret is a PKCS12 file, decode the base64 encoding - chain.Import(Convert.FromBase64String(secretValue), "", X509KeyStorageFlags.EphemeralKeySet); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // macOS doesn't support non-encrypted MAC + // https://github.com/dotnet/runtime/issues/23635 + chain.Import( + rawData: Pkcs12.ReEncode(Convert.FromBase64String(secretValue)), + password: null, + keyStorageFlags: X509KeyStorageFlags.DefaultKeySet); + } + else + { + chain.Import( + rawData: Convert.FromBase64String(secretValue), + password: null, + keyStorageFlags: X509KeyStorageFlags.EphemeralKeySet); + } break; case "application/x-pem-file": // If the secret is a PEM file, parse the PEM content directly diff --git a/Notation.Plugin.AzureKeyVault/Notation.Plugin.AzureKeyVault.csproj b/Notation.Plugin.AzureKeyVault/Notation.Plugin.AzureKeyVault.csproj index bcefa212..7ba5e827 100644 --- a/Notation.Plugin.AzureKeyVault/Notation.Plugin.AzureKeyVault.csproj +++ b/Notation.Plugin.AzureKeyVault/Notation.Plugin.AzureKeyVault.csproj @@ -20,6 +20,7 @@ +