Skip to content

Commit

Permalink
Support serializing JWK using RFC 7517 (#24282)
Browse files Browse the repository at this point in the history
* Support serializing JWK using RFC 7517

Resolves #16155

* Make JsonWebKeyConverter internal
  • Loading branch information
heaths authored Sep 30, 2021
1 parent b95d393 commit e782583
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 8 deletions.
6 changes: 3 additions & 3 deletions eng/service.proj
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
<ItemGroup>
<MgmtExcludePaths Include="$(MSBuildThisFileDirectory)..\sdk\*\Microsoft.*.Management.*\**\*.csproj;$(MSBuildThisFileDirectory)..\sdk\*mgmt*\**\*.csproj" />
<TestProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\tests\**\*.csproj" />
<SamplesProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\samples\**\*.csproj" />
<SamplesProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\samples\**\*.csproj;..\sdk\$(ServiceDirectory)\samples\**\*.csproj" />
<PerfProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\perf\**\*.csproj" />
<StressProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\stress\**\*.csproj" />
<SampleApplications Include="..\samples\**\*.csproj" />
<SrcProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\src\**\*.csproj" />
<SrcProjects Include="..\sdk\$(ServiceDirectory)\$(Project)\src\**\*.csproj" Exclude="@(TestProjects);@(SamplesProjects);@(PerfProjects);@(StressProjects)" />
<ProjectReference Include="@(TestProjects)" Exclude="@(MgmtExcludePaths)" Condition="'$(IncludeTests)' == 'true'" />
<ProjectReference Include="@(SamplesProjects)" Exclude="@(MgmtExcludePaths)" Condition="'$(IncludeSamples)' == 'true'" />
<ProjectReference Include="@(PerfProjects)" Exclude="@(MgmtExcludePaths)" Condition="'$(IncludePerf)' == 'true'" />
Expand Down Expand Up @@ -71,4 +71,4 @@
SkipNonexistentProjects="false"
SkipNonexistentTargets="true" />
</Target>
</Project>
</Project>
1 change: 1 addition & 0 deletions sdk/keyvault/Azure.Security.KeyVault.Keys/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features Added

- 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))
- 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))
- Added `KeyVaultKeyIdentifier.TryCreate` to parse key URIs without throwing an exception when invalid. ([#23146](https://github.com/Azure/azure-sdk-for-net/issues/23146))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Serializing and encrypting with a JWK

This sample demonstrates how to serialize a [JSON web key (JWK)][JWK] and use it in a `CryptographyClient` to perform
cryptographic operations requiring only the public key. We subsequently verify the operation by decrypting the
ciphertext in Key Vault or Managed HSM using the same key.

To get started, you'll need a URI to an Azure Key Vault or Managed HSM. See the [README][] for links and instructions.

## Creating a KeyClient

To create a new `KeyClient` to create, get, update, or delete keys, you need the endpoint to an Azure Key Vault and credentials.
You can use the [DefaultAzureCredential][] to try a number of common authentication methods optimized for both running as a service and development.

In the sample below, you can set `keyVaultUrl` based on an environment variable, configuration setting, or any way that works for your application.

```C# Snippet:KeysSample7KeyClient
var keyClient = new KeyClient(new Uri(keyVaultUrl), new DefaultAzureCredential());
```

## Creating a key

First, create an RSA key which will be used to wrap and unwrap another key.

```C# Snippet:KeysSample7CreateKey
string rsaKeyName = $"CloudRsaKey-{Guid.NewGuid()}";
var rsaKey = new CreateRsaKeyOptions(rsaKeyName, hardwareProtected: false)
{
KeySize = 2048,
};

KeyVaultKey cloudRsaKey = keyClient.CreateRsaKey(rsaKey);
Debug.WriteLine($"Key is returned with name {cloudRsaKey.Name} and type {cloudRsaKey.KeyType}");
```

## Serialize the JWK

The `KeyVaultKey.Key` property is the JSON web key (JWK) which can be serialized using `System.Text.Json`. You might
serialize the JWK to save it for future sessions or use it with other libraries.

```C# Snippet:KeysSample7Serialize
using FileStream file = File.Create(path);
using (Utf8JsonWriter writer = new Utf8JsonWriter(file))
{
JsonSerializer.Serialize(writer, cloudRsaKey.Key);
}

Debug.WriteLine($"Saved JWK to {path}");
```

## Encrypting with the JWK

Assuming you had saved the serialized JWK for future sessions, you can decrypt it before you need to use it:

```C# Snippet:KeysSamples7Deserialize
byte[] buffer = File.ReadAllBytes(path);
JsonWebKey jwk = JsonSerializer.Deserialize<JsonWebKey>(buffer);

Debug.WriteLine($"Read JWK from {path} with ID {jwk.Id}");
```

You can then create a new `CryptographyClient` from the JWK to perform cryptographic operations using what public
key information is contained within the JWK:

```C# Snippet:KeysSample7Encrypt
var encryptClient = new CryptographyClient(jwk);

byte[] plaintext = Encoding.UTF8.GetBytes(content);
EncryptResult encrypted = encryptClient.Encrypt(EncryptParameters.RsaOaepParameters(plaintext));

Debug.WriteLine($"Encrypted: {Encoding.UTF8.GetString(plaintext)}");
```

## Decrypting with Key Vault or Managed HSM

Because Key Vault and Managed HSM do not return the private key material, you can decrypt the ciphertext encrypted above
remotely in the Key Vault or Managed HSM. We'll get a `CryptographyClient` from our original `KeyClient` that shares
the same policy, including any customized pipeline policies, diagnostic information, and more.

```C# Snippet:KeysSample7Decrypt
CryptographyClient decryptClient = keyClient.GetCryptographyClient(cloudRsaKey.Name, cloudRsaKey.Properties.Version);
DecryptResult decrypted = decryptClient.Decrypt(DecryptParameters.RsaOaepParameters(ciphertext));

Debug.WriteLine($"Decrypted: {Encoding.UTF8.GetString(decrypted.Plaintext)}");
```

## Source

To see the full example source, see:

* [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)
* [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)

[DefaultAzureCredential]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/README.md
[JWK]: https://datatracker.ietf.org/doc/html/rfc7517
[README]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/keyvault/Azure.Security.KeyVault.Keys/README.md
9 changes: 8 additions & 1 deletion sdk/keyvault/Azure.Security.KeyVault.Keys/src/JsonWebKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.Core;

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

private static readonly JsonEncodedText s_keyIdPropertyNameBytes = JsonEncodedText.Encode(KeyIdPropertyName);
private static readonly JsonEncodedText s_keyTypePropertyNameBytes = JsonEncodedText.Encode(KeyTypePropertyName);
private static readonly JsonEncodedText s_keyOpsPropertyNameBytes = JsonEncodedText.Encode(KeyOpsPropertyName);
private static readonly JsonEncodedText s_curveNamePropertyNameBytes = JsonEncodedText.Encode(CurveNamePropertyName);
Expand Down Expand Up @@ -428,8 +431,12 @@ internal void ReadProperties(JsonElement json)
}
}

internal void WriteProperties(Utf8JsonWriter json)
internal void WriteProperties(Utf8JsonWriter json, bool withId = false)
{
if (Id != null && withId)
{
json.WriteString(s_keyIdPropertyNameBytes, Id);
}
if (KeyType != default)
{
json.WriteString(s_keyTypePropertyNameBytes, KeyType.ToString());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.Core;

namespace Azure.Security.KeyVault.Keys
{
/// <summary>
/// Converts a <see cref="JsonWebKey"/> to or from JSON.
/// </summary>
internal sealed class JsonWebKeyConverter : JsonConverter<JsonWebKey>
{
/// <inheritdoc/>
public override JsonWebKey Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using JsonDocument doc = JsonDocument.ParseValue(ref reader);

JsonWebKey value = new();
value.ReadProperties(doc.RootElement);

return value;
}

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, JsonWebKey value, JsonSerializerOptions options)
{
Argument.AssertNotNull(writer, nameof(writer));
Argument.AssertNotNull(value, nameof(value));

writer.WriteStartObject();
value.WriteProperties(writer, withId: true);
writer.WriteEndObject();
}
}
}
36 changes: 32 additions & 4 deletions sdk/keyvault/Azure.Security.KeyVault.Keys/tests/JsonWebKeyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Azure.Core;
using NUnit.Framework;

Expand Down Expand Up @@ -42,7 +43,7 @@ public void SerializeOctet()
JsonWebKey deserialized = new JsonWebKey();
deserialized.Deserialize(ms);

Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.s_instance));
Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.Shared));
}

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

Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.s_instance));
Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.Shared));
#endif
}

Expand Down Expand Up @@ -281,7 +282,7 @@ public void SerializeRSA(bool includePrivateParameters)
JsonWebKey deserialized = new JsonWebKey();
deserialized.Deserialize(ms);

Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.s_instance));
Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.Shared));
}

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

[Test]
public void SerializesJwt()
{
using RSA rsa = RSA.Create();
JsonWebKey jwk = new(rsa, true)
{
Id = "https://test.vault.azure.net/keys/test/abcd1234",
};

// Serialize
using MemoryStream ms = new();
using (Utf8JsonWriter writer = new(ms))
{
JsonSerializer.Serialize(writer, jwk);
}

string content = Encoding.UTF8.GetString(ms.ToArray());
StringAssert.Contains(@"""key_ops""", content);
StringAssert.Contains(@"""kid"":""https://test.vault.azure.net/keys/test/abcd1234""", content);
StringAssert.Contains(@"""kty"":""RSA""", content);

// Deserialize
JsonWebKey deserialized = JsonSerializer.Deserialize<JsonWebKey>(content);

Assert.That(deserialized, Is.EqualTo(jwk).Using(JsonWebKeyComparer.Shared));
}

private static IEnumerable<object> GetECDSaTestData()
{
(string Oid, string FriendlyName)[] oids = new[]
Expand Down Expand Up @@ -455,7 +483,7 @@ private static bool HasPrivateKey(JsonWebKey jwk)

private class JsonWebKeyComparer : IEqualityComparer<JsonWebKey>
{
internal static readonly IEqualityComparer<JsonWebKey> s_instance = new JsonWebKeyComparer();
public static IEqualityComparer<JsonWebKey> Shared { get; } = new JsonWebKeyComparer();

public bool Equals(JsonWebKey x, JsonWebKey y)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Azure.Core.TestFramework;
using Azure.Security.KeyVault.Tests;
Expand Down Expand Up @@ -95,6 +97,14 @@ public async Task CreateEcHsmKey()
KeyVaultKey keyReturned = await Client.GetKeyAsync(keyName);

AssertKeyVaultKeysEqual(ecHsmkey, keyReturned);

using MemoryStream ms = new();
await JsonSerializer.SerializeAsync(ms, keyReturned.Key);
string json = Encoding.UTF8.GetString(ms.ToArray());

StringAssert.Contains($@"""kid"":""{keyReturned.Id}""", json);
StringAssert.Contains(@"""kty"":""EC-HSM""", json);
StringAssert.Contains(@"""crv"":""P-256""", json);
}

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

AssertKeyVaultKeysEqual(rsaHsmkey, keyReturned);

using MemoryStream ms = new();
await JsonSerializer.SerializeAsync(ms, keyReturned.Key);
string json = Encoding.UTF8.GetString(ms.ToArray());

StringAssert.Contains($@"""kid"":""{keyReturned.Id}""", json);
StringAssert.Contains(@"""kty"":""RSA-HSM""", json);
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public partial class GetKeys : SampleFixture { }
public partial class Sample4_EncryptDecypt : SampleFixture { }
public partial class Sample5_SignVerify : SampleFixture { }
public partial class Sample6_WrapUnwrap : SampleFixture { }
public partial class Sample7_SerializeJsonWebKey : SampleFixture { }
public partial class Snippets : SampleFixture { }
#pragma warning restore SA1402 // File may only contain a single type
}
Loading

0 comments on commit e782583

Please sign in to comment.