Skip to content

Commit

Permalink
Add support for additional string-based key/value pairs in the protec…
Browse files Browse the repository at this point in the history
…ted header
  • Loading branch information
alexzautke committed Oct 3, 2024
1 parent 5bc853a commit 3a098ef
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 2 deletions.
71 changes: 71 additions & 0 deletions CreativeCode.JWS.Tests/JwsTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;
using System.Text.RegularExpressions;
using CreativeCode.JWK.KeyParts;
using Xunit;
using FluentAssertions;
Expand Down Expand Up @@ -285,4 +286,74 @@ void export()
var tasks = Enumerable.Range(0, 4).Select(_ => Task.Run(export));
await Task.WhenAll(tasks);
}

[Fact]
public void JwsWithAdditionalProtectedHeadersCanBeSerialized()
{
var keyUse = PublicKeyUse.Signature;
var keyOperations = new HashSet<KeyOperation>(new[] {KeyOperation.ComputeDigitalSignature, KeyOperation.VerifyDigitalSignature});
var algorithm = Algorithm.ES256;
var jwk = new JWK.JWK(algorithm, keyUse, keyOperations);
var payloadJson = @"{
""key1"": ""test"",
""key2"": ""test2"",
""testArray"": [
{
""complexTest"": ""test"",
""success"": true
}
]
}";
var payloadJsonNormalized = Regex.Replace(payloadJson, @"\s+", string.Empty, RegexOptions.Compiled);
var payload = Encoding.UTF8.GetBytes(payloadJsonNormalized);
var additionalHeaders = new Dictionary<string, string>()
{
{"testKey", "testValue"}
};

var joseHeader = new ProtectedJoseHeader(jwk, SerializationOption.JwsCompactSerialization, "application/json", additionalHeaders);
var jws = new JWS(new []{joseHeader}, payload);
jws.CalculateSignature();
var jwsCompactJson = jws.Export();

var parts = jwsCompactJson.Split(".");
parts.Count().Should().Be(3, "A JWS using compact serialization should consist of three parts");

var headerJson = Encoding.UTF8.GetString(Base64urlDecode(parts.First()));
headerJson.Length.Should().BePositive("A JWS protected header should be present");
var parsedProtectedHeader = JObject.Parse(headerJson);

parsedProtectedHeader.TryGetValue("alg", out var _).Should().BeTrue();
parsedProtectedHeader.TryGetValue("jwk", out var _).Should().BeTrue();
parsedProtectedHeader.TryGetValue("kid", out var _).Should().BeTrue();
parsedProtectedHeader.TryGetValue("typ", out var _).Should().BeTrue();
parsedProtectedHeader.TryGetValue("cty", out var _).Should().BeTrue();
parsedProtectedHeader.TryGetValue("testKey", out var _).Should().BeTrue();

parsedProtectedHeader.GetValue("alg").ToString().Should().Be("ES256");
parsedProtectedHeader.GetValue("jwk").Children().Count().Should().Be(8);
var parsedJwk = JObject.Parse(parsedProtectedHeader.GetValue("jwk").ToString());
parsedJwk.GetValue("kty").ToString().Should().Be(jwk.KeyType.Type);
parsedJwk.GetValue("use").ToString().Should().Be(jwk.PublicKeyUse.KeyUse);
parsedJwk.GetValue("alg").ToString().Should().Be(jwk.Algorithm.Name);
parsedJwk.GetValue("kid").ToString().Should().Be(jwk.KeyID);
parsedJwk.GetValue("crv").ToString().Should().Be(jwk.KeyParameters[KeyParameter.ECKeyParameterCRV]);
parsedJwk.GetValue("y").ToString().Should().Be(jwk.KeyParameters[KeyParameter.ECKeyParameterY]);
parsedJwk.GetValue("x").ToString().Should().Be(jwk.KeyParameters[KeyParameter.ECKeyParameterX]);
parsedJwk.GetValue("key_ops").Values<string>().Should().BeEquivalentTo(jwk.KeyOperations.Select(op => op.Operation));
parsedProtectedHeader.GetValue("kid").ToString().Should().Be(jwk.KeyID);
parsedProtectedHeader.GetValue("typ").ToString().Should().Be("JOSE");
parsedProtectedHeader.GetValue("cty").ToString().Should().Be("json");
parsedProtectedHeader.GetValue("testKey").ToString().Should().Be("testValue");

var payloadFromJws = Encoding.UTF8.GetString(Base64urlDecode(parts.ElementAt(1)));
payloadFromJws.Length.Should().BePositive("A JWS payload should be present");
payloadFromJws.Should().Be(payloadJsonNormalized);

var signature = parts.Last();
signature.Length.Should().BePositive("A JWS signature should be present");

var publicKey = new JWK.JWK(jwk.Export());
VerifySignature(publicKey, SigningInput(joseHeader, Encoding.UTF8.GetBytes(payloadJsonNormalized)), Base64urlDecode(signature)).Should().BeTrue();
}
}
25 changes: 23 additions & 2 deletions JWS/ProtectedJoseHeader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using CreativeCode.JWK.KeyParts;
Expand All @@ -10,7 +11,7 @@ namespace CreativeCode.JWS

/*
See RFC7517 JSON Web Signature - Section 3. JSON Web Signature (JWS) Overview
See RFC7515 JSON Web Signature - Section 3. JSON Web Signature (JWS) Overview
For a JWS, the JOSE Header members are the union of the members of
these values:
Expand Down Expand Up @@ -46,17 +47,37 @@ public class ProtectedJoseHeader

[JsonProperty(PropertyName = "cty")]
public string ContentType { get; internal set; } // OPTIONAL

[JsonProperty()]
[JWSConverterAttribute(typeof(AdditionalHeadersConverter))]
public IReadOnlyDictionary<string, string> AdditionalHeaders { get; internal set; } // OPTIONAL

public ProtectedJoseHeader(JWK.JWK jwk, string contentType, SerializationOption serializationOption)
{
if (jwk is null)
throw new ArgumentNullException("jwk MUST be provided");


if (string.IsNullOrEmpty(contentType))
throw new ArgumentNullException("contentType MUST be provided and MUST NOT be empty");

JWK = jwk;
Algorithm = jwk.Algorithm;
KeyID = jwk.KeyID;
Type = serializationOption;
ContentType = ShortenContentType(contentType);
}

public ProtectedJoseHeader(JWK.JWK jwk, SerializationOption serializationOption, string contentType = null, IReadOnlyDictionary<string, string> additionalHeaders = null)
{
if (jwk is null)
throw new ArgumentNullException("jwk MUST be provided");

JWK = jwk;
Algorithm = jwk.Algorithm;
KeyID = jwk.KeyID;
Type = serializationOption;
ContentType = ShortenContentType(contentType);
AdditionalHeaders = additionalHeaders;
}

/*
Expand Down
39 changes: 39 additions & 0 deletions JWS/TypeConverters/AdditionalProtectedHeadersConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace CreativeCode.JWS.TypeConverters
{
internal class AdditionalHeadersConverter : IJWSConverter
{
public string Serialize(object propertyValue = null)
{
if (propertyValue == null)
return string.Empty;

var sb = new StringBuilder();
var sw = new StringWriter(sb);
var writer = new JsonTextWriter(sw);

var additionalHeaders = propertyValue as IReadOnlyDictionary<string, string>;
foreach (var header in additionalHeaders!)
{
writer.WritePropertyName(header.Key);
writer.WriteValue(header.Value);
}
return sb.ToString();
}

public object Deserialize(JToken jwkRepresentation)
{
throw new System.NotImplementedException();
}

public object Deserialize(JObject jwkRepresentation)
{
throw new System.NotImplementedException();
}
}
}

0 comments on commit 3a098ef

Please sign in to comment.