Skip to content

Commit

Permalink
Merge pull request #40 from QuantozTechnology/nounce-security
Browse files Browse the repository at this point in the history
Implemented Signature Verification Middleware
  • Loading branch information
PJvGrol authored Nov 29, 2023
2 parents 6b74c42 + cb8c855 commit a4265a6
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 3 deletions.
17 changes: 16 additions & 1 deletion QBSApplication.sln
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution-items", "solution-items", "{13E6C59E-0DF1-4F66-B9D2-E62E2CB8F061}"
ProjectSection(SolutionItems) = preProject
.gitlab-ci.yml = .gitlab-ci.yml
generate_migration_scripts.ps1 = generate_migration_scripts.ps1
init_local.ps1 = init_local.ps1
nuget.config = nuget.config
generate_migration_scripts.ps1 = generate_migration_scripts.ps1
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "backend", "backend", "{CDE75D4E-65F6-4582-AE10-F835B1362A9F}"
Expand Down Expand Up @@ -57,6 +57,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1B557F67
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SigningService.API.Tests", "backend\signing-service\tests\SigningService.API.Tests\SigningService.API.Tests.csproj", "{C3241342-A6BA-45A0-A151-FA6C09E18626}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core.APITests", "backend\core\tests\Core.APITests\Core.APITests.csproj", "{55289D63-5BF0-4106-B142-C8729A1BBF43}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -235,6 +237,18 @@ Global
{C3241342-A6BA-45A0-A151-FA6C09E18626}.Release|iPhone.Build.0 = Release|Any CPU
{C3241342-A6BA-45A0-A151-FA6C09E18626}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{C3241342-A6BA-45A0-A151-FA6C09E18626}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{55289D63-5BF0-4106-B142-C8729A1BBF43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{55289D63-5BF0-4106-B142-C8729A1BBF43}.Debug|Any CPU.Build.0 = Debug|Any CPU
{55289D63-5BF0-4106-B142-C8729A1BBF43}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{55289D63-5BF0-4106-B142-C8729A1BBF43}.Debug|iPhone.Build.0 = Debug|Any CPU
{55289D63-5BF0-4106-B142-C8729A1BBF43}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{55289D63-5BF0-4106-B142-C8729A1BBF43}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{55289D63-5BF0-4106-B142-C8729A1BBF43}.Release|Any CPU.ActiveCfg = Release|Any CPU
{55289D63-5BF0-4106-B142-C8729A1BBF43}.Release|Any CPU.Build.0 = Release|Any CPU
{55289D63-5BF0-4106-B142-C8729A1BBF43}.Release|iPhone.ActiveCfg = Release|Any CPU
{55289D63-5BF0-4106-B142-C8729A1BBF43}.Release|iPhone.Build.0 = Release|Any CPU
{55289D63-5BF0-4106-B142-C8729A1BBF43}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{55289D63-5BF0-4106-B142-C8729A1BBF43}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -262,6 +276,7 @@ Global
{4C285867-547A-463E-B4A5-72C94D0F8E44} = {B35D45F9-B8ED-4750-A672-56B6FF15AA8E}
{1B557F67-5477-42A7-9CBA-E07462FB7946} = {B35D45F9-B8ED-4750-A672-56B6FF15AA8E}
{C3241342-A6BA-45A0-A151-FA6C09E18626} = {1B557F67-5477-42A7-9CBA-E07462FB7946}
{55289D63-5BF0-4106-B142-C8729A1BBF43} = {53D4149D-89CD-4A60-8355-EA93F07714E6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4CF9DA35-A8B6-44C1-ADE3-5266BA68DB61}
Expand Down
1 change: 1 addition & 0 deletions backend/core/src/Core.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

app.ConfigureCustomAuthenticationMiddleware();
app.ConfigureCustomExceptionMiddleware();
app.ConfigureSignatureVerificationMiddleware();

app.UseAuthentication();
app.UseRouting();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed
// under the Apache License, Version 2.0. See the NOTICE file at the root
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

using Core.Domain.Exceptions;
using Core.Presentation.Models;
using Newtonsoft.Json.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using static Core.Domain.Constants;

namespace Core.API.ResponseHandling
{
public class SignatureVerificationMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SignatureVerificationMiddleware> _logger;

public SignatureVerificationMiddleware(
RequestDelegate next,
ILogger<SignatureVerificationMiddleware> logger)
{
_next = next;
_logger = logger;
}

public async Task Invoke(HttpContext context)
{
try
{
// Retrieve headers from the request
string? signatureHeader = context.Request.Headers["x-signature"];
string? payloadHeader = context.Request.Headers["x-payload"];

// Retrieve method from the request
var method = context.Request.Method;

if (string.IsNullOrWhiteSpace(payloadHeader))
{
_logger.LogError("Missing payload header");
var customErrors = new CustomErrors(new CustomError("Forbidden", "Missing Header", "x-payload"));
await WriteCustomErrors(context.Response, customErrors, (int)HttpStatusCode.Forbidden);
return;
}

if (string.IsNullOrWhiteSpace(signatureHeader))
{
_logger.LogError("Missing x-signature header");
var customErrors = new CustomErrors(new CustomError("Forbidden", "Missing Header", "x-signature"));
await WriteCustomErrors(context.Response, customErrors, (int)HttpStatusCode.Forbidden);
return;
}

byte[] payloadBytes = Encoding.UTF8.GetBytes(payloadHeader);

JObject payloadJson = JObject.Parse(payloadHeader);

string? publicKey = null;
string? postPayload = null;

if (method == "POST" || method == "PUT")
{
// Check if the "PostPayload" property is present
if (payloadJson.TryGetValue(SignaturePayload.PostPayload, out var post))
{
postPayload = (string?)post;
}
else
{
_logger.LogError("Missing postPayload header");
var customErrors = new CustomErrors(new CustomError("Forbidden", "Missing Header", "postPayload"));
await WriteCustomErrors(context.Response, customErrors, (int)HttpStatusCode.Forbidden);
return;
}
}

// Check if the "publicKey" property is present
if (payloadJson.TryGetValue(SignaturePayload.PublicKey, out var pubKey))
{
publicKey = (string?)pubKey;
}
else
{
_logger.LogError("Missing publicKey header");
var customErrors = new CustomErrors(new CustomError("Forbidden", "Missing Header", "publicKey"));
await WriteCustomErrors(context.Response, customErrors, (int)HttpStatusCode.Forbidden);
return;
}

// Get the current Unix UTC timestamp (rounded to 30 seconds)
long currentTimestamp = (long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
currentTimestamp = (currentTimestamp / 30) * 30; // Round to the nearest 30 seconds

// Decode the signature header from Base64
byte[]? signatureBytes = Convert.FromBase64String(signatureHeader);

long timestamp = 0;

// Check if the "timestamp" property is present
if (payloadJson.TryGetValue(SignaturePayload.Timestamp, out var timestampToken))
{
// Extract the timestamp value
timestamp = (long)timestampToken;
}
else
{
_logger.LogError("Missing timestamp in header");
var customErrors = new CustomErrors(new CustomError("Forbidden", "Missing Header", "timestamp"));
await WriteCustomErrors(context.Response, customErrors, (int)HttpStatusCode.Forbidden);
return;
}

bool isCurrentTime = timestamp == currentTimestamp;

long allowedDifference = 30; // 30 seconds
bool isWithin30Seconds = Math.Abs(currentTimestamp - timestamp) <= allowedDifference;

if (isCurrentTime || isWithin30Seconds)
{
if (VerifySignature(publicKey!, payloadBytes, signatureBytes))
{
await _next(context); // Signature is valid, continue with the request
}
else
{
_logger.LogError("Invalid signature");
var customErrors = new CustomErrors(new CustomError("Forbidden", "Invalid signature", "x-signature"));
await WriteCustomErrors(context.Response, customErrors, (int)HttpStatusCode.Forbidden);
}
}
else
{
_logger.LogError("Invalid signature");
var customErrors = new CustomErrors(new CustomError("Forbidden", "Invalid signature", "x-signature"));
await WriteCustomErrors(context.Response, customErrors, (int)HttpStatusCode.Forbidden);
}
}
catch (Exception ex)
{
_logger.LogError("Unknown exception thrown: {message}", ex.Message);
var customErrors = new CustomErrors(new CustomError("Forbidden", ex.Message, ex.Source!));
await WriteCustomErrors(context.Response, customErrors, (int)HttpStatusCode.Forbidden);
}
}

private static async Task WriteCustomErrors(HttpResponse httpResponse, CustomErrors customErrors, int statusCode)
{
httpResponse.StatusCode = statusCode;
httpResponse.ContentType = "application/json";

var response = CustomErrorsResponse.FromCustomErrors(customErrors);
var json = System.Text.Json.JsonSerializer.Serialize(response);
await httpResponse.WriteAsync(json);
}

private static bool VerifySignature(string publicKey, byte[] payload, byte[] signature)
{
try
{
using (RSA rsa = RSA.Create())
{
// Import the public key (assuming it's in PEM format)
rsa.ImportFromPem(publicKey);

// Verify the signature using the SHA256 algorithm and PKCS1 padding
var isValidSignature = rsa.VerifyData(payload, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

return isValidSignature;
}
}
catch (CryptographicException)
{
// Signature verification failed
return false;
}
}
}

public static class SignatureVerificationMiddlewareExtensions
{
public static void ConfigureSignatureVerificationMiddleware(this IApplicationBuilder app)
{
app.UseMiddleware<SignatureVerificationMiddleware>();
}
}
}
7 changes: 7 additions & 0 deletions backend/core/src/Core.Domain/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,12 @@ public static class MerchantCustomerPersonalData
public const string ContactPersonFullName = "ContactPersonFullName";
public const string CountryOfRegistration = "CountryOfRegistration";
}

public static class SignaturePayload
{
public const string PublicKey = "publicKey";
public const string Timestamp = "timestamp";
public const string PostPayload = "postPayload";
}
}
}
2 changes: 0 additions & 2 deletions backend/core/src/Core.Presentation/Models/CustomResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
// under the Apache License, Version 2.0. See the NOTICE file at the root
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

using System.Text.Json.Serialization;

namespace Core.Presentation.Models
{
public class CustomResponse<T>
Expand Down
25 changes: 25 additions & 0 deletions backend/core/tests/Core.APITests/Core.APITests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>

</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Core.API\Core.API.csproj" />
</ItemGroup>

</Project>
5 changes: 5 additions & 0 deletions backend/core/tests/Core.APITests/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright 2023 Quantoz Technology B.V. and contributors. Licensed
// under the Apache License, Version 2.0. See the NOTICE file at the root
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0

global using Microsoft.VisualStudio.TestTools.UnitTesting;

0 comments on commit a4265a6

Please sign in to comment.