Last active
July 25, 2023 12:34
-
-
Save ddjerqq/8772efba8f1dedb9216ed58512a6e463 to your computer and use it in GitHub Desktop.
digital signature for web API security
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Security.Claims; | |
using System.Text; | |
using System.IdentityModel.Tokens.Jwt; | |
using Microsoft.IdentityModel.Tokens; | |
namespace Diamond; | |
public static class JwtTokenManager | |
{ | |
private static readonly string Secret; | |
static JwtTokenManager() => Secret = Guid.NewGuid().ToString(); | |
private static SymmetricSecurityKey Key => | |
new(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("DIAMOND__KEY") ?? Secret)); | |
private static SigningCredentials Creds => new(Key, SecurityAlgorithms.HmacSha256); | |
private static JwtSecurityTokenHandler Handler => new(); | |
public static JwtSecurityToken GenerateToken(ClaimsPrincipal principal, TimeSpan? expires = null) | |
{ | |
expires ??= TimeSpan.FromDays(1); | |
return new( | |
issuer: "Diamond", | |
audience: "Diamond", | |
claims: principal.Claims, | |
expires: DateTime.Now.Add(expires.Value), | |
signingCredentials: Creds | |
); | |
} | |
public static string WriteTokenToString(JwtSecurityToken token) => Handler.WriteToken(token); | |
public static bool TryExtractPrincipal(string token, out ClaimsPrincipal principal) | |
{ | |
principal = null!; | |
var validationParameters = new TokenValidationParameters | |
{ | |
ValidateIssuer = true, | |
ValidIssuer = "Diamond", | |
ValidateAudience = true, | |
ValidAudience = "Diamond", | |
ValidateLifetime = true, | |
ValidateIssuerSigningKey = true, | |
IssuerSigningKey = Key | |
}; | |
try | |
{ | |
principal = Handler.ValidateToken(token, validationParameters, out _); | |
return principal is not null; | |
} | |
catch | |
{ | |
return false; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.IdentityModel.Tokens.Jwt; | |
using System.Security.Claims; | |
using System.Security.Cryptography; | |
using System.Text; | |
namespace Diamond; | |
/// <summary> | |
/// Salt - random Guid md5 hashed | |
/// HashHead - random int in range: [0x1000, 0xffff] | |
/// HashTail - random int in range: [0x10000000, 0x7fffffff] but in hex | |
/// ChecksumStart - random byte full range | |
/// ChecksumIndexes - 32 uints all in range [0, 64) | |
/// HashFormat - {HashHead}:{Sha1 of payload}:{checksum hex}:{HashTail} | |
/// AppToken - two random longs bit shifted, converted to hex, and combined | |
/// | |
/// the payload is in the format: var payload {Salt}{timestampMs}{extractedUri}{userId} | |
/// </summary> | |
public sealed class Rules | |
{ | |
private readonly Guid _salt; | |
private readonly int _hashHead; | |
private readonly int _hashTail; | |
private readonly byte _checksumStart; | |
private readonly int[] _checksumIndexes; | |
private readonly UInt128 _appToken; | |
private string HashFormat => $"{_hashHead:x4}:{{0}}:{{1:D3}}:{_hashTail:x8}"; | |
private Rules(Guid salt, int hashHead, int hashTail, byte checksumStart, IEnumerable<int> checksumIndexes, | |
UInt128 appToken) | |
{ | |
_salt = salt; | |
_hashHead = hashHead; | |
_hashTail = hashTail; | |
_checksumIndexes = checksumIndexes.ToArray(); | |
_checksumStart = checksumStart; | |
_appToken = appToken; | |
} | |
public static Rules Random() | |
{ | |
var salt = Guid.NewGuid(); | |
int hashHead = RandomNumberGenerator.GetInt32(0x0, 0xffff); | |
int hashTail = RandomNumberGenerator.GetInt32(0x0, 0x7fffffff); | |
byte checksumStart = RandomNumberGenerator.GetBytes(1).First(); | |
var checksumIndexes = Enumerable | |
.Range(0, 32) | |
.Select(_ => RandomNumberGenerator.GetInt32(0, 40)); | |
var appToken = new UInt128( | |
(ulong) System.Random.Shared.NextInt64(), | |
(ulong) System.Random.Shared.NextInt64()); | |
return new Rules(salt, hashHead, hashTail, checksumStart, checksumIndexes, appToken); | |
} | |
private string SignUri(string uri, long userId, DateTime timestamp) | |
{ | |
var parsedUri = new Uri(uri); | |
var parsedUrl = $"{parsedUri.AbsolutePath}{parsedUri.Query}"; | |
var payload = $"{_salt}:{timestamp:O}:{parsedUrl}:{userId}"; | |
byte[] hash = SHA1.HashData(Encoding.UTF8.GetBytes(payload)); | |
string digest = string.Concat(hash.Select(b => b.ToString("x2"))); | |
byte[] digestBytes = Encoding.UTF8.GetBytes(digest); | |
var sum = (int) _checksumIndexes.Aggregate(_checksumStart, (sum, idx) => (byte) (sum + digestBytes[idx])); | |
sum = Math.Abs(sum); | |
return string.Format(HashFormat, digest, sum); | |
} | |
public JwtSecurityToken GenerateToken(string uri, long userId, DateTime timestamp) | |
{ | |
string signature = SignUri(uri, userId, timestamp); | |
var claims = new List<Claim> | |
{ | |
new("uri", uri), | |
new("app-token", _appToken.ToString("x16")), | |
new("user-id", userId.ToString()), | |
new("timestamp", $"{timestamp:O}"), | |
new("signature", signature) | |
}; | |
var identity = new ClaimsIdentity(claims, "Diamond"); | |
var principal = new ClaimsPrincipal(identity); | |
JwtSecurityToken token = JwtTokenManager.GenerateToken(principal); | |
return token; | |
} | |
public bool VerifyToken(string token) | |
{ | |
if (!JwtTokenManager.TryExtractPrincipal(token, out ClaimsPrincipal principal)) return false; | |
var claims = principal.Claims.ToList(); | |
string? uri = claims.FirstOrDefault(x => x.Type == "uri")?.Value; | |
string? appToken = claims.FirstOrDefault(x => x.Type == "app-token")?.Value; | |
string? userId = claims.FirstOrDefault(x => x.Type == "user-id")?.Value; | |
string? timestamp = claims.FirstOrDefault(x => x.Type == "timestamp")?.Value; | |
string? signature = claims.FirstOrDefault(x => x.Type == "signature")?.Value; | |
var conditions = new List<Func<bool>> | |
{ | |
() => uri is not null, | |
() => appToken == _appToken.ToString("x16"), | |
() => long.TryParse(userId, out _), | |
() => DateTime.TryParse(timestamp, out _), | |
() => !string.IsNullOrEmpty(signature), | |
() => signature == SignUri(uri!, long.Parse(userId!), DateTime.Parse(timestamp!).ToUniversalTime()) | |
}; | |
// we want to stop at the first error. | |
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator | |
foreach (var condition in conditions) | |
if (!condition()) return false; | |
return true; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment