diff --git a/.openapi-generator/FILES b/.openapi-generator/FILES index cc739df5..51211f6a 100644 --- a/.openapi-generator/FILES +++ b/.openapi-generator/FILES @@ -131,7 +131,6 @@ docs/UpdateConferenceRequest.md docs/UpdateConferenceRequestStatus.md freeclimb.sln git_push.sh -src/freeclimb.Test/Api/DefaultApiTests.cs src/freeclimb.Test/Model/AccountStatusTests.cs src/freeclimb.Test/Model/AccountTypeTests.cs src/freeclimb.Test/Model/AnsweredByTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index b3bf04a3..d97ee3c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm None + + +## [4.3.0] 2023-04-03 + +### Added + +- Introduce signing secret verification class (RequestVerifier) - https://docs.freeclimb.com/docs/validating-requests-from-freeclimb#how-to-verify-requests-manually + ## [4.2.3] 2023-03-13 diff --git a/README.md b/README.md index fb3d36b8..95908097 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ FreeClimb is a cloud-based application programming interface (API) that puts the This C# SDK is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: - API version: 1.0.0 -- SDK version: 4.2.3 +- SDK version: 4.3.0 - Build package: org.openapitools.codegen.languages.CSharpNetCoreClientCodegen For more information, please visit [https://www.freeclimb.com/support/](https://www.freeclimb.com/support/) @@ -316,7 +316,7 @@ Class | Method | HTTP request | Description ## Documentation for Serialization/Deserialization for Enums -###These methods are not required unless being used for debugging/logging purposes +### These methods are not required unless being used for debugging/logging purposes - To serialize (turn value into enum), we would need to use the reflection method GetEnumByValue where you pass the enum as a type and value into the method to get the associated enum. @@ -396,4 +396,44 @@ Class | Method | HTTP request | Description } } - ``` \ No newline at end of file + ``` + + + +## Documentation for verifying request signature + +- To verify the request signature, we will need to use the verifyRequestSignature method within the Request Verifier class + + RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance) + + This is a method that you can call directly from the request verifier class, it will throw exceptions depending on whether all parts of the request signature is valid otherwise it will throw a specific error message depending on which request signature part is causing issues + + This method requires a requestBody of type string, a requestHeader of type string, a signingSecret of type string, and a tolerance value of type int + + Example code down below + + ```csharp + + using freeclimb.Utils; + using System; + + + namespace Example + { + public class verifySignatureRequestExample + { + public static void Main() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793"; + + string requestHeader = "t=1679944186,v1=c3957749baf61df4b1506802579cc69a74c77a1ae21447b930e5a704f9ec4120,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8"; + int tolerance = 5 * 60; + + RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance) + } + } + + } + ``` \ No newline at end of file diff --git a/freeclimb.sln b/freeclimb.sln index b2381362..24001613 100644 --- a/freeclimb.sln +++ b/freeclimb.sln @@ -2,7 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 2012 VisualStudioVersion = 12.0.0.0 MinimumVisualStudioVersion = 10.0.0.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "freeclimb", "src\freeclimb\freeclimb.csproj", "{6D009CFA-77F3-4A59-BABE-221167D9A270}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "freeclimb", "src\freeclimb\freeclimb.csproj", "{1FC7C4DB-B627-4A2A-9881-BC4BCB079A7A}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "freeclimb.Test", "src\freeclimb.Test\freeclimb.Test.csproj", "{19F1DEBC-DE5E-4517-8062-F000CD499087}" EndProject @@ -12,10 +12,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6D009CFA-77F3-4A59-BABE-221167D9A270}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6D009CFA-77F3-4A59-BABE-221167D9A270}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6D009CFA-77F3-4A59-BABE-221167D9A270}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6D009CFA-77F3-4A59-BABE-221167D9A270}.Release|Any CPU.Build.0 = Release|Any CPU + {1FC7C4DB-B627-4A2A-9881-BC4BCB079A7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FC7C4DB-B627-4A2A-9881-BC4BCB079A7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FC7C4DB-B627-4A2A-9881-BC4BCB079A7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FC7C4DB-B627-4A2A-9881-BC4BCB079A7A}.Release|Any CPU.Build.0 = Release|Any CPU {19F1DEBC-DE5E-4517-8062-F000CD499087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {19F1DEBC-DE5E-4517-8062-F000CD499087}.Debug|Any CPU.Build.0 = Debug|Any CPU {19F1DEBC-DE5E-4517-8062-F000CD499087}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/freeclimb.Test/Utils/RequestVerifierTests.cs b/src/freeclimb.Test/Utils/RequestVerifierTests.cs new file mode 100644 index 00000000..f42c1c96 --- /dev/null +++ b/src/freeclimb.Test/Utils/RequestVerifierTests.cs @@ -0,0 +1,155 @@ +using Xunit; + +using freeclimb.Utils; +using System; + +namespace freeclimb.Test.Utils +{ + public class RequestVerifierTests : IDisposable + { + private RequestVerifier instance; + + + public RequestVerifierTests() + { + RequestVerifier instance = new RequestVerifier(); + } + + public void Dispose() + { + // Cleanup when everything is done. + } + + /// + /// Test an instance of SignatureInformation + /// + [Fact] + public void RequestVerifierInstanceTests() + { + // TODO uncomment below to test "IsType" AccountRequest + //Assert.IsType(instance); + } + + [Fact] + public void checkRequestBodyTest() + { + string requestBody = ""; + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793"; + string requestHeader = "t=1679931346,v1=4945505e46930b6e31df721c069f10cd3a4cfb3c8e2ec67d2663fae49f95644f,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8"; + int tolerance = 5 * 60 * 1000; + Action act = () => RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance); + Exception exception = Assert.Throws(act); + Assert.Equal("Request Body cannot be empty or null", exception.Message); + } + [Fact] + public void checkRequestHeaderTest() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793"; + string requestHeader = "t=1679944186,"; + int tolerance = 5 * 60 * 1000; + Action act = () => RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance); + Exception exception = Assert.Throws(act); + Assert.Equal("Error with request header, signatures are not present", exception.Message); + } + [Fact] + public void checkRequestHeaderTest2() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793"; + string requestHeader = "v1=c3957749baf61df4b1506802579cc69a74c77a1ae21447b930e5a704f9ec4120,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8"; + int tolerance = 5 * 60 * 1000; + Action act = () => RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance); + Exception exception = Assert.Throws(act); + Assert.Equal("Error with request header, timestamp is not present", exception.Message); + } + [Fact] + public void checkRequestHeaderTest3() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793"; + string requestHeader = ""; + int tolerance = 5 * 60; + Action act = () => RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance); + Exception exception = Assert.Throws(act); + Assert.Equal("Error with request header, Request header is empty", exception.Message); + } + [Fact] + public void checkSigningSecretTest() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + string signingSecret = ""; + string requestHeader = "t=1679944186,v1=c3957749baf61df4b1506802579cc69a74c77a1ae21447b930e5a704f9ec4120,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8"; + int tolerance = 5 * 60; + Action act = () => RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance); + Exception exception = Assert.Throws(act); + Assert.Equal("Signing secret cannot be empty or null", exception.Message); + } + [Fact] + public void checkToleranceTest() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793"; + string requestHeader = "t=1679944186,v1=c3957749baf61df4b1506802579cc69a74c77a1ae21447b930e5a704f9ec4120,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8"; + int tolerance = int.MaxValue; + Action act = () => RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance); + Exception exception = Assert.Throws(act); + Assert.Equal("Tolerance value must be a positive integer", exception.Message); + } + [Fact] + public void checkToleranceTest2() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793"; + string requestHeader = "t=1679944186,v1=c3957749baf61df4b1506802579cc69a74c77a1ae21447b930e5a704f9ec4120,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8"; + int tolerance = -5; + Action act = () => RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance); + Exception exception = Assert.Throws(act); + Assert.Equal("Tolerance value must be a positive integer", exception.Message); + } + [Fact] + public void checkToleranceTest3() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793"; + string requestHeader = "t=1679944186,v1=c3957749baf61df4b1506802579cc69a74c77a1ae21447b930e5a704f9ec4120,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8"; + int tolerance = 0; + Action act = () => RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance); + Exception exception = Assert.Throws(act); + Assert.Equal("Tolerance value must be a positive integer", exception.Message); + } + [Fact] + public void verifyToleranceTest() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793"; + string requestHeader = "t=1900871395,v1=1d798c86e977ff734dec3a8b8d67fe8621dcc1df46ef4212e0bfe2e122b01bfd,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8"; + int tolerance = 5 * 60; + int currentTime = (int)((DateTimeOffset)DateTime.UtcNow).ToUnixTimeSeconds(); + Action act = () => RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance); + Exception exception = Assert.Throws(act); + Assert.Equal("Request time exceeded tolerance threshold. Request: 1900871395, CurrentTime: " + currentTime.ToString() + ", tolerance: 300", exception.Message); + } + [Fact] + public void verifySignatureTest() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793"; + string requestHeader = "t=1679944186,v1=1d798c86e977ff734dec3a8b8d67fe8621dcc1df46ef4212e0bfe2e122b01bfd,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8"; + int tolerance = 5 * 60; + Action act = () => RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance); + Exception exception = Assert.Throws(act); + Assert.Equal("Unverified signature request, If this request was unexpected, it may be from a bad actor. Please proceed with caution. If the request was exepected, please check any typos or issues with the signingSecret", exception.Message); + } + [Fact] + public void verifyRequestSignatureTest() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793"; + string requestHeader = "t=1679944186,v1=c3957749baf61df4b1506802579cc69a74c77a1ae21447b930e5a704f9ec4120,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8"; + int tolerance = 5 * 60; + var exception = Record.Exception(() => RequestVerifier.verifyRequestSignature(requestBody, requestHeader, signingSecret, tolerance)); + Assert.Null(exception); + } + } +} \ No newline at end of file diff --git a/src/freeclimb.Test/Utils/SignatureInformationTests.cs b/src/freeclimb.Test/Utils/SignatureInformationTests.cs new file mode 100644 index 00000000..9f9bc90a --- /dev/null +++ b/src/freeclimb.Test/Utils/SignatureInformationTests.cs @@ -0,0 +1,74 @@ +using Xunit; + +using freeclimb.Utils; +using System; + +namespace freeclimb.Test.Utils +{ + public class SignatureInformationTests : IDisposable + { + // TODO uncomment below to declare an instance variable for AccountRequest + private SignatureInformation instance; + + public SignatureInformationTests() + { + string requestHeader = "t=1679944186,v1=c3957749baf61df4b1506802579cc69a74c77a1ae21447b930e5a704f9ec4120,v1=1ba18712726898fbbe48cd862dd096a709f7ad761a5bab14bda9ac24d963a6a8"; + instance = new SignatureInformation(requestHeader); + } + + public void Dispose() + { + // Cleanup when everything is done. + } + + /// + /// Test an instance of SignatureInformation + /// + [Fact] + public void SignatureInformationInstanceTests() + { + // TODO uncomment below to test "IsType" AccountRequest + //Assert.IsType(instance); + } + + + /// + /// Test the method 'isRequestTimeValid' + /// + [Fact] + public void isRequestTimeValidTest() + { + //For test purposes, this relates to three days, we also want to ensure that the signature header remains the same during tests + int tolerance = 5 * 60; + Boolean isRequestTimeValid = instance.isRequestTimeValid(tolerance); + Assert.Equal(isRequestTimeValid, true); + } + [Fact] + public void isRequestTimeValidTest2() + { + //For test purposes, this relates to three days, we also want to ensure that the signature header remains the same during tests + int tolerance = 5 * 60 * 10000; + Boolean isRequestTimeValid = instance.isRequestTimeValid(tolerance); + Assert.Equal(isRequestTimeValid, false); + } + /// + /// Test the method 'isSignatureSafe' + /// + [Fact] + public void isSignatureSafeTest() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7793"; + Boolean isSignatureSafe = instance.isSignatureSafe(requestBody, signingSecret); + Assert.Equal(isSignatureSafe, true); + } + [Fact] + public void isSignatureSafeTest2() + { + string requestBody = "{\"accountId\":\"AC1334ffb694cd8d969f51cddf5f7c9b478546d50c\",\"callId\":\"CAccb0b00506553cda09b51c5477f672a49e0b2213\",\"callStatus\":\"ringing\",\"conferenceId\":null,\"direction\":\"inbound\",\"from\":\"+13121000109\",\"parentCallId\":null,\"queueId\":null,\"requestType\":\"inboundCall\",\"to\":\"+13121000096\"}"; + string signingSecret = "sigsec_ead6d3b6904196c60835d039e91b3341c77a7794"; + Boolean isSignatureSafe = instance.isSignatureSafe(requestBody, signingSecret); + Assert.Equal(isSignatureSafe, false); + } + } +} \ No newline at end of file diff --git a/src/freeclimb/Client/Configuration.cs b/src/freeclimb/Client/Configuration.cs index 25f6fa0c..61717ac4 100644 --- a/src/freeclimb/Client/Configuration.cs +++ b/src/freeclimb/Client/Configuration.cs @@ -32,7 +32,7 @@ public class Configuration : IReadableConfiguration /// Version of the package. /// /// Version of the package. - public const string Version = "4.2.3"; + public const string Version = "4.3.0"; /// /// Identifier for ISO 8601 DateTime Format @@ -107,7 +107,7 @@ public class Configuration : IReadableConfiguration public Configuration() { Proxy = null; - UserAgent = "OpenAPI-Generator/4.2.3/csharp"; + UserAgent = "OpenAPI-Generator/4.3.0/csharp"; BasePath = "https://www.freeclimb.com/apiserver"; DefaultHeaders = new ConcurrentDictionary(); ApiKey = new ConcurrentDictionary(); @@ -452,7 +452,7 @@ public static string ToDebugReport() report += " OS: " + System.Environment.OSVersion + "\n"; report += " .NET Framework Version: " + System.Environment.Version + "\n"; report += " Version of the API: 1.0.0\n"; - report += " SDK Package Version: 4.2.3\n"; + report += " SDK Package Version: 4.3.0\n"; return report; } diff --git a/src/freeclimb/Utils/RequestVerifier.cs b/src/freeclimb/Utils/RequestVerifier.cs new file mode 100644 index 00000000..f290aea6 --- /dev/null +++ b/src/freeclimb/Utils/RequestVerifier.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace freeclimb.Utils +{ + public class RequestVerifier + { + public const int DEFAULT_TOLERANCE = 5 * 60 * 1000; + public static void verifyRequestSignature(String requestBody, String requestHeader, String signingSecret, int tolerance = DEFAULT_TOLERANCE) + { + RequestVerifier verifier = new RequestVerifier(); + verifier.checkRequestBody(requestBody); + verifier.checkRequestHeader(requestHeader); + verifier.checkSigningSecret(signingSecret); + verifier.checkTolerance(tolerance); + SignatureInformation info = new SignatureInformation(requestHeader); + verifier.verifyTolerance(info, tolerance); + verifier.verifySignature(info, requestBody, signingSecret); + } + + private void checkRequestBody(String requestBody) + { + if (requestBody == "" || requestBody == null) + { + throw new Exception("Request Body cannot be empty or null"); + } + } + + private void checkRequestHeader(String requestHeader) + { + if (requestHeader == "" || requestHeader == null) + { + throw new Exception("Error with request header, Request header is empty"); + } + else if (!requestHeader.Contains("t=")) + { + throw new Exception("Error with request header, timestamp is not present"); + } + else if (!requestHeader.Contains("v1=")) + { + throw new Exception("Error with request header, signatures are not present"); + } + } + + private void checkSigningSecret(String signingSecret) + { + if (signingSecret == "" || signingSecret == null) + { + throw new Exception("Signing secret cannot be empty or null"); + } + } + private void checkTolerance(int tolerance) + { + if (tolerance <= 0 || tolerance >= int.MaxValue) + { + throw new Exception("Tolerance value must be a positive integer"); + } + } + + private void verifyTolerance(SignatureInformation info, int tolerance) + { + int currentTime = info.getCurrentUnixTime(); + if (!info.isRequestTimeValid(tolerance)) + { + throw new Exception(String.Format("Request time exceeded tolerance threshold. Request: {0}, CurrentTime: {1}, tolerance: {2}", info.requestTimestamp, currentTime, tolerance)); + } + } + private void verifySignature(SignatureInformation info, String requestBody, String signingSecret) + { + if (!info.isSignatureSafe(requestBody, signingSecret)) + { + throw new Exception("Unverified signature request, If this request was unexpected, it may be from a bad actor. Please proceed with caution. If the request was exepected, please check any typos or issues with the signingSecret"); + } + } + } +} \ No newline at end of file diff --git a/src/freeclimb/Utils/SignatureInformation.cs b/src/freeclimb/Utils/SignatureInformation.cs new file mode 100644 index 00000000..b5236a3c --- /dev/null +++ b/src/freeclimb/Utils/SignatureInformation.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace freeclimb.Utils +{ + /// + /// This is the Signature Information class, this parses and validates the signature information class + /// + public class SignatureInformation + { + public int requestTimestamp; + public List signatures; + /// + /// This is the Signature Information constructor, this takes the requestHeader and parses it into the attribute values + /// + /// + public SignatureInformation(string requestHeader) + { + List constructorSignatures = new List(); + string[] signatureHeaders = requestHeader.Split(','); + foreach (var signatureHeader in signatureHeaders) + { + var header = signatureHeader.Split('=')[0]; + var value = signatureHeader.Split('=')[1]; + if (header == "t") + { + requestTimestamp = Int32.Parse(value); + } + else if (header == "v1") + { + constructorSignatures.Add(value); + } + } + signatures = constructorSignatures; + } + /// + /// This is the timestamp validity function, this checks if the request timestamp is valid + /// + /// + /// true or false if timestamp is valid or invalid based on condition + public Boolean isRequestTimeValid(int tolerance) + { + var currentUnixTimestamp = getCurrentUnixTime(); + return (requestTimestamp + tolerance) < currentUnixTimestamp; + } + /// + /// This is the signature validity function, this checks if the request timestamp is valid + /// + /// + /// + /// true or false if timestamp is valid or invalid based on condition + public Boolean isSignatureSafe(String requestBody, String signingSecret) + { + string hashValue = computeHash(requestBody, signingSecret); + return signatures.Contains(hashValue); + } + + private String computeHash(String requestBody, String signingSecret) + { + string hashSeedString = requestTimestamp + "." + requestBody; + byte[] hashSeed = Encoding.ASCII.GetBytes(hashSeedString); + byte[] signingSecretBytes = Encoding.ASCII.GetBytes(signingSecret); + HMACSHA256 hmac = new HMACSHA256(signingSecretBytes); + byte[] hashValue = hmac.ComputeHash(hashSeed); + return BitConverter.ToString(hashValue).Replace("-", "").ToLower(); + } + + public int getCurrentUnixTime() + { + return (int)((DateTimeOffset)DateTime.UtcNow).ToUnixTimeSeconds(); + } + } +} diff --git a/src/freeclimb/freeclimb.csproj b/src/freeclimb/freeclimb.csproj index 6b11a129..b62ea34e 100644 --- a/src/freeclimb/freeclimb.csproj +++ b/src/freeclimb/freeclimb.csproj @@ -12,7 +12,7 @@ A library generated from a OpenAPI doc No Copyright freeclimb - 4.2.3 + 4.3.0 bin\$(Configuration)\$(TargetFramework)\freeclimb.xml https://github.com/freeclimbapi/csharp-sdk.git git