Skip to content

Commit e782583

Browse files
authored
Support serializing JWK using RFC 7517 (Azure#24282)
* Support serializing JWK using RFC 7517 Resolves Azure#16155 * Make JsonWebKeyConverter internal
1 parent b95d393 commit e782583

File tree

10 files changed

+376
-8
lines changed

10 files changed

+376
-8
lines changed

eng/service.proj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
<ItemGroup>
1717
<MgmtExcludePaths Include="$(MSBuildThisFileDirectory)..\sdk\*\Microsoft.*.Management.*\**\*.csproj;$(MSBuildThisFileDirectory)..\sdk\*mgmt*\**\*.csproj" />
1818
<TestProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\tests\**\*.csproj" />
19-
<SamplesProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\samples\**\*.csproj" />
19+
<SamplesProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\samples\**\*.csproj;..\sdk\$(ServiceDirectory)\samples\**\*.csproj" />
2020
<PerfProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\perf\**\*.csproj" />
2121
<StressProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\stress\**\*.csproj" />
2222
<SampleApplications Include="..\samples\**\*.csproj" />
23-
<SrcProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\src\**\*.csproj" />
23+
<SrcProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\src\**\*.csproj" Exclude="@(TestProjects);@(SamplesProjects);@(PerfProjects);@(StressProjects)" />
2424
<ProjectReference Include="@(TestProjects)" Exclude="@(MgmtExcludePaths)" Condition="'$(IncludeTests)' == 'true'" />
2525
<ProjectReference Include="@(SamplesProjects)" Exclude="@(MgmtExcludePaths)" Condition="'$(IncludeSamples)' == 'true'" />
2626
<ProjectReference Include="@(PerfProjects)" Exclude="@(MgmtExcludePaths)" Condition="'$(IncludePerf)' == 'true'" />
@@ -71,4 +71,4 @@
7171
SkipNonexistentProjects="false"
7272
SkipNonexistentTargets="true" />
7373
</Target>
74-
</Project>
74+
</Project>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features Added
66

7+
- Added `JsonWebKeyConverter` to support serializing and deserializing a `JsonWebKey` to a RFC 7517 JWK. ([#16155](https://github.com/Azure/azure-sdk-for-net/issues/16155))
78
- Added `KeyClient.GetCryptographyClient` to get a `CryptographyClient` that uses the same options, policies, and pipeline as the `KeyClient` that created it. ([#23786](https://github.com/Azure/azure-sdk-for-net/issues/23786))
89
- Added `KeyVaultKeyIdentifier.TryCreate` to parse key URIs without throwing an exception when invalid. ([#23146](https://github.com/Azure/azure-sdk-for-net/issues/23146))
910

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Serializing and encrypting with a JWK
2+
3+
This sample demonstrates how to serialize a [JSON web key (JWK)][JWK] and use it in a `CryptographyClient` to perform
4+
cryptographic operations requiring only the public key. We subsequently verify the operation by decrypting the
5+
ciphertext in Key Vault or Managed HSM using the same key.
6+
7+
To get started, you'll need a URI to an Azure Key Vault or Managed HSM. See the [README][] for links and instructions.
8+
9+
## Creating a KeyClient
10+
11+
To create a new `KeyClient` to create, get, update, or delete keys, you need the endpoint to an Azure Key Vault and credentials.
12+
You can use the [DefaultAzureCredential][] to try a number of common authentication methods optimized for both running as a service and development.
13+
14+
In the sample below, you can set `keyVaultUrl` based on an environment variable, configuration setting, or any way that works for your application.
15+
16+
```C# Snippet:KeysSample7KeyClient
17+
var keyClient = new KeyClient(new Uri(keyVaultUrl), new DefaultAzureCredential());
18+
```
19+
20+
## Creating a key
21+
22+
First, create an RSA key which will be used to wrap and unwrap another key.
23+
24+
```C# Snippet:KeysSample7CreateKey
25+
string rsaKeyName = $"CloudRsaKey-{Guid.NewGuid()}";
26+
var rsaKey = new CreateRsaKeyOptions(rsaKeyName, hardwareProtected: false)
27+
{
28+
KeySize = 2048,
29+
};
30+
31+
KeyVaultKey cloudRsaKey = keyClient.CreateRsaKey(rsaKey);
32+
Debug.WriteLine($"Key is returned with name {cloudRsaKey.Name} and type {cloudRsaKey.KeyType}");
33+
```
34+
35+
## Serialize the JWK
36+
37+
The `KeyVaultKey.Key` property is the JSON web key (JWK) which can be serialized using `System.Text.Json`. You might
38+
serialize the JWK to save it for future sessions or use it with other libraries.
39+
40+
```C# Snippet:KeysSample7Serialize
41+
using FileStream file = File.Create(path);
42+
using (Utf8JsonWriter writer = new Utf8JsonWriter(file))
43+
{
44+
JsonSerializer.Serialize(writer, cloudRsaKey.Key);
45+
}
46+
47+
Debug.WriteLine($"Saved JWK to {path}");
48+
```
49+
50+
## Encrypting with the JWK
51+
52+
Assuming you had saved the serialized JWK for future sessions, you can decrypt it before you need to use it:
53+
54+
```C# Snippet:KeysSamples7Deserialize
55+
byte[] buffer = File.ReadAllBytes(path);
56+
JsonWebKey jwk = JsonSerializer.Deserialize<JsonWebKey>(buffer);
57+
58+
Debug.WriteLine($"Read JWK from {path} with ID {jwk.Id}");
59+
```
60+
61+
You can then create a new `CryptographyClient` from the JWK to perform cryptographic operations using what public
62+
key information is contained within the JWK:
63+
64+
```C# Snippet:KeysSample7Encrypt
65+
var encryptClient = new CryptographyClient(jwk);
66+
67+
byte[] plaintext = Encoding.UTF8.GetBytes(content);
68+
EncryptResult encrypted = encryptClient.Encrypt(EncryptParameters.RsaOaepParameters(plaintext));
69+
70+
Debug.WriteLine($"Encrypted: {Encoding.UTF8.GetString(plaintext)}");
71+
```
72+
73+
## Decrypting with Key Vault or Managed HSM
74+
75+
Because Key Vault and Managed HSM do not return the private key material, you can decrypt the ciphertext encrypted above
76+
remotely in the Key Vault or Managed HSM. We'll get a `CryptographyClient` from our original `KeyClient` that shares
77+
the same policy, including any customized pipeline policies, diagnostic information, and more.
78+
79+
```C# Snippet:KeysSample7Decrypt
80+
CryptographyClient decryptClient = keyClient.GetCryptographyClient(cloudRsaKey.Name, cloudRsaKey.Properties.Version);
81+
DecryptResult decrypted = decryptClient.Decrypt(DecryptParameters.RsaOaepParameters(ciphertext));
82+
83+
Debug.WriteLine($"Decrypted: {Encoding.UTF8.GetString(decrypted.Plaintext)}");
84+
```
85+
86+
## Source
87+
88+
To see the full example source, see:
89+
90+
* [Synchronous Sample7_SerializeJsonWebKey.cs](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/samples/Sample7_SerializeJsonWebKey.cs)
91+
* [Asynchronous Sample7_SerializeJsonWebKeyAsync.cs](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/keyvault/Azure.Security.KeyVault.Keys/tests/samples/Sample7_SerializeJsonWebKeyAsync.cs)
92+
93+
[DefaultAzureCredential]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/README.md
94+
[JWK]: https://datatracker.ietf.org/doc/html/rfc7517
95+
[README]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/keyvault/Azure.Security.KeyVault.Keys/README.md

sdk/keyvault/Azure.Security.KeyVault.Keys/src/JsonWebKey.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Runtime.CompilerServices;
99
using System.Security.Cryptography;
1010
using System.Text.Json;
11+
using System.Text.Json.Serialization;
1112
using Azure.Core;
1213

1314
namespace Azure.Security.KeyVault.Keys
@@ -17,6 +18,7 @@ namespace Azure.Security.KeyVault.Keys
1718
/// structure that represents a cryptographic key.
1819
/// For more information, see <see href="http://tools.ietf.org/html/draft-ietf-jose-json-web-key-18">JSON Web Key (JWK)</see>.
1920
/// </summary>
21+
[JsonConverter(typeof(JsonWebKeyConverter))]
2022
public class JsonWebKey : IJsonDeserializable, IJsonSerializable
2123
{
2224
private const string KeyIdPropertyName = "kid";
@@ -36,6 +38,7 @@ public class JsonWebKey : IJsonDeserializable, IJsonSerializable
3638
private const string KPropertyName = "k";
3739
private const string TPropertyName = "key_hsm";
3840

41+
private static readonly JsonEncodedText s_keyIdPropertyNameBytes = JsonEncodedText.Encode(KeyIdPropertyName);
3942
private static readonly JsonEncodedText s_keyTypePropertyNameBytes = JsonEncodedText.Encode(KeyTypePropertyName);
4043
private static readonly JsonEncodedText s_keyOpsPropertyNameBytes = JsonEncodedText.Encode(KeyOpsPropertyName);
4144
private static readonly JsonEncodedText s_curveNamePropertyNameBytes = JsonEncodedText.Encode(CurveNamePropertyName);
@@ -428,8 +431,12 @@ internal void ReadProperties(JsonElement json)
428431
}
429432
}
430433

431-
internal void WriteProperties(Utf8JsonWriter json)
434+
internal void WriteProperties(Utf8JsonWriter json, bool withId = false)
432435
{
436+
if (Id != null && withId)
437+
{
438+
json.WriteString(s_keyIdPropertyNameBytes, Id);
439+
}
433440
if (KeyType != default)
434441
{
435442
json.WriteString(s_keyTypePropertyNameBytes, KeyType.ToString());
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Text.Json;
6+
using System.Text.Json.Serialization;
7+
using Azure.Core;
8+
9+
namespace Azure.Security.KeyVault.Keys
10+
{
11+
/// <summary>
12+
/// Converts a <see cref="JsonWebKey"/> to or from JSON.
13+
/// </summary>
14+
internal sealed class JsonWebKeyConverter : JsonConverter<JsonWebKey>
15+
{
16+
/// <inheritdoc/>
17+
public override JsonWebKey Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
18+
{
19+
using JsonDocument doc = JsonDocument.ParseValue(ref reader);
20+
21+
JsonWebKey value = new();
22+
value.ReadProperties(doc.RootElement);
23+
24+
return value;
25+
}
26+
27+
/// <inheritdoc/>
28+
public override void Write(Utf8JsonWriter writer, JsonWebKey value, JsonSerializerOptions options)
29+
{
30+
Argument.AssertNotNull(writer, nameof(writer));
31+
Argument.AssertNotNull(value, nameof(value));
32+
33+
writer.WriteStartObject();
34+
value.WriteProperties(writer, withId: true);
35+
writer.WriteEndObject();
36+
}
37+
}
38+
}

sdk/keyvault/Azure.Security.KeyVault.Keys/tests/JsonWebKeyTests.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using System.Security.Cryptography;
99
using System.Text;
10+
using System.Text.Json;
1011
using Azure.Core;
1112
using NUnit.Framework;
1213

@@ -42,7 +43,7 @@ public void SerializeOctet()
4243
JsonWebKey deserialized = new JsonWebKey();
4344
deserialized.Deserialize(ms);
4445

45-
Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.s_instance));
46+
Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.Shared));
4647
}
4748

4849
[Test]
@@ -140,7 +141,7 @@ public void SerializeECDsa(string oid, string friendlyName, bool includePrivateP
140141
JsonWebKey deserialized = new JsonWebKey();
141142
deserialized.Deserialize(ms);
142143

143-
Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.s_instance));
144+
Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.Shared));
144145
#endif
145146
}
146147

@@ -281,7 +282,7 @@ public void SerializeRSA(bool includePrivateParameters)
281282
JsonWebKey deserialized = new JsonWebKey();
282283
deserialized.Deserialize(ms);
283284

284-
Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.s_instance));
285+
Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.Shared));
285286
}
286287

287288
[Test]
@@ -357,6 +358,33 @@ public void ToRSAInvalidKey(RSAParameters rsaParameters, string name)
357358
Assert.Throws<InvalidOperationException>(() => jwk.ToRSA(), "Expected exception not thrown for data named '{0}'", name);
358359
}
359360

361+
[Test]
362+
public void SerializesJwt()
363+
{
364+
using RSA rsa = RSA.Create();
365+
JsonWebKey jwk = new(rsa, true)
366+
{
367+
Id = "https://test.vault.azure.net/keys/test/abcd1234",
368+
};
369+
370+
// Serialize
371+
using MemoryStream ms = new();
372+
using (Utf8JsonWriter writer = new(ms))
373+
{
374+
JsonSerializer.Serialize(writer, jwk);
375+
}
376+
377+
string content = Encoding.UTF8.GetString(ms.ToArray());
378+
StringAssert.Contains(@"""key_ops""", content);
379+
StringAssert.Contains(@"""kid"":""https://test.vault.azure.net/keys/test/abcd1234""", content);
380+
StringAssert.Contains(@"""kty"":""RSA""", content);
381+
382+
// Deserialize
383+
JsonWebKey deserialized = JsonSerializer.Deserialize<JsonWebKey>(content);
384+
385+
Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.Shared));
386+
}
387+
360388
private static IEnumerable<object> GetECDSaTestData()
361389
{
362390
(string Oid, string FriendlyName)[] oids = new[]
@@ -455,7 +483,7 @@ private static bool HasPrivateKey(JsonWebKey jwk)
455483

456484
private class JsonWebKeyComparer : IEqualityComparer<JsonWebKey>
457485
{
458-
internal static readonly IEqualityComparer<JsonWebKey> s_instance = new JsonWebKeyComparer();
486+
public static IEqualityComparer<JsonWebKey> Shared { get; } = new JsonWebKeyComparer();
459487

460488
public bool Equals(JsonWebKey x, JsonWebKey y)
461489
{

sdk/keyvault/Azure.Security.KeyVault.Keys/tests/KeyClientLiveTests.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.IO;
67
using System.Linq;
78
using System.Text;
9+
using System.Text.Json;
810
using System.Threading.Tasks;
911
using Azure.Core.TestFramework;
1012
using Azure.Security.KeyVault.Tests;
@@ -95,6 +97,14 @@ public async Task CreateEcHsmKey()
9597
KeyVaultKey keyReturned = await Client.GetKeyAsync(keyName);
9698

9799
AssertKeyVaultKeysEqual(ecHsmkey, keyReturned);
100+
101+
using MemoryStream ms = new();
102+
await JsonSerializer.SerializeAsync(ms, keyReturned.Key);
103+
string json = Encoding.UTF8.GetString(ms.ToArray());
104+
105+
StringAssert.Contains($@"""kid"":""{keyReturned.Id}""", json);
106+
StringAssert.Contains(@"""kty"":""EC-HSM""", json);
107+
StringAssert.Contains(@"""crv"":""P-256""", json);
98108
}
99109

100110
[Test]
@@ -149,6 +159,13 @@ public async Task CreateRsaHsmKey()
149159
KeyVaultKey keyReturned = await Client.GetKeyAsync(keyName);
150160

151161
AssertKeyVaultKeysEqual(rsaHsmkey, keyReturned);
162+
163+
using MemoryStream ms = new();
164+
await JsonSerializer.SerializeAsync(ms, keyReturned.Key);
165+
string json = Encoding.UTF8.GetString(ms.ToArray());
166+
167+
StringAssert.Contains($@"""kid"":""{keyReturned.Id}""", json);
168+
StringAssert.Contains(@"""kty"":""RSA-HSM""", json);
152169
}
153170

154171
[Test]

sdk/keyvault/Azure.Security.KeyVault.Keys/tests/SampleFixture.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public partial class GetKeys : SampleFixture { }
2727
public partial class Sample4_EncryptDecypt : SampleFixture { }
2828
public partial class Sample5_SignVerify : SampleFixture { }
2929
public partial class Sample6_WrapUnwrap : SampleFixture { }
30+
public partial class Sample7_SerializeJsonWebKey : SampleFixture { }
3031
public partial class Snippets : SampleFixture { }
3132
#pragma warning restore SA1402 // File may only contain a single type
3233
}

0 commit comments

Comments
 (0)