Skip to content

Commit

Permalink
feat: Revoked Reason
Browse files Browse the repository at this point in the history
Revoked Reason
  • Loading branch information
brunobritodev authored Apr 14, 2023
2 parents 696c579 + 8a2bf80 commit c7f10bc
Show file tree
Hide file tree
Showing 10 changed files with 80 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal class DataProtectionStore : IJsonWebKeyStore
private IXmlRepository KeyRepository => _keyManagementOptions.Value.XmlRepository ?? GetFallbackKeyRepositoryEncryptorPair();

private const string Name = "NetDevPackSecurityJwt";
internal const string DefaultRevocationReason = "Revoked";

public DataProtectionStore(
ILoggerFactory loggerFactory,
Expand Down Expand Up @@ -98,7 +99,7 @@ private IReadOnlyCollection<KeyMaterial> GetKeys()
{
var allElements = KeyRepository.GetAllElements();
var keys = new List<KeyMaterial>();
var revokedKeys = new List<string>();
var revokedKeys = new List<RevokedKeyInfo>();
foreach (var element in allElements)
{
if (element.Name == Name)
Expand All @@ -124,21 +125,22 @@ private IReadOnlyCollection<KeyMaterial> GetKeys()
if (key.IsExpired(_options.Value.DaysUntilExpire))
{
//Revoke(key).Wait();
revokedKeys.Add(key.Id.ToString());
revokedKeys.Add(new RevokedKeyInfo(key.Id.ToString()));
}

keys.Add(key);
}
else if (element.Name == RevocationElementName)
{
var keyIdAsString = (string)element.Element(Name)!.Attribute(IdAttributeName)!;
revokedKeys.Add(keyIdAsString);
var reason = (string)element.Element(ReasonElementName);
revokedKeys.Add(new RevokedKeyInfo(keyIdAsString, reason));
}
}

foreach (var revokedKey in revokedKeys)
{
keys.FirstOrDefault(a => a.Id.ToString().Equals(revokedKey))?.Revoke();
keys.FirstOrDefault(a => a.Id.ToString().Equals(revokedKey.Id))?.Revoke(revokedKey.RevokedReason);
}
return keys.ToList();
}
Expand Down Expand Up @@ -181,7 +183,7 @@ public async Task Clear()
}


public async Task Revoke(KeyMaterial keyMaterial)
public async Task Revoke(KeyMaterial keyMaterial, string reason = null)
{
if(keyMaterial == null)
return;
Expand All @@ -193,12 +195,13 @@ public async Task Revoke(KeyMaterial keyMaterial)
return;

keyMaterial.Revoke();
var revokeReason = reason ?? DefaultRevocationReason;
var revocationElement = new XElement(RevocationElementName,
new XAttribute(VersionAttributeName, 1),
new XElement(RevocationDateElementName, DateTimeOffset.UtcNow),
new XElement(Name,
new XAttribute(IdAttributeName, keyMaterial.Id)),
new XElement(ReasonElementName, "Revoked"));
new XElement(ReasonElementName, revokeReason));


// Persist it to the underlying repository and trigger the cancellation token
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace NetDevPack.Security.Jwt.Core.DefaultStore;

internal class InMemoryStore : IJsonWebKeyStore
{

internal const string DefaultRevocationReason = "Revoked";
private static readonly List<KeyMaterial> _store = new();
private readonly SemaphoreSlim _slim = new(1);
public Task Store(KeyMaterial keyMaterial)
Expand All @@ -23,12 +23,12 @@ public Task<KeyMaterial> GetCurrent()
return Task.FromResult(_store.OrderByDescending(s => s.CreationDate).FirstOrDefault());
}

public async Task Revoke(KeyMaterial keyMaterial)
public async Task Revoke(KeyMaterial keyMaterial, string reason = null)
{
if(keyMaterial == null)
return;

keyMaterial.Revoke();
var revokeReason = reason ?? DefaultRevocationReason;
keyMaterial.Revoke(revokeReason);
var oldOne = _store.Find(f => f.Id == keyMaterial.Id);
if (oldOne != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public interface IJsonWebKeyStore
{
Task Store(KeyMaterial keyMaterial);
Task<KeyMaterial> GetCurrent();
Task Revoke(KeyMaterial keyMaterial);
Task Revoke(KeyMaterial keyMaterial, string reason=default);
Task<ReadOnlyCollection<KeyMaterial>> GetLastKeys(int quantity);
Task<KeyMaterial> Get(string keyId);
Task Clear();
Expand Down
6 changes: 5 additions & 1 deletion src/NetDevPack.Security.Jwt.Core/Model/Key.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public KeyMaterial(CryptographicKey cryptographicKey)
public string Type { get; set; }
public string Parameters { get; set; }
public bool IsRevoked { get; set; }
public string? RevokedReason { get; set; }
public DateTime CreationDate { get; set; }
public DateTime? ExpiredAt { get; set; }

Expand All @@ -30,15 +31,18 @@ public JsonWebKey GetSecurityKey()
return JsonSerializer.Deserialize<JsonWebKey>(Parameters);
}

public void Revoke()
public void Revoke(string reason=default)
{
var jsonWebKey = GetSecurityKey();
var publicWebKey = PublicJsonWebKey.FromJwk(jsonWebKey);
ExpiredAt = DateTime.UtcNow;
IsRevoked = true;
RevokedReason = reason;
Parameters = JsonSerializer.Serialize(publicWebKey.ToNativeJwk(), new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault });
}



public bool IsExpired(int valueDaysUntilExpire)
{
return CreationDate.AddDays(valueDaysUntilExpire) < DateTime.UtcNow.Date;
Expand Down
7 changes: 7 additions & 0 deletions src/NetDevPack.Security.Jwt.Core/Model/RevokedKeyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace NetDevPack.Security.Jwt.Core.Model;

record RevokedKeyInfo(string Id, string? RevokedReason=default)
{
public string Id { get; } = Id;
public string? RevokedReason { get; } = RevokedReason;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>NetDevPack.Security.Jwt.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<Target Name="CopyHook" AfterTargets="AfterBuild" Condition="'$(Configuration)' == 'Debug'">
<ItemGroup>
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public bool NeedsUpdate()
return !File.Exists(GetCurrentFile()) || File.GetCreationTimeUtc(GetCurrentFile()).AddDays(_options.Value.DaysUntilExpire) < DateTime.UtcNow.Date;
}

public async Task Revoke(KeyMaterial? securityKeyWithPrivate)
public async Task Revoke(KeyMaterial securityKeyWithPrivate, string reason = null)
{
if (securityKeyWithPrivate == null)
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
using NetDevPack.Security.Jwt.Tests.Warmups;
using System.Linq;
using Microsoft.IdentityModel.Tokens;
using NetDevPack.Security.Jwt.Tests.Warmups;
using System.Threading.Tasks;
using FluentAssertions;
using NetDevPack.Security.Jwt.Core.DefaultStore;
using NetDevPack.Security.Jwt.Core.Jwa;
using NetDevPack.Security.Jwt.Core.Model;
using Xunit;

namespace NetDevPack.Security.Jwt.Tests.StoreTests;
Expand All @@ -9,4 +16,6 @@ public class DataProtectionStoreTest : GenericStoreServiceTest<WarmupDataProtect
public DataProtectionStoreTest(WarmupDataProtectionStore unifiedContext) : base(unifiedContext)
{
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using NetDevPack.Security.Jwt.Core;
using NetDevPack.Security.Jwt.Core.DefaultStore;
using NetDevPack.Security.Jwt.Core.Interfaces;
using NetDevPack.Security.Jwt.Core.Jwa;
using NetDevPack.Security.Jwt.Core.Model;
Expand All @@ -21,7 +22,7 @@ public abstract class GenericStoreServiceTest<TWarmup> : IClassFixture<TWarmup>
where TWarmup : class, IWarmupTest
{
private static SemaphoreSlim TestSync = new(1);
private readonly IJsonWebKeyStore _store;
protected readonly IJsonWebKeyStore _store;
private readonly IOptions<JwtOptions> _options;
public TWarmup WarmupData { get; }

Expand Down Expand Up @@ -498,6 +499,42 @@ public async Task ShouldGenerateAndValidateJweAndJws()

}

[Fact]
public async Task Should_Read_Default_Revocation_Reason()
{
var keyMaterial = await StoreRandomKey();
/*Revoke*/
await _store.Revoke(keyMaterial);
await CheckRevocationReasonIsStored(keyMaterial.KeyId, DataProtectionStore.DefaultRevocationReason);
}

[Theory]
[InlineData("ManualRevocation")]
[InlineData("StolenKey")]
public async Task Should_Read_NonDefault_Revocation_Reason(string reason)
{
var keyMaterial = await StoreRandomKey();
/*Revoke with reason*/
await _store.Revoke(keyMaterial, reason);
await CheckRevocationReasonIsStored(keyMaterial.KeyId, reason);
}

private async Task CheckRevocationReasonIsStored(string keyId, string revocationReason)
{
var dbKey = (await _store.GetLastKeys(5)).First(w => w.KeyId == keyId);
dbKey.Type.Should().NotBeNullOrEmpty();
dbKey.RevokedReason.Should().BeEquivalentTo(revocationReason);
}

private async Task<KeyMaterial> StoreRandomKey()
{
var alg = Algorithm.Create(DigitalSignaturesAlgorithm.RsaSha512);
var key = new CryptographicKey(alg);
var keyMaterial = new KeyMaterial(key);
await _store.Store(keyMaterial);
return keyMaterial;
}



private Task GenerateKey()
Expand Down

0 comments on commit c7f10bc

Please sign in to comment.