Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added initial b64=false support for detached payload JWS #119

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions UnitTestsNet46/DetachedPayloadTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Jose;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Xunit;

namespace UnitTests
{
public class DetachedPayloadTest
{

[Fact]
public void UKOpenBankingSignatureWorks()
{
var cert = X509();
DateTime start = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
long unixTime = (long)DateTime.UtcNow.Subtract(start).TotalSeconds;
string payload = @"{""toto"": ""titi""}";

var headers = new Dictionary<string, object>()
{
{ "b64", false },
{ "http://openbanking.org.uk/iat", unixTime },
{ "http://openbanking.org.uk/tan", "openbanking.org.uk" },
{ "crit", new string[] {"b64", "http://openbanking.org.uk/iat", "http://openbanking.org.uk/iss", "http://openbanking.org.uk/tan" } },
{ "kid", cert.Thumbprint.ToLower() },
{ "http://openbanking.org.uk/iss", "5d7ce654aba91f0019a87709" },
{ "alg", "PS256" }
};
var privateKey = cert.GetRSAPrivateKey();
var publicKey = (RSACryptoServiceProvider)cert.PublicKey.Key;

string token = Jose.JWT.Encode(payload, privateKey, JwsAlgorithm.PS256, extraHeaders: headers);
Console.Out.WriteLine("PS256 = {0}", token);
string[] parts = token.Split('.');
Assert.Equal(3, parts.Length);
Assert.True(string.IsNullOrEmpty(parts[1]));

var decodedToken = Jose.JWT.DecodeDetached(token, System.Text.Encoding.UTF8.GetBytes(payload), publicKey);
Assert.Equal(payload, decodedToken);

}
private X509Certificate2 X509()
{
return new X509Certificate2("jwt-2048.p12", "1", X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet);
}

private RSA PrivRsaKey()
{
return X509().GetRSAPrivateKey();
}

private RSACryptoServiceProvider PubKey()
{
return (RSACryptoServiceProvider)X509().PublicKey.Key;
}

private RSA PubRsaKey()
{
return X509().GetRSAPublicKey();
}
}
}
1 change: 1 addition & 0 deletions UnitTestsNet46/UnitTestsNet46.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
<Compile Include="Base64UrlTest.cs" />
<Compile Include="CompactTest.cs" />
<Compile Include="ConcatKDFTest.cs" />
<Compile Include="DetachedPayloadTest.cs" />
<Compile Include="DictionariesTest.cs" />
<Compile Include="SecurityVulnerabilitiesTest.cs" />
<Compile Include="SettingsTest.cs" />
Expand Down
106 changes: 96 additions & 10 deletions jose-jwt/JWT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public static T Headers<T>(string token, JwtSettings settings = null)
return GetSettings(settings).JsonMapper.Parse<T>(Encoding.UTF8.GetString(parts[0]));
}


/// <summary>
/// Parses signed JWT token, extracts and returns payload part as string
/// This method is NOT supported for encrypted JWT tokens.
Expand All @@ -137,6 +138,8 @@ public static string Payload(string token, JwtSettings settings = null)
/// <exception cref="JoseException">if encrypted JWT token is provided</exception>
public static byte[] PayloadBytes(string token, JwtSettings settings = null)
{
if (token == null)
throw new ArgumentNullException(nameof(token));
byte[][] parts = Compact.Parse(token);

if (parts.Length < 3)
Expand Down Expand Up @@ -311,11 +314,32 @@ public static string EncodeBytes(byte[] payload, object key, JwsAlgorithm algori


var jwtHeader = new Dictionary<string, object> { { "alg", jwtSettings.JwsHeaderValue(algorithm) } };
bool b64EncodePayload = true;

if (extraHeaders.ContainsKey("b64"))
{
var b64Header = extraHeaders["b64"];
if (b64Header != null && b64Header is bool)
{
b64EncodePayload = (bool)b64Header;
}
}
Dictionaries.Append(jwtHeader, extraHeaders);
byte[] headerBytes = Encoding.UTF8.GetBytes(jwtSettings.JsonMapper.Serialize(jwtHeader));

var bytesToSign = Encoding.UTF8.GetBytes(Compact.Serialize(headerBytes, payload));

byte[] bytesToSign;
if (b64EncodePayload)
{
bytesToSign = Encoding.UTF8.GetBytes(Compact.Serialize(headerBytes, payload));
}
else
{
var tmpBytes = Encoding.UTF8.GetBytes(Compact.Serialize(headerBytes) + ".");
bytesToSign = new byte[tmpBytes.Length + payload.Length];
System.Buffer.BlockCopy(tmpBytes, 0, bytesToSign, 0, tmpBytes.Length);
System.Buffer.BlockCopy(payload, 0, bytesToSign, tmpBytes.Length, payload.Length);
}

var jwsAlgorithm = jwtSettings.Jws(algorithm);

Expand All @@ -326,7 +350,15 @@ public static string EncodeBytes(byte[] payload, object key, JwsAlgorithm algori

byte[] signature = jwsAlgorithm.Sign(bytesToSign, key);

return Compact.Serialize(headerBytes, payload, signature);
if (b64EncodePayload)
{
return Compact.Serialize(headerBytes, payload, signature);
}
else
{
return Base64Url.Encode(headerBytes) + ".." + Base64Url.Encode(signature);
}

}

/// <summary>
Expand Down Expand Up @@ -484,26 +516,80 @@ public static T Decode<T>(string token, object key = null, JwtSettings settings
{
return GetSettings(settings).JsonMapper.Parse<T>(Decode(token, key, settings));
}
public static IDictionary<string, object> Headers(byte[] headerBytes, JwtSettings settings = null)
{
return GetSettings(settings).JsonMapper.Parse<IDictionary<string, object>>(Encoding.UTF8.GetString(headerBytes));
}

private static bool GetBase64DecodeFlag(byte[] headerBytes, JwtSettings settings = null)
{
bool result = true;
var headers = Headers(headerBytes, settings);
if (headers.ContainsKey("b64"))
{
var b64Header = headers["b64"];
if (b64Header != null && b64Header is bool)
{
result = (bool)b64Header;
}
}
return result;
}

/// <summary>
/// Decode detached payload JWS
/// </summary>
/// <param name="token">JWS token with empty payload</param>
/// <param name="payload">Raw payload to attach to the JWS to validate signature.</param>
/// <param name="key">Key for decoding suitable for JWT algorithm used, can be null.</param>
/// <param name="settings">Optional settings to override global DefaultSettings</param>
/// <returns></returns>
public static string DecodeDetached(string token, byte[] payload, object key = null, JwtSettings settings = null)
{
Ensure.IsNotEmpty(token, "Incoming token expected to be in compact serialization form, not empty, whitespace or null.");
if (token.IndexOf("..") < 0)
throw new ArgumentException("JWS token must not include payload", nameof(token));
token = token.Replace("..", "." + Base64Url.Encode(payload) + ".");
return Decode(token, key, null, null, null, settings);
}
private static byte[] DecodeBytes(string token, object key = null, JwsAlgorithm? expectedJwsAlg = null, JweAlgorithm? expectedJweAlg = null, JweEncryption? expectedJweEnc = null, JwtSettings settings = null)
{
Ensure.IsNotEmpty(token, "Incoming token expected to be in compact serialization form, not empty, whitespace or null.");

byte[][] parts = Compact.Parse(token);
if (token == null)
throw new ArgumentNullException(nameof(token));

string[] stringParts = token.Split('.');

if (parts.Length == 5) //encrypted JWT
if (stringParts.Length == 5) //encrypted JWT
{
byte[][] parts = Compact.Parse(token);
return DecryptBytes(parts, key, expectedJweAlg, expectedJweEnc, settings);
}
else
{
//signed or plain JWT
byte[] header = parts[0];
byte[] payload = parts[1];
byte[] signature = parts[2];

byte[] securedInput = Encoding.UTF8.GetBytes(Compact.Serialize(header, payload));

byte[] header = Base64Url.Decode(stringParts[0]);
byte[] payload;
byte[] signature = Base64Url.Decode(stringParts[2]);
bool base64DecodePayload = GetBase64DecodeFlag(header);

//Always base 64 decode paylod, even in b64=false detached since we have already attached the payload and encoded it
payload = Base64Url.Decode(stringParts[1]);

byte[] securedInput;
if (base64DecodePayload)
{
securedInput = Encoding.UTF8.GetBytes(Compact.Serialize(header, payload));
}
else
{
var tmpBytes = Encoding.UTF8.GetBytes(Compact.Serialize(header) + ".");
securedInput = new byte[tmpBytes.Length + payload.Length];
System.Buffer.BlockCopy(tmpBytes, 0, securedInput, 0, tmpBytes.Length);
System.Buffer.BlockCopy(payload, 0, securedInput, tmpBytes.Length, payload.Length);
}

var jwtSettings = GetSettings(settings);

var headerData = jwtSettings.JsonMapper.Parse<Dictionary<string, object>>(Encoding.UTF8.GetString(header));
Expand Down
35 changes: 31 additions & 4 deletions jose-jwt/jose-jwt.net46.project.lock.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"locked": false,
"version": 2,
"version": 3,
"targets": {
".NETFramework,Version=v4.6.1": {},
".NETFramework,Version=v4.6.1/win": {}
Expand All @@ -10,6 +9,34 @@
"": [],
".NETFramework,Version=v4.6.1": []
},
"tools": {},
"projectFileToolGroups": {}
"packageFolders": {
"C:\\Users\\chris\\.nuget\\packages\\": {}
},
"project": {
"restore": {
"projectUniqueName": "D:\\Projects\\myjose-jwt\\jose-jwt\\jose-jwt\\jose-jwt.net46.csproj",
"projectName": "jose-jwt.net46",
"projectPath": "D:\\Projects\\myjose-jwt\\jose-jwt\\jose-jwt\\jose-jwt.net46.csproj",
"projectJsonPath": "D:\\Projects\\myjose-jwt\\jose-jwt\\jose-jwt\\jose-jwt.net46.project.json",
"packagesPath": "C:\\Users\\chris\\.nuget\\packages\\",
"outputPath": "D:\\Projects\\myjose-jwt\\jose-jwt\\jose-jwt\\obj\\",
"projectStyle": "ProjectJson",
"configFilePaths": [
"C:\\Users\\chris\\AppData\\Roaming\\NuGet\\NuGet.Config",
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
],
"sources": {
"D:\\Projects\\Finapivity.PG\\localnuget": {},
"https://api.nuget.org/v3/index.json": {}
}
},
"frameworks": {
"net461": {}
},
"runtimes": {
"win": {
"#import": []
}
}
}
}