Skip to content

Commit 3fdd056

Browse files
authored
Add DownloadCertificate method to get X509Certificate2 (Azure#16815)
* Add DownloadCertificate method to get X509Certificate2 Resolves Azure#12083 * Add sample for just pubkey certificate * Resolve feedback * Return Response<X509Certificate2>
1 parent 2a69ffe commit 3fdd056

File tree

11 files changed

+4127
-5
lines changed

11 files changed

+4127
-5
lines changed

sdk/keyvault/Azure.Security.KeyVault.Certificates/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## 4.2.0-beta.3 (Unreleased)
44

5+
### Added
6+
7+
- Added `DownloadCertificate` and `DownloadCertificateAsync` methods to get `X509Certificate2` with private key if permitted ([#12083](https://github.com/Azure/azure-sdk-for-net/issues/12083))
58

69
## 4.2.0-beta.2 (2020-10-06)
710

sdk/keyvault/Azure.Security.KeyVault.Certificates/api/Azure.Security.KeyVault.Certificates.netstandard2.0.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public CertificateClient(System.Uri vaultUri, Azure.Core.TokenCredential credent
2222
public virtual System.Threading.Tasks.Task<Azure.Response<System.Collections.Generic.IList<Azure.Security.KeyVault.Certificates.CertificateContact>>> DeleteContactsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
2323
public virtual Azure.Response<Azure.Security.KeyVault.Certificates.CertificateIssuer> DeleteIssuer(string issuerName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
2424
public virtual System.Threading.Tasks.Task<Azure.Response<Azure.Security.KeyVault.Certificates.CertificateIssuer>> DeleteIssuerAsync(string issuerName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
25+
public virtual Azure.Response<System.Security.Cryptography.X509Certificates.X509Certificate2> DownloadCertificate(string certificateName, string version = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
26+
public virtual System.Threading.Tasks.Task<Azure.Response<System.Security.Cryptography.X509Certificates.X509Certificate2>> DownloadCertificateAsync(string certificateName, string version = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
2527
public virtual Azure.Response<Azure.Security.KeyVault.Certificates.KeyVaultCertificateWithPolicy> GetCertificate(string certificateName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
2628
public virtual System.Threading.Tasks.Task<Azure.Response<Azure.Security.KeyVault.Certificates.KeyVaultCertificateWithPolicy>> GetCertificateAsync(string certificateName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
2729
public virtual Azure.Security.KeyVault.Certificates.CertificateOperation GetCertificateOperation(string certificateName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }

sdk/keyvault/Azure.Security.KeyVault.Certificates/src/CertificateClient.cs

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

44
using System;
55
using System.Collections.Generic;
66
using System.Globalization;
7+
using System.Security.Cryptography;
8+
using System.Security.Cryptography.X509Certificates;
79
using System.Threading;
810
using System.Threading.Tasks;
911
using Azure.Core;
@@ -152,6 +154,104 @@ public virtual async Task<CertificateOperation> StartCreateCertificateAsync(stri
152154
}
153155
}
154156

157+
#pragma warning disable AZC0015 // Unexpected client method return type.
158+
/// <summary>
159+
/// Creates an <see cref="X509Certificate2"/> from the specified certificate.
160+
/// </summary>
161+
/// <remarks>
162+
/// Because <see cref="KeyVaultCertificate.Cer"/> contains only the public key, this method attempts to download the managed secret
163+
/// that contains the full certificate. If you do not have permissions to get the secret,
164+
/// <see cref="RequestFailedException"/> will be thrown with an appropriate error response.
165+
/// If you want an <see cref="X509Certificate2"/> with only the public key, instantiate it passing only the
166+
/// <see cref="KeyVaultCertificate.Cer"/> property.
167+
/// </remarks>
168+
/// <param name="certificateName">The name of the certificate to download.</param>
169+
/// <param name="version">Optional version of a certificate to download.</param>
170+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
171+
/// <returns>An <see cref="X509Certificate2"/> from the specified certificate.</returns>
172+
/// <exception cref="ArgumentException"><paramref name="certificateName"/> is empty.</exception>
173+
/// <exception cref="ArgumentNullException"><paramref name="certificateName"/> is null.</exception>
174+
/// <exception cref="InvalidOperationException">The managed secret did not contain a certificate.</exception>
175+
/// <exception cref="RequestFailedException">The request failed. See <see cref="RequestFailedException.ErrorCode"/> and the exception message for details.</exception>
176+
public virtual Response<X509Certificate2> DownloadCertificate(string certificateName, string version = null, CancellationToken cancellationToken = default)
177+
{
178+
Argument.AssertNotNullOrEmpty(certificateName, nameof(certificateName));
179+
180+
using DiagnosticScope scope = _pipeline.CreateScope($"{nameof(CertificateClient)}.{nameof(DownloadCertificate)}");
181+
scope.AddAttribute("certificate", certificateName);
182+
scope.Start();
183+
184+
try
185+
{
186+
KeyVaultCertificateWithPolicy certificate = _pipeline.SendRequest(RequestMethod.Get, () => new KeyVaultCertificateWithPolicy(), cancellationToken, CertificatesPath, certificateName, "/", version);
187+
Response<KeyVaultSecret> secretResponse = _pipeline.SendRequest(RequestMethod.Get, () => new KeyVaultSecret(), certificate.SecretId, cancellationToken);
188+
189+
string value = secretResponse.Value.Value;
190+
if (string.IsNullOrEmpty(value))
191+
{
192+
throw new InvalidOperationException($"Secret {certificate.SecretId} contains no value");
193+
}
194+
195+
byte[] rawData = Convert.FromBase64String(value);
196+
197+
return Response.FromValue(new X509Certificate2(rawData), secretResponse.GetRawResponse());
198+
}
199+
catch (Exception e)
200+
{
201+
scope.Failed(e);
202+
throw;
203+
}
204+
}
205+
206+
/// <summary>
207+
/// Creates an <see cref="X509Certificate2"/> from the specified certificate.
208+
/// </summary>
209+
/// <remarks>
210+
/// Because <see cref="KeyVaultCertificate.Cer"/> contains only the public key, this method attempts to download the managed secret
211+
/// that contains the full certificate. If you do not have permissions to get the secret,
212+
/// <see cref="RequestFailedException"/> will be thrown with an appropriate error response.
213+
/// If you want an <see cref="X509Certificate2"/> with only the public key, instantiate it passing only the
214+
/// <see cref="KeyVaultCertificate.Cer"/> property.
215+
/// </remarks>
216+
/// <param name="certificateName">The name of the certificate to download.</param>
217+
/// <param name="version">Optional version of a certificate to download.</param>
218+
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param>
219+
/// <returns>An <see cref="X509Certificate2"/> from the specified certificate.</returns>
220+
/// <exception cref="ArgumentException"><paramref name="certificateName"/> is empty.</exception>
221+
/// <exception cref="ArgumentNullException"><paramref name="certificateName"/> is null.</exception>
222+
/// <exception cref="InvalidOperationException">The managed secret did not contain a certificate.</exception>
223+
/// <exception cref="RequestFailedException">The request failed. See <see cref="RequestFailedException.ErrorCode"/> and the exception message for details.</exception>
224+
public virtual async Task<Response<X509Certificate2>> DownloadCertificateAsync(string certificateName, string version = null, CancellationToken cancellationToken = default)
225+
{
226+
Argument.AssertNotNullOrEmpty(certificateName, nameof(certificateName));
227+
228+
using DiagnosticScope scope = _pipeline.CreateScope($"{nameof(CertificateClient)}.{nameof(DownloadCertificate)}");
229+
scope.AddAttribute("certificate", certificateName);
230+
scope.Start();
231+
232+
try
233+
{
234+
KeyVaultCertificateWithPolicy certificate = await _pipeline.SendRequestAsync(RequestMethod.Get, () => new KeyVaultCertificateWithPolicy(), cancellationToken, CertificatesPath, certificateName, "/", version).ConfigureAwait(false);
235+
Response<KeyVaultSecret> secretResponse = await _pipeline.SendRequestAsync(RequestMethod.Get, () => new KeyVaultSecret(), certificate.SecretId, cancellationToken).ConfigureAwait(false);
236+
237+
string value = secretResponse.Value.Value;
238+
if (string.IsNullOrEmpty(value))
239+
{
240+
throw new InvalidOperationException($"Secret {certificate.SecretId} contains no value");
241+
}
242+
243+
byte[] rawData = Convert.FromBase64String(value);
244+
245+
return Response.FromValue(new X509Certificate2(rawData), secretResponse.GetRawResponse());
246+
}
247+
catch (Exception e)
248+
{
249+
scope.Failed(e);
250+
throw;
251+
}
252+
}
253+
#pragma warning restore AZC0015 // Unexpected client method return type.
254+
155255
/// <summary>
156256
/// Returns the latest version of the <see cref="KeyVaultCertificate"/> along with its <see cref="CertificatePolicy"/>. This operation requires the certificates/get permission.
157257
/// </summary>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json;
5+
6+
namespace Azure.Security.KeyVault.Certificates
7+
{
8+
internal class KeyVaultSecret : IJsonDeserializable
9+
{
10+
private const string ValuePropertyName = "value";
11+
12+
internal KeyVaultSecret()
13+
{
14+
}
15+
16+
public string Value { get; internal set; }
17+
18+
internal virtual void ReadProperty(JsonProperty prop)
19+
{
20+
switch (prop.Name)
21+
{
22+
case ValuePropertyName:
23+
Value = prop.Value.GetString();
24+
break;
25+
}
26+
}
27+
28+
void IJsonDeserializable.ReadProperties(JsonElement json)
29+
{
30+
foreach (JsonProperty prop in json.EnumerateObject())
31+
{
32+
ReadProperty(prop);
33+
}
34+
}
35+
}
36+
}

sdk/keyvault/Azure.Security.KeyVault.Certificates/tests/CertificateClientLiveTests.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using System.Collections.Generic;
1818
using System.IO;
1919
using System.Linq;
20+
using System.Security.Cryptography;
2021
using System.Security.Cryptography.X509Certificates;
2122
using System.Text;
2223
using System.Threading;
@@ -739,6 +740,101 @@ public async Task VerifyUpdateCertificatePolicy()
739740
Assert.AreEqual(certificatePolicy.KeySize, updatePolicy.KeySize);
740741
}
741742

743+
[Test]
744+
public async Task DownloadLatestCertificate()
745+
{
746+
string name = Recording.GenerateId();
747+
CertificatePolicy policy = new CertificatePolicy
748+
{
749+
IssuerName = WellKnownIssuerNames.Self,
750+
Subject = "CN=default",
751+
KeyType = CertificateKeyType.Rsa,
752+
Exportable = true,
753+
ReuseKey = false,
754+
KeyUsage =
755+
{
756+
CertificateKeyUsage.DataEncipherment,
757+
CertificateKeyUsage.DigitalSignature,
758+
},
759+
CertificateTransparency = false,
760+
ContentType = CertificateContentType.Pkcs12,
761+
};
762+
763+
CertificateOperation operation = await Client.StartCreateCertificateAsync(name, policy);
764+
RegisterForCleanup(name);
765+
766+
await operation.WaitForCompletionAsync();
767+
768+
KeyVaultCertificate certificate = await Client.GetCertificateAsync(name);
769+
770+
using X509Certificate2 pub = new X509Certificate2(certificate.Cer);
771+
using RSA pubkey = (RSA)pub.PublicKey.Key;
772+
773+
byte[] plaintext = Encoding.UTF8.GetBytes("Hello, world!");
774+
byte[] ciphertext = pubkey.Encrypt(plaintext, RSAEncryptionPadding.Pkcs1);
775+
776+
using X509Certificate2 x509certificate = await Client.DownloadCertificateAsync(name);
777+
Assert.IsTrue(x509certificate.HasPrivateKey);
778+
779+
using RSA rsa = (RSA)x509certificate.PrivateKey;
780+
byte[] decrypted = rsa.Decrypt(ciphertext, RSAEncryptionPadding.Pkcs1);
781+
782+
CollectionAssert.AreEqual(plaintext, decrypted);
783+
}
784+
785+
[Test]
786+
public async Task DownloadVersionedCertificate()
787+
{
788+
string name = Recording.GenerateId();
789+
CertificatePolicy policy = new CertificatePolicy
790+
{
791+
IssuerName = WellKnownIssuerNames.Self,
792+
Subject = "CN=default",
793+
KeyType = CertificateKeyType.Rsa,
794+
Exportable = true,
795+
ReuseKey = false,
796+
KeyUsage =
797+
{
798+
CertificateKeyUsage.DataEncipherment,
799+
CertificateKeyUsage.DigitalSignature,
800+
},
801+
CertificateTransparency = false,
802+
ContentType = CertificateContentType.Pkcs12,
803+
};
804+
805+
CertificateOperation operation = await Client.StartCreateCertificateAsync(name, policy);
806+
RegisterForCleanup(name);
807+
808+
await operation.WaitForCompletionAsync();
809+
810+
KeyVaultCertificate certificate = await Client.GetCertificateAsync(name);
811+
string version = certificate.Properties.Version;
812+
813+
using X509Certificate2 pub = new X509Certificate2(certificate.Cer);
814+
using RSA pubkey = (RSA)pub.PublicKey.Key;
815+
816+
byte[] plaintext = Encoding.UTF8.GetBytes("Hello, world!");
817+
byte[] ciphertext = pubkey.Encrypt(plaintext, RSAEncryptionPadding.Pkcs1);
818+
819+
// Create a new certificate version that is not exportable just to further prove we are not downloading it.
820+
policy.Exportable = false;
821+
operation = await Client.StartCreateCertificateAsync(name, policy);
822+
823+
await operation.WaitForCompletionAsync();
824+
825+
certificate = await Client.GetCertificateAsync(name);
826+
Assert.AreNotEqual(version, certificate.Properties.Version);
827+
828+
// Now download the certificate and test decryption.
829+
using X509Certificate2 x509certificate = await Client.DownloadCertificateAsync(name, version);
830+
Assert.IsTrue(x509certificate.HasPrivateKey);
831+
832+
using RSA rsa = (RSA)x509certificate.PrivateKey;
833+
byte[] decrypted = rsa.Decrypt(ciphertext, RSAEncryptionPadding.Pkcs1);
834+
835+
CollectionAssert.AreEqual(plaintext, decrypted);
836+
}
837+
742838
private static CertificatePolicy DefaultPolicy => new CertificatePolicy
743839
{
744840
IssuerName = WellKnownIssuerNames.Self,

0 commit comments

Comments
 (0)