diff --git a/.gitignore b/.gitignore index 13e13dd435fbfe8febcdb01de8f2410b7c9f92be..d3d3e50d719fc2a4b2e4fcb1de3ad08509bc147d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ **/**notes.md private_notes/ +IntegrationTests/certificates ### VisualStudioCode template .vscode/* diff --git a/BasicUnitTest/SecurityEventTokenTests.cs b/BasicUnitTest/SecurityEventTokenTests.cs index ddddef9579bd34c02d80b3efa899cc57c4aad5a1..54bec9717b7d03ced31f7283256cec1ebb6a2d95 100644 --- a/BasicUnitTest/SecurityEventTokenTests.cs +++ b/BasicUnitTest/SecurityEventTokenTests.cs @@ -13,18 +13,28 @@ namespace BasicUnitTest; [TestFixture] public class SecurityEventTokenTests { + + private const string rejectSubmission = + SecurityEventToken.RejectSubmissionSchema; + + private FitEncryption _encryption = null!; + + private const string acceptSubmission = + SecurityEventToken.AcceptSubmissionSchema; + + [SetUp] public void Setup() { var container = Container.Create(); _encryption = new FitEncryption(container.Resolve<KeySet>(), null); } - private FitEncryption _encryption = null!; + [Test] public void CreateJwt_AcceptSubmission() { var token = _encryption.CreateAcceptSecurityEventToken(new SubmissionForPickupDto { - SubmissionId = Guid.NewGuid().ToString(), CaseId = Guid.NewGuid().ToString(), + Id = Guid.NewGuid().ToString(), CaseId = Guid.NewGuid().ToString(), DestinationId = Guid.NewGuid().ToString() }); Console.WriteLine(token); diff --git a/BasicUnitTest/SenderTests.cs b/BasicUnitTest/SenderTests.cs index ac5740fc719002b0b5c48af1a6dbb0c2fdff49fd..e824755cf22491380dab5cd161d3f1e15f6e3436 100644 --- a/BasicUnitTest/SenderTests.cs +++ b/BasicUnitTest/SenderTests.cs @@ -12,13 +12,19 @@ namespace BasicUnitTest; public class SenderTests { private IContainer _container = null!; - protected string clientId = "20175c2b-c4dd-4a01-99b1-3a08436881a1"; - protected string clientSecret = "KV2qd7qc5n-xESB6dvfrTlMDx2BWHJd5hXJ6pKKnbEQ"; + protected string clientId = null!; + protected string clientSecret = null!; private ILogger? logger; + private string leikaKey = null!; [OneTimeSetUp] public void OneTimeSetup() { _container = Container.Create(); + logger = _container.Resolve<ILogger>(); + var settings = _container.Resolve<MockSettings>(); + clientId = settings.SenderClientId; + clientSecret = settings.SenderClientSecret; + leikaKey = settings.LeikaKey; } [SetUp] @@ -46,7 +52,7 @@ public class SenderTests { clientId, clientSecret, Container.Create()) .WithDestination(Guid.NewGuid().ToString()) - .WithServiceType("", "urn:de:fim:leika:leistung:99400048079000") + .WithServiceType("", leikaKey) .WithAttachments(Array.Empty<Attachment>()) .WithData("") .Submit(); @@ -58,7 +64,7 @@ public class SenderTests { clientId, clientSecret, Container.Create()) .WithDestination(Guid.NewGuid().ToString()) - .WithServiceType("", "urn:de:fim:leika:leistung:99400048079000") + .WithServiceType("", leikaKey) .WithAttachments(Array.Empty<Attachment>()) .Submit(); } @@ -67,7 +73,7 @@ public class SenderTests { public void VerifyMetaData_ValidData_Fine() { // Arrange var submission = new Submission(); - submission.ServiceType.Identifier = "urn:de:fim:leika:leistung:99400048079000"; + submission.ServiceType.Identifier = leikaKey; // Act var metadata = Sender.CreateMetadata(submission); @@ -82,7 +88,6 @@ public class SenderTests { public void VerifyMetaData_MissingLeikaKey_ThorwsAnError() { // Arrange var submission = new Submission(); - //submission.ServiceType.Identifier = "urn:de:fim:leika:leistung:99400048079000"; // Act var metadata = Sender.CreateMetadata(submission); diff --git a/DemoRunner/DemoRunner.csproj b/DemoRunner/DemoRunner.csproj index 1e670320d5da6e923c6e02273e721b38128fafd4..ac77d58532b65d9b7ac13e46f81cec6b37a0afe4 100644 --- a/DemoRunner/DemoRunner.csproj +++ b/DemoRunner/DemoRunner.csproj @@ -10,11 +10,11 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="FitConnect" Version="0.0.1-beta.2"/> - <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0"/> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0"/> - <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0"/> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0"/> + <PackageReference Include="FitConnect" Version="0.0.1-beta.2" /> + <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" /> </ItemGroup> <ItemGroup> @@ -42,7 +42,7 @@ </ItemGroup> <ItemGroup> - <ProjectReference Include="..\FitConnect\FitConnect.csproj"/> + <ProjectReference Include="..\FitConnect\FitConnect.csproj" /> </ItemGroup> </Project> diff --git a/DemoRunner/SubscriberDemo.cs b/DemoRunner/SubscriberDemo.cs index d08b9f6e51ed37bf55be0d21a7a410b1ca101399..5a8c96bd75a1f464731e43a92f13e9932fa9f6ee 100644 --- a/DemoRunner/SubscriberDemo.cs +++ b/DemoRunner/SubscriberDemo.cs @@ -33,7 +33,7 @@ public static class SubscriberDemo { foreach (var submission in submissions) try { var subscriberWithSubmission = - subscriber.RequestSubmission(submission.SubmissionId); + subscriber.RequestSubmission(submission.Id); var attachments = subscriberWithSubmission .GetAttachments(); diff --git a/DemoRunner/appsettings.json.template b/DemoRunner/appsettings.json.template index a3c1adeacf97b9b3d0bb8752ccd19934ef7d8cf6..22d1b2e054b134a606deab61659be4b4a24f0f9c 100644 --- a/DemoRunner/appsettings.json.template +++ b/DemoRunner/appsettings.json.template @@ -3,6 +3,7 @@ "Sender": { "ClientId": "00000000-0000-0000-0000-000000000000", "ClientSecret": "", + "DestinationId": "00000000-0000-0000-0000-000000000000", "LeikaKey": "urn:de:fim:leika:leistung:99400048079000" }, "Subscriber": { diff --git a/Documentation/documentation.de-DE.md b/Documentation/documentation.de-DE.md index 24fb1aab9576d4eff7efe71eb64cb18695d72a76..6933d0a820563490681d10d26b9b9c1eb8039644 100644 --- a/Documentation/documentation.de-DE.md +++ b/Documentation/documentation.de-DE.md @@ -8,6 +8,15 @@ Das FIT-Connect .NET SDK bietet eine einfache Möglichkeit, sowohl einen Antrags ### OSX +__Anmerkung:__ +Bei einem Mac mit einem M1 chip, muss brew in der _x86-64_-Version installiert werden. + +_Die Version x86_64 kann parallel zur arm64 Version installiert werden._ + +```sh +arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + Auf OSX wird das SDK nur dann unterstützt, wenn OpenSSL auf dem System installiert ist. ```sh diff --git a/E2ETest/RejectSubmissionTest.cs b/E2ETest/RejectSubmissionTest.cs index f69713c5a1e5a8650702273e822cf10ba215b0b4..393084aae66ca071b098a01bb32d1e3fb3ce50f8 100644 --- a/E2ETest/RejectSubmissionTest.cs +++ b/E2ETest/RejectSubmissionTest.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging; namespace E2ETest; public class RejectSubmissionTest : EndToEndTestBase { - private string _caseId = null!; + private string _caseId = null!; private string _submissionId = null!; [Order(10)] diff --git a/E2ETest/StraightForwardTest.cs b/E2ETest/StraightForwardTest.cs index 0eb0df15fcf30a010be572d890acfdf1a7c1713e..19a3000b38c5d3a3d60fa82ef2dfff4c3d6f50e4 100644 --- a/E2ETest/StraightForwardTest.cs +++ b/E2ETest/StraightForwardTest.cs @@ -47,7 +47,7 @@ public class StraightForwardTest : EndToEndTestBase { status.ForEach( s => Logger.LogInformation("Status {When} {Event}", s.EventTime, s.EventType)); } - + [Test] [Order(40)] public void RequestSubmission() { diff --git a/FitConnect/Encryption/CertificateHelper.cs b/FitConnect/Encryption/CertificateHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..691a54eff175c575a9ebbb5546f22f6bc1901114 --- /dev/null +++ b/FitConnect/Encryption/CertificateHelper.cs @@ -0,0 +1,98 @@ +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.Unicode; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Win32.SafeHandles; +using Newtonsoft.Json; + +namespace FitConnect.Encryption; + +public class CertificateHelper { + private readonly ILogger? _logger; + + public CertificateHelper(ILogger? logger = null) { + _logger = logger; + } + + internal bool ValidateCertificate(string keyJson, LogLevel logLevel, + X509Certificate2[]? rootCertificate = null) => + ValidateCertificate(new JsonWebKey(keyJson), logLevel, rootCertificate); + + internal bool ValidateCertificate(X509Certificate2 certificate, + out X509ChainStatus[] chainStatus, + X509Certificate2[]? rootCertificate = null, + LogLevel logLevel = LogLevel.Warning) { + var certificateChain = new X509Chain(); + +// certificate.ExportToPem($"./temp/{Guid.NewGuid().ToString()}"); + _logger?.LogDebug("Issuers: {Issuer}", certificate.Issuer); + + if (rootCertificate != null) { + certificateChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + certificateChain.ChainPolicy.CustomTrustStore.AddRange(rootCertificate); + certificateChain.ChainPolicy.ExtraStore.AddRange(rootCertificate); + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + certificateChain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; + certificateChain.ChainPolicy.DisableCertificateDownloads = false; + // certificateChain.ChainPolicy.VerificationFlags = X509VerificationFlags.IgnoreEndRevocationUnknown; + certificateChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; + _logger?.LogDebug("Using custom root certificate"); + } + else { + certificateChain.ChainPolicy.RevocationMode = X509RevocationMode.Online; + certificateChain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain; + certificateChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; + certificateChain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 30); + } + + + var result = certificateChain.Build(certificate); + + chainStatus = certificateChain.ChainStatus + .Where(s => s.Status != X509ChainStatusFlags.PartialChain).ToArray(); + + var statusAggregation = certificateChain.ChainStatus.Aggregate("", + (r, s) => r + "\n\t - " + s.Status + ": " + s.StatusInformation); + + if (certificateChain.ChainStatus.Length > 0) + _logger?.Log(logLevel, "Certificate status: {ObjStatusInformation}", + statusAggregation); + + return result; + } + + internal bool ValidateCertificate(JsonWebKey key, LogLevel logLevel = LogLevel.Error, + X509Certificate2[]? root = null) { + var certificates = key.X5c.Select(s => new X509Certificate2(Convert.FromBase64String(s))) + .ToList(); + + // if (certificates.Count != 3) { + // _logger?.Log(logLevel, "Found {Count} certificate(s) but should be 3", + // certificates.Count); + // return false; + // } + // root ??= new X509Certificate2(Convert.FromBase64String(key.X5t)); + + var valid = certificates.Aggregate(true, + (result, cert) => result + && ValidateCertificate(cert, out _, root, logLevel) + // && cert.Verify() + ); + return valid; + } +} + +public static class X509Certificate2Extensions { + public static void ExportToPem(this X509Certificate2 certificate, string fileName) { + StringBuilder builder = new StringBuilder(); + builder.AppendLine("-----BEGIN CERTIFICATE-----"); + builder.AppendLine( + Convert.ToBase64String(certificate.RawData, Base64FormattingOptions.InsertLineBreaks)); + builder.AppendLine("-----END CERTIFICATE-----"); + var content = builder.ToString(); + + File.WriteAllText(fileName + ".pem", content); + File.WriteAllText(fileName + ".json", JsonConvert.SerializeObject(certificate)); + } +} diff --git a/FitConnect/Encryption/FitEncryption.cs b/FitConnect/Encryption/FitEncryption.cs index 6061f0f5b8642a59677652225706ab8dc099e199..4975ed142f25316c18ce1b3f94aecfe7e4ca9d20 100644 --- a/FitConnect/Encryption/FitEncryption.cs +++ b/FitConnect/Encryption/FitEncryption.cs @@ -1,6 +1,7 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; using System.Text; +using FitConnect.Models; using FitConnect.Models.v1.Api; using FitConnect.Services.Models.v1.Submission; using IdentityModel; @@ -21,6 +22,10 @@ public class FitEncryption { private readonly IEncryptor _encryptor = new JoseEncryptor(); private readonly ILogger? _logger; private JwtHeader? _jwtHeader; + public string? PrivateKeyDecryption { get; set; } + public string? PrivateKeySigning { get; set; } + public string? PublicKeyEncryption { get; set; } + public string? PublicKeySignatureVerification { get; set; } internal FitEncryption(ILogger? logger) { _logger = logger; @@ -44,11 +49,6 @@ public class FitEncryption { PublicKeySignatureVerification = keySet.PublicKeySignatureVerification; } - public string? PrivateKeyDecryption { get; set; } - public string? PrivateKeySigning { get; set; } - public string? PublicKeyEncryption { get; set; } - public string? PublicKeySignatureVerification { get; set; } - public (string plainText, byte[] plainBytes, byte[] tag) Decrypt(string cypherText, string key) { @@ -93,31 +93,50 @@ public class FitEncryption { new { problems = problemsArray }); } - public string CreateAcceptSecurityEventToken(SubmissionForPickupDto submission) { - // string submissionSubmissionId,string submissionCaseId, string submissionDestinationId) { - - #warning Dummy data is used for testing purposes, replace with actual data - // NOMERGE Remove Dummy data - var payload = new { - authenticationTags = new { - data = "UCGiqJxhBI3IFVdPalHHvA", metadata = "XFBoMYUZodetZdvTiFvSkQ", - attachments = new Dictionary<string, string> { - { "0b799252-deb9-42b0-98d3-c50d24bbafe0", "rT99rwrBTbTI7IJM8fU3El" } + private dynamic GenerateAuthenticationTags(Submission submission) { + _logger?.LogInformation("Generating authentication tags"); + var attachmentDictionary = new Dictionary<string, string>(); + submission.Attachments.ForEach(a => + attachmentDictionary.Add(a.Id, a.AttachmentAuthentication!)); + + if (submission.Data != null) + return new { + authenticationTags = new { + data = submission.DataAuthentication, + metadata = submission.MetaAuthentication, + attachments = attachmentDictionary } + }; + return new { + authenticationTags = new { + metadata = submission.MetaAuthentication, + attachments = attachmentDictionary } }; + } + - // NOMERGE Add payload - if (submission?.SubmissionId == null || submission?.CaseId == null || - submission?.DestinationId == null) + public string CreateAcceptSecurityEventToken(SubmissionForPickupDto submission, + dynamic? payload = null) { + if (submission?.Id == null || submission?.CaseId == null || + submission?.DestinationId == null) { throw new ArgumentException("SubmissionId, CaseId and DestinationId are required"); + } - return CreateSecurityEventToken(submission.SubmissionId, submission.CaseId, + return CreateSecurityEventToken(submission.Id, submission.CaseId, submission.DestinationId, "https://schema.fitko.de/fit-connect/events/accept-submission", null); } + public string CreateAcceptSecurityEventToken(Submission submission) { + var payload = GenerateAuthenticationTags(submission); + return CreateSecurityEventToken(submission.Id, submission.CaseId, + submission.DestinationId, + "https://schema.fitko.de/fit-connect/events/accept-submission", payload); + } + + private string CreateSecurityEventToken(string submissionId, string caseId, string destinationId, @@ -179,6 +198,10 @@ public class FitEncryption { return ByteToHexString(SHA512.Create().ComputeHash(Encoding.UTF8.GetBytes(data))); } + public static string CalculateHash(byte[] data) { + return ByteToHexString(SHA512.Create().ComputeHash(data)); + } + private static string ByteToHexString(IEnumerable<byte> data) { var sb = new StringBuilder(); foreach (var b in data) sb.Append(b.ToString("x2")); @@ -247,4 +270,4 @@ public class FitEncryption { return result.IsValid; } -} +} \ No newline at end of file diff --git a/FitConnect/FitConnect.csproj b/FitConnect/FitConnect.csproj index c131e6d93b08c118aba6c431caebe62b8a071bfd..c4077619411f1911413c68b3bb5023ffd09bc28a 100644 --- a/FitConnect/FitConnect.csproj +++ b/FitConnect/FitConnect.csproj @@ -11,28 +11,28 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Autofac" Version="6.4.0"/> - <PackageReference Include="IdentityModel" Version="6.0.0"/> - <PackageReference Include="jose-jwt" Version="4.0.0"/> - <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0"/> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1"/> - <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.22.0"/> - <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.22.0"/> - <PackageReference Include="Newtonsoft.Json" Version="13.0.1"/> - <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14"/> - <PackageReference Include="NJsonSchema" Version="10.7.2"/> - <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.21.0"/> + <PackageReference Include="Autofac" Version="6.4.0" /> + <PackageReference Include="IdentityModel" Version="6.0.0" /> + <PackageReference Include="jose-jwt" Version="4.0.0" /> + <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" /> + <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.22.0" /> + <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.22.0" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" /> + <PackageReference Include="NJsonSchema" Version="10.7.2" /> + <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.21.0" /> </ItemGroup> <ItemGroup> - <None Remove="metadata.schema.json"/> - <EmbeddedResource Include="metadata.schema.json"/> + <None Remove="metadata.schema.json" /> + <EmbeddedResource Include="metadata.schema.json" /> </ItemGroup> <ItemGroup> - <Compile Remove="FunctionalBaseClass.cs"/> - <Compile Remove="Models\OAuthAccessToken.cs"/> - <Compile Remove="DiContainer.cs"/> + <Compile Remove="FunctionalBaseClass.cs" /> + <Compile Remove="Models\OAuthAccessToken.cs" /> + <Compile Remove="DiContainer.cs" /> </ItemGroup> </Project> diff --git a/FitConnect/FitConnectClient.cs b/FitConnect/FitConnectClient.cs index a46e63e14b482179d379e803171d5e0d24f27378..19089979576abe25e29fb04b368216e49d167689 100644 --- a/FitConnect/FitConnectClient.cs +++ b/FitConnect/FitConnectClient.cs @@ -15,6 +15,7 @@ public abstract class FitConnectClient { private readonly string? _privateKeySigning; private readonly string? _publicKeyEncryption; private readonly string? _publicKeySignatureVerification; + protected readonly bool VerifiedKeysAreMandatory; protected FitConnectClient(FitConnectEnvironment environment, @@ -41,6 +42,7 @@ public abstract class FitConnectClient { DestinationService = new DestinationService(environment.SubmissionUrl[0], OAuthService, logger: logger); Logger = logger; + VerifiedKeysAreMandatory = environment.VerifiedKeysAreMandatory; } protected FitConnectClient(FitConnectEnvironment environment, @@ -115,8 +117,9 @@ public abstract class FitConnectClient { /// <returns></returns> public List<SecurityEventToken> GetStatusForSubmission(SubmissionForPickupDto submission, bool skipTest = false) { - if (submission?.CaseId == null || submission.DestinationId == null) + if (submission?.CaseId == null || submission.DestinationId == null) { throw new ArgumentNullException(nameof(submission)); + } var events = SubmissionService .GetStatusForSubmissionAsync(submission.CaseId, submission.DestinationId, skipTest) diff --git a/FitConnect/Interfaces/IFitConnectClient.cs b/FitConnect/Interfaces/IFitConnectClient.cs index 7e25169e1ccffac20e4115d9a0e769f8a376c64b..d27dc7f9f2c5e898ae05859e98f45f6a68c45c3b 100644 --- a/FitConnect/Interfaces/IFitConnectClient.cs +++ b/FitConnect/Interfaces/IFitConnectClient.cs @@ -17,6 +17,7 @@ public interface IFitConnectClient { bool skipTest = false); /// <summary> + /// /// </summary> /// <param name="submission"></param> /// <param name="skipTest"></param> diff --git a/FitConnect/Interfaces/Subscriber/ISubscriber.cs b/FitConnect/Interfaces/Subscriber/ISubscriber.cs index 2f6c2489409826aafe2847d9413a3fef07541359..2f3b0520aa008b362a9b4d71cbd4d9953ffc64e6 100644 --- a/FitConnect/Interfaces/Subscriber/ISubscriber.cs +++ b/FitConnect/Interfaces/Subscriber/ISubscriber.cs @@ -20,6 +20,6 @@ public interface ISubscriber : IFitConnectClient { /// <param name="submissionId">unique identifier of a <see cref="Submission" /></param> /// <param name="skipSchemaTest"></param> /// <returns>A subscriber object with a submission</returns> - public ISubscriberWithSubmission RequestSubmission(string? submissionId, + public ISubscriberWithSubmission RequestSubmission(string submissionId, bool skipSchemaTest = false); } diff --git a/FitConnect/Interfaces/Subscriber/ISubscriberWithSubmission.cs b/FitConnect/Interfaces/Subscriber/ISubscriberWithSubmission.cs index 6d82850af24e57431622e8f8a80cc54f900f903c..c7cd242a8b95c98145a98ff751ddb6a6600719b3 100644 --- a/FitConnect/Interfaces/Subscriber/ISubscriberWithSubmission.cs +++ b/FitConnect/Interfaces/Subscriber/ISubscriberWithSubmission.cs @@ -1,9 +1,10 @@ using FitConnect.Models; using FitConnect.Models.v1.Api; +using FitConnect.Services.Models; namespace FitConnect.Interfaces.Subscriber; -public interface ISubscriberWithSubmission { +public interface ISubscriberWithSubmission : WithSubmission { public Submission? Submission { get; } /// <summary> @@ -42,3 +43,12 @@ public interface ISubscriberWithSubmission { /// <param name="status">state the submission has to be set to</param> public void CompleteSubmission(FinishSubmissionStatus status); } + +public interface WithSubmission { + /// <summary> + /// Verifies the submission acceptance status + /// </summary> + /// <param name="acceptanceStatus"></param> + /// <returns></returns> + public bool VerifyStatus(AcceptanceStatus acceptanceStatus); +} diff --git a/FitConnect/Models/Attachment.cs b/FitConnect/Models/Attachment.cs index 86ed85f32c0fdafc7a38b9c8c001b08b88fceb1e..88cdc74a7c7f13e0279dd7f47f13f441c8b8cfa9 100644 --- a/FitConnect/Models/Attachment.cs +++ b/FitConnect/Models/Attachment.cs @@ -5,9 +5,10 @@ using FitConnect.Models.Api.Metadata; namespace FitConnect.Models; public class Attachment { - public Attachment(Api.Metadata.Attachment metadata, byte[] content) { + public Attachment(Api.Metadata.Attachment metadata, byte[] content, string attachmentAuthentication) { Filename = metadata.Filename; Content = content; + AttachmentAuthentication = attachmentAuthentication; MimeType = Path.GetExtension(metadata.Filename) switch { "pdf" => "application/pdf", "xml" => "application/xml", @@ -43,6 +44,7 @@ public class Attachment { public string Id { get; } = Guid.NewGuid().ToString(); public byte[]? Content { get; init; } + public string? AttachmentAuthentication { get; } public string? Hash => CalculateHash(); diff --git a/FitConnect/Models/Callback.cs b/FitConnect/Models/Callback.cs index 9794d32f12ad89f8ff1f6ed135b6b3de411b56ca..9e77458289dd2b29cef838f3fe1199b1accbc429 100644 --- a/FitConnect/Models/Callback.cs +++ b/FitConnect/Models/Callback.cs @@ -3,9 +3,8 @@ using FitConnect.Services.Models; namespace FitConnect.Models; public record Callback(string? Url, string? Secret) { - public static explicit operator Callback(CallbackDto dto) { - return new(dto.Url, null); - } + public static explicit operator Callback(CallbackDto dto) + => new(dto.Url, null); public static explicit operator CallbackDto(Callback model) { return new CallbackDto { Url = model.Url }; diff --git a/FitConnect/Models/FitConnectEnvironment.cs b/FitConnect/Models/FitConnectEnvironment.cs index 294cf80468e440db3aadfea90c00548fbb0fc195..fd377f512e52e9f507a0deb46d3355522337d847 100644 --- a/FitConnect/Models/FitConnectEnvironment.cs +++ b/FitConnect/Models/FitConnectEnvironment.cs @@ -4,25 +4,32 @@ public class FitConnectEnvironment { // List of Domains // https://wiki.fit-connect.fitko.dev/de/Betrieb/Dokumentation/Domains + public static readonly FitConnectEnvironment Develop = new( + "https://auth-dev.fit-connect.fitko.dev/token", + new[] { "https://submission-api-dev.fit-connect.fitko.dev" }, + string.Empty, // "https://routing-api-testing.fit-connect.fitko.dev", // Dev does not have a routing API + "https://portal.auth-dev.fit-connect.fitko.dev" + ) { VerifiedKeysAreMandatory = false }; + public static readonly FitConnectEnvironment Testing = new( "https://auth-testing.fit-connect.fitko.dev/token", new[] { "https://submission-api-testing.fit-connect.fitko.dev" }, "https://routing-api-testing.fit-connect.fitko.dev", "https://portal.auth-testing.fit-connect.fitko.dev" - ); + ) { VerifiedKeysAreMandatory = false }; public static readonly FitConnectEnvironment Staging = new( - "https://auth-refz.fit-connect.fitko.dev/token", - new[] { "https://submission-api-refz.fit-connect.fitko.dev" }, - "https://routing-api-refz.fit-connect.fitko.dev", - "https://portal.auth-refz.fit-connect.fitko.dev" + "https://auth-refz.fit-connect.fitko.net/token", + new[] { "submission-api-refz.fit-connect.niedersachsen.de" }, + string.Empty, // "https://routing-api-testing.fit-connect.fitko.dev", // Stage does not have a routing API + "https://portal.auth-refz.fit-connect.fitko.net" ); public static readonly FitConnectEnvironment Production = new( - "https://auth.fit-connect.fitko.net/token", - new[] { "https://submission-api.fit-connect.fitko.net" }, - "https://routing-api.fit-connect.fitko.net", - "https://portal.auth.fit-connect.fitko.net" + "https://auth-prod.fit-connect.fitko.net/token", + new[] { "https://submission-api-prod.fit-connect.niedersachsen.de" }, + "https://routing-api-prod.fit-connect.fitko.net", + "https://portal.auth-prod.fit-connect.fitko.net" ); public FitConnectEnvironment(string sspUrl, string tokenUrl, string[] submissionUrl, @@ -67,6 +74,8 @@ public class FitConnectEnvironment { /// </summary> public string RoutingUrl { get; } + public bool VerifiedKeysAreMandatory { get; private init; } = true; + /// <summary> /// Creates the endpoints for the given environment. /// </summary> diff --git a/FitConnect/Models/SecurityEventToken.cs b/FitConnect/Models/SecurityEventToken.cs index 02204d91b8d5bd5988696f71eda6768bb76b662c..de3b3ec1000f0dd55eafb8e7101134256034c753 100644 --- a/FitConnect/Models/SecurityEventToken.cs +++ b/FitConnect/Models/SecurityEventToken.cs @@ -18,16 +18,36 @@ public enum EventType { } public class SecurityEventToken { - private const string Accept = "https://schema.fitko.de/fit-connect/events/accept-submission"; - private const string Reject = "https://schema.fitko.de/fit-connect/events/reject-submission"; + + public const string CreateSubmissionSchema = + "https://schema.fitko.de/fit-connect/events/create-submission"; + + public const string SubmitSubmissionSchema = + "https://schema.fitko.de/fit-connect/events/submit-submission"; + + public const string NotifySubmissionSchema = + "https://schema.fitko.de/fit-connect/events/notify-submission"; + + public const string ForwardSubmissionSchema = + "https://schema.fitko.de/fit-connect/events/forward-submission"; + + public const string RejectSubmissionSchema = + "https://schema.fitko.de/fit-connect/events/reject-submission"; + + public const string AcceptSubmissionSchema = + "https://schema.fitko.de/fit-connect/events/accept-submission"; + + public const string DeleteSubmissionSchema = + "https://schema.fitko.de/fit-connect/events/delete-submission"; + public SecurityEventToken(string jwtEncodedString) { Token = new JsonWebToken(jwtEncodedString); EventType = DecodeEventType(Token.Claims); - if (Token.Claims.All(c => c.Type != "iat")) + if (Token.Claims.All(c => c.Type != "iat")) return; - + var iat = Token.Claims.FirstOrDefault(c => c.Type == "iat")!.Value; if (long.TryParse(iat, out var timeEpoch)) EventTime = DateTime.UnixEpoch.AddSeconds(timeEpoch); @@ -52,30 +72,36 @@ public class SecurityEventToken { var contentData = JsonConvert.DeserializeObject(eventsClaim.Value); - if (eventsClaim.Value.Contains( - "https://schema.fitko.de/fit-connect/events/create-submission")) + CreateSubmissionSchema)) return EventType.Create; if (eventsClaim.Value.Contains( - "https://schema.fitko.de/fit-connect/events/submit-submission")) + SubmitSubmissionSchema)) return EventType.Submit; if (eventsClaim.Value.Contains( - "https://schema.fitko.de/fit-connect/events/notify-submission")) + NotifySubmissionSchema)) return EventType.Notify; if (eventsClaim.Value.Contains( - "https://schema.fitko.de/fit-connect/events/forward-submission")) + ForwardSubmissionSchema)) return EventType.Forward; - if (eventsClaim.Value.Contains(Reject)) { + if (eventsClaim.Value.Contains( + + RejectSubmissionSchema)) { Problems = GetProblems(events?.Values?.FirstOrDefault()?.ToString() ?? ""); return EventType.Reject; } - if (eventsClaim.Value.Contains(Accept)) return EventType.Accept; + + + if (eventsClaim.Value.Contains(AcceptSubmissionSchema)) + + return EventType.Accept; + if (eventsClaim.Value.Contains( - "https://schema.fitko.de/fit-connect/events/delete-submission")) + DeleteSubmissionSchema)) return EventType.Delete; return EventType.NotSet; diff --git a/FitConnect/Models/Submission.cs b/FitConnect/Models/Submission.cs index bd9b20e8dbba5e95beaeaa7f32d5ab49e0a52aef..b8792a1d46a3790e20986c32dc9b4a42afad5ee0 100644 --- a/FitConnect/Models/Submission.cs +++ b/FitConnect/Models/Submission.cs @@ -1,11 +1,12 @@ +using System.Security.Cryptography.X509Certificates; using FitConnect.Services.Models; using FitConnect.Services.Models.v1.Submission; namespace FitConnect.Models; public class Submission { - public string? Id { get; set; } - public string? CaseId { get; set; } + public string Id { get; set; } = null!; + public string CaseId { get; set; } = null!; public Destination Destination { get; set; } = new(); public string DestinationId { @@ -23,6 +24,8 @@ public class Submission { public string? Data { get; set; } public string? EncryptedMetadata { get; set; } public string? EncryptedData { get; set; } + public string? MetaAuthentication => EncryptedMetadata?.Split('.').Last(); + public string? DataAuthentication => EncryptedData?.Split('.').Last(); public bool IsSubmissionReadyToAdd(out string? error) { var innerError = ""; @@ -45,7 +48,7 @@ public class Submission { public static implicit operator SubmissionForPickupDto(Submission sub) { return new SubmissionForPickupDto { - SubmissionId = sub.Id, + Id = sub.Id, CaseId = sub.CaseId, DestinationId = sub.DestinationId }; @@ -53,7 +56,7 @@ public class Submission { public static explicit operator Submission(SubmissionForPickupDto dto) { return new Submission { - Id = dto.SubmissionId, + Id = dto.Id, Callback = null, DestinationId = dto.DestinationId ?? throw new NullReferenceException(nameof(dto.DestinationId)), diff --git a/FitConnect/Router.cs b/FitConnect/Router.cs index 9f211eb2361c64e39151619bc9705b9f323477e0..51d28dce0aa17511d9579d3b545cf1dde2dee3d9 100644 --- a/FitConnect/Router.cs +++ b/FitConnect/Router.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using FitConnect.Encryption; using FitConnect.Models; using FitConnect.Services; @@ -13,7 +13,7 @@ using Route = FitConnect.Services.Models.v1.Routes.Route; namespace FitConnect; -public class Router : IRouter { +internal class Router : IRouter { private readonly FitConnectEnvironment _environment; private readonly ILogger? _logger; private readonly IRouteService _routeService; diff --git a/FitConnect/SecuritySpecification.cs b/FitConnect/SecuritySpecification.cs new file mode 100644 index 0000000000000000000000000000000000000000..d476074188e69935a77d371e82b91f020e95003f --- /dev/null +++ b/FitConnect/SecuritySpecification.cs @@ -0,0 +1,18 @@ +using System.Security.Cryptography; +using System.Text; + +namespace FitConnect; + +/// <summary> +/// This class contains all security related methods that can be changed due to future +/// security issues or decisions from the BSI. +/// </summary> +internal static class SecuritySpecification { + public const int MaxCallbackAge = 5; + + public static byte[] CalculateCallbackHmac(string callbackSecret, long timestamp, string body) { + return new HMACSHA512(Encoding.UTF8.GetBytes(callbackSecret)) + .ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{body}")); + } + +} diff --git a/FitConnect/Sender.cs b/FitConnect/Sender.cs index 6841c6abe34ca33bd6e932f0e304b94c143d3324..a140a1e438302b4f29b79917226a5ab7835e0c19 100644 --- a/FitConnect/Sender.cs +++ b/FitConnect/Sender.cs @@ -1,3 +1,4 @@ +using System.Security; using System.Text.RegularExpressions; using Autofac; using FitConnect.Encryption; @@ -29,6 +30,9 @@ namespace FitConnect; /// </example> public class Sender : FitConnectClient, ISender, ISenderWithDestination, ISenderWithAttachments, ISenderWithData, ISenderWithService { + public string? PublicKey { get; set; } + public Submission? Submission { get; set; } + public Sender(FitConnectEnvironment environment, string clientId, string clientSecret, ILogger? logger = null) : base(environment, clientId, clientSecret, logger) { } @@ -38,10 +42,6 @@ public class Sender : FitConnectClient, ISender, ISenderWithDestination, container) { } - public Submission? Submission { get; set; } - - public string? PublicKey { get; set; } - public ISenderWithDestination FindDestinationId(string leiaKey, string? ags = null, string? ars = null, @@ -181,8 +181,14 @@ public class Sender : FitConnectClient, ISender, ISenderWithDestination, return submission; } - private async Task<string> GetPublicKeyFromDestination(string destinationId) { + internal async Task<string> GetPublicKeyFromDestination(string destinationId) { var publicKey = await DestinationService.GetPublicKey(destinationId); + + var keyIsValid = new CertificateHelper(Logger).ValidateCertificate(publicKey, + VerifiedKeysAreMandatory ? LogLevel.Error : LogLevel.Warning); + if (VerifiedKeysAreMandatory && !keyIsValid) + throw new SecurityException("Public key is not trusted"); + return publicKey; } @@ -225,6 +231,20 @@ public class Sender : FitConnectClient, ISender, ISenderWithDestination, return JsonConvert.SerializeObject(metaData); } + /// <summary> + /// Finding Areas + /// </summary> + /// <param name="filter"></param> + /// <param name="totalCount"></param> + /// <param name="offset"></param> + /// <param name="limit"></param> + /// <returns></returns> + public IEnumerable<Area> GetAreas(string filter, out int totalCount, int offset = 0, + int limit = 100) { + var dto = RouteService.GetAreas(filter, offset, limit).Result; + totalCount = dto?.TotalCount ?? 0; + return dto?.Areas ?? new List<Area>(); + } /// <summary> /// Uploading the encrypted data to the server diff --git a/FitConnect/Services/Interfaces/ISelfServicePortalService.cs b/FitConnect/Services/Interfaces/ISelfServicePortalService.cs index dce0390c717dbc7e6df5c696f1a6f8c57f265c27..7b87c7804a264e92343ac2e828c46bb741d52acd 100644 --- a/FitConnect/Services/Interfaces/ISelfServicePortalService.cs +++ b/FitConnect/Services/Interfaces/ISelfServicePortalService.cs @@ -1,3 +1,4 @@ +using Jose; using Microsoft.IdentityModel.Tokens; namespace FitConnect.Services.Interfaces; diff --git a/FitConnect/Services/Models/ServiceTypeDto.cs b/FitConnect/Services/Models/ServiceTypeDto.cs index c34a7521674d279968d1b07a9c36c8421563055b..25591062ac054a093237cb3654ccc6ad84a7dd1b 100644 --- a/FitConnect/Services/Models/ServiceTypeDto.cs +++ b/FitConnect/Services/Models/ServiceTypeDto.cs @@ -12,3 +12,14 @@ public class ServiceTypeDto { [JsonProperty("identifier")] public string? Identifier { get; set; } } + +public class AcceptanceStatus { + [JsonProperty("metadata")] + public string? Metadata { get; set; } + + [JsonProperty("data")] + public string? Data { get; set; } + + [JsonProperty("attachments")] + public Dictionary<string, string> Attachments { get; set; } = new Dictionary<string, string>(); +} diff --git a/FitConnect/Services/Models/v1/Api/Metadata.cs b/FitConnect/Services/Models/v1/Api/Metadata.cs index c5ccaf433011c00cc3ca3068b80d95a09ac78c45..77fa78761a218643d7ada6cfa73899479c6af4e5 100644 --- a/FitConnect/Services/Models/v1/Api/Metadata.cs +++ b/FitConnect/Services/Models/v1/Api/Metadata.cs @@ -53,6 +53,8 @@ namespace FitConnect.Models.Api.Metadata [JsonProperty("replyChannel", NullValueHandling = NullValueHandling.Ignore)] public ReplyChannel ReplyChannel { get; set; } + + public override string ToString() => JsonConvert.SerializeObject(this); } /// <summary> diff --git a/FitConnect/Services/Models/v1/Routes/Routes.cs b/FitConnect/Services/Models/v1/Routes/Routes.cs index 375913cdd3a7f39f58c0108f8375e104bd279c8d..8719150b365692ec259ad3c0f311ffd8f6814f21 100644 --- a/FitConnect/Services/Models/v1/Routes/Routes.cs +++ b/FitConnect/Services/Models/v1/Routes/Routes.cs @@ -1,5 +1,4 @@ // Root myDeserializedClass = JsonSerializer.Deserialize<Root>(myJsonResponse); - #nullable disable using Newtonsoft.Json; diff --git a/FitConnect/Services/Models/v1/Submission/SubmissionCreatedDto.cs b/FitConnect/Services/Models/v1/Submission/SubmissionCreatedDto.cs index fa607ae2dfa44a67008a0052ee68f75abd6d6fb8..048c3490e83081d74952465691e80bf56f4b0026 100644 --- a/FitConnect/Services/Models/v1/Submission/SubmissionCreatedDto.cs +++ b/FitConnect/Services/Models/v1/Submission/SubmissionCreatedDto.cs @@ -4,11 +4,11 @@ namespace FitConnect.Services.Models.v1.Submission; public class SubmissionCreatedDto { [JsonProperty("destinationId")] - public string? DestinationId { get; set; } + public string DestinationId { get; set; } = null!; [JsonProperty("submissionId")] - public string? SubmissionId { get; set; } + public string SubmissionId { get; set; } = null!; [JsonProperty("caseId")] - public string? CaseId { get; set; } + public string CaseId { get; set; } = null!; } diff --git a/FitConnect/Services/Models/v1/Submission/SubmissionDto.cs b/FitConnect/Services/Models/v1/Submission/SubmissionDto.cs index 9a4f1aa1c17a3796cb435cc7ee5a6022597931de..1c0e3be45e4188ee8111f9434a463cf4491388c4 100644 --- a/FitConnect/Services/Models/v1/Submission/SubmissionDto.cs +++ b/FitConnect/Services/Models/v1/Submission/SubmissionDto.cs @@ -12,10 +12,10 @@ public class SubmissionDto { [JsonProperty("caseId")] - public string? CaseId { get; set; } + public string CaseId { get; set; } = null!; [JsonProperty("destinationId")] - public string? DestinationId { get; set; } + public string DestinationId { get; set; } = null!; [JsonProperty("encryptedData")] @@ -31,5 +31,5 @@ public class SubmissionDto { [JsonProperty("submissionId")] - public string? SubmissionId { get; set; } + public string SubmissionId { get; set; } = null!; } diff --git a/FitConnect/Services/Models/v1/Submission/SubmissionForPickupDto.cs b/FitConnect/Services/Models/v1/Submission/SubmissionForPickupDto.cs index 7540c18ff8926a41e13d75ccfadd16e242339072..c1a7911aacd32b07f4bf7427993e173b18ad1523 100644 --- a/FitConnect/Services/Models/v1/Submission/SubmissionForPickupDto.cs +++ b/FitConnect/Services/Models/v1/Submission/SubmissionForPickupDto.cs @@ -10,5 +10,5 @@ public class SubmissionForPickupDto { public string? DestinationId { get; set; } [JsonProperty("submissionId")] - public string? SubmissionId { get; set; } + public string Id { get; set; } = null!; } diff --git a/FitConnect/Services/OAuthService.cs b/FitConnect/Services/OAuthService.cs index 19f915311ebb549601bb7df2013885e0de76cb90..1df17608f732d16abc599abca08c0b6ae965f30a 100644 --- a/FitConnect/Services/OAuthService.cs +++ b/FitConnect/Services/OAuthService.cs @@ -38,8 +38,6 @@ internal class OAuthService : RestCallService, IOAuthService { /// https://portal.auth-testing.fit-connect.fitko.dev /// </para> /// </summary> - /// <param name="clientId">Your client Id</param> - /// <param name="clientSecret">Your client Secret</param> /// <param name="scope">Scope if needed</param> /// <returns>The received token or null</returns> public async Task AuthenticateAsync( diff --git a/FitConnect/Services/SelfServicePortalService.cs b/FitConnect/Services/SelfServicePortalService.cs index 67269c1b30d9aa0dc9711a9a5e7a8e50a6511add..34d520ec72967f316f74891ad147bd277325c9f1 100644 --- a/FitConnect/Services/SelfServicePortalService.cs +++ b/FitConnect/Services/SelfServicePortalService.cs @@ -1,4 +1,5 @@ using FitConnect.Services.Interfaces; +using Jose; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; diff --git a/FitConnect/Services/SubmissionService.cs b/FitConnect/Services/SubmissionService.cs index 49aa903b2d78ebcf0f312291ec387318363464e2..b937358b8a2811609152e5224eddfe87ce9aca70 100644 --- a/FitConnect/Services/SubmissionService.cs +++ b/FitConnect/Services/SubmissionService.cs @@ -152,7 +152,6 @@ internal class SubmissionService : RestCallService, ISubmissionService { if (events == null) return null; - // Download well known keys var valid = await ValidateSignature(events, destinationId); valid &= await ValidateSchema(events); diff --git a/FitConnect/Subscriber.cs b/FitConnect/Subscriber.cs index 7d08e9d4d306bfe4a441362d12e13f91e9779c8b..f957e3b36aaa3dacb266e8699d3f75ba7e3b3046 100644 --- a/FitConnect/Subscriber.cs +++ b/FitConnect/Subscriber.cs @@ -1,17 +1,21 @@ +using System.Net; using System.Reflection; using System.Security.Cryptography; using System.Text; using Autofac; +using Autofac.Core.Activators.Reflection; using FitConnect.Encryption; using FitConnect.Interfaces.Subscriber; using FitConnect.Models; using FitConnect.Models.v1.Api; +using FitConnect.Services.Models; using FitConnect.Services.Models.v1.Submission; using IdentityModel; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using NJsonSchema; +using NJsonSchema.Annotations; using NJsonSchema.Validation; using Metadata = FitConnect.Models.Api.Metadata.Metadata; @@ -23,6 +27,8 @@ namespace FitConnect; public class Subscriber : FitConnectClient, ISubscriber, ISubscriberWithSubmission { + public Submission? Submission { get; private set; } + public Subscriber(FitConnectEnvironment environment, string clientId, string clientSecret, string privateKeyDecryption, @@ -74,11 +80,8 @@ public class Subscriber : FitConnectClient, /// <param name="submissionId"></param> /// <param name="skipSchemaTest"></param> /// <returns></returns> - public ISubscriberWithSubmission RequestSubmission(string? submissionId, + public ISubscriberWithSubmission RequestSubmission(string submissionId, bool skipSchemaTest = false) { - if (submissionId == null) - throw new ArgumentNullException($"{nameof(submissionId)} has to be set"); - var submission = (Submission)SubmissionService.GetSubmission(submissionId); var (metaDataString, _, metaHash) = Encryption.Decrypt(submission.EncryptedMetadata!); @@ -93,7 +96,6 @@ public class Subscriber : FitConnectClient, submission.Metadata = JsonConvert.DeserializeObject<Metadata>(metaDataString); - if (submission.EncryptedData != null) { var (dataString, _, dataHash) = Encryption.Decrypt(submission.EncryptedData); submission.Data = dataString; @@ -111,8 +113,6 @@ public class Subscriber : FitConnectClient, return this; } - public Submission? Submission { get; private set; } - /// <summary> /// Reading attachments for a submission. @@ -129,7 +129,8 @@ public class Subscriber : FitConnectClient, var attachmentMeta = Submission.Metadata.ContentStructure.Attachments.First(a => a.AttachmentId == id); - attachments.Add(new Attachment(attachmentMeta, content)); + attachments.Add(new Attachment(attachmentMeta, content, + encryptedAttachment.Split('.').Last())); } Submission.Attachments = attachments; @@ -153,11 +154,24 @@ public class Subscriber : FitConnectClient, CompleteSubmission(Submission!, status); } + public bool VerifyStatus(AcceptanceStatus acceptanceStatus) { + if (Submission == null) + throw new NullReferenceException("Submission is null"); + var result = true; + result &= acceptanceStatus.Metadata == Submission.MetaAuthentication; + result &= acceptanceStatus.Data == Submission.DataAuthentication; + foreach (var attachment in Submission.Attachments) { + result &= acceptanceStatus.Attachments[attachment.Id] == + attachment.AttachmentAuthentication; + } + + return result; + } - private void CompleteSubmission(SubmissionForPickupDto submission, + private void CompleteSubmission(Submission submission, FinishSubmissionStatus status, Problems[]? problems = null) { - if (submission.SubmissionId == null || submission.CaseId == null || + if (submission.Id == null || submission.CaseId == null || submission.DestinationId == null) throw new ArgumentException("Submission does not contain all required fields"); @@ -166,7 +180,7 @@ public class Subscriber : FitConnectClient, var token = status switch { FinishSubmissionStatus.Rejected => - Encryption.CreateRejectSecurityEventToken(submission.SubmissionId, + Encryption.CreateRejectSecurityEventToken(submission.Id, submission.CaseId, submission.DestinationId, problems), FinishSubmissionStatus.Accepted => Encryption.CreateAcceptSecurityEventToken( @@ -181,23 +195,22 @@ public class Subscriber : FitConnectClient, public static string VerifyCallback(string callbackSecret, long timestamp, string body) { - if (timestamp < DateTime.Now.AddMinutes(-5).ToEpochTime()) + if (timestamp < DateTime.Now.AddMinutes(SecuritySpecification.MaxCallbackAge * -1) + .ToEpochTime()) throw new ArgumentException("Request is too old"); - - var hmac = new HMACSHA512(Encoding.UTF8.GetBytes(callbackSecret)) - .ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{body}")); - - + var hmac = SecuritySpecification.CalculateCallbackHmac(callbackSecret, timestamp, body); return Convert.ToHexString(hmac).ToLower(); } + public static bool VerifyCallback(string callbackSecret, HttpRequest request) { if (!request.Headers.ContainsKey("callback-timestamp")) throw new ArgumentException("Missing callback-timestamp header"); var timeStampString = request.Headers["callback-timestamp"].ToString(); - if (!long.TryParse(timeStampString, out var timestamp)) + if (!long.TryParse(timeStampString, out var timestamp)) { throw new ArgumentException("Invalid callback-timestamp header"); + } var authentication = request.Headers["callback-authentication"]; diff --git a/IntegrationTests/CertificateValidation.cs b/IntegrationTests/CertificateValidation.cs new file mode 100644 index 0000000000000000000000000000000000000000..7a26e50a8bff96bb44352117d7066bd4f2a44103 --- /dev/null +++ b/IntegrationTests/CertificateValidation.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security; +using System.Security.Cryptography.X509Certificates; +using Autofac; +using FitConnect; +using FitConnect.Encryption; +using FitConnect.Models; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; +using MockContainer; +using Moq; +using NUnit.Framework; + +namespace IntegrationTests; + +[TestFixture] +public class CertificateValidation { + private MockSettings _settings = null!; + private ILogger _logger = null!; + private CertificateHelper _certificateHelper = null!; + + [SetUp] + public void Setup() { + var container = Container.Create(); + _settings = container.Resolve<MockSettings>(); + + _logger = LoggerFactory.Create( + builder => { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Debug); + }).CreateLogger("E2E Test"); + + + _certificateHelper = new CertificateHelper(_logger); + } + + [Test] + [Ignore("No credentials for dev environment")] + public void CheckCertificateInEnvironment_Dev() { + var environment = FitConnectEnvironment.Develop; + var sender = Client.GetSender(environment, _settings.SenderClientId, + _settings.SenderClientSecret, + _logger); + + var certificate = (sender as FitConnect.Sender)! + .GetPublicKeyFromDestination(_settings.DestinationId).Result; + new CertificateHelper(_logger).ValidateCertificate(JsonWebKey.Create(certificate), + LogLevel.Trace); + } + + [Test] + public void CheckCertificateInEnvironment_Testing() { + var environment = FitConnectEnvironment.Testing; + var sender = Client.GetSender(environment, _settings.SenderClientId, + _settings.SenderClientSecret, + _logger); + + var certificate = (sender as FitConnect.Sender)! + .GetPublicKeyFromDestination(_settings.DestinationId).Result; + new CertificateHelper(_logger).ValidateCertificate(JsonWebKey.Create(certificate), + LogLevel.Trace); + } + + + [Test] + [Ignore("No credentials for staging environment")] + public void CheckCertificateInEnvironment_Staging() { + var environment = FitConnectEnvironment.Staging; + var sender = Client.GetSender(environment, _settings.SenderClientId, + _settings.SenderClientSecret, + _logger); + + Assert.Throws<AggregateException>(() => { + sender.WithDestination(_settings.DestinationId) + .WithServiceType("", _settings.LeikaKey) + .WithAttachments(new Attachment("Test.pdf", "Simple Test PDF")) + .Submit(); + })!.InnerExceptions.Any(e => e.GetType() == typeof(SecurityException)).Should().BeTrue(); + } + + [Test] + [Ignore("No credentials for production environment")] + public void CheckCertificateInEnvironment_Production() { + var environment = FitConnectEnvironment.Production; + var sender = Client.GetSender(environment, _settings.SenderClientId, + _settings.SenderClientSecret, + _logger); + + Assert.Throws<AggregateException>(() => { + sender.WithDestination(_settings.DestinationId) + .WithServiceType("", _settings.LeikaKey) + .WithAttachments(new Attachment("Test.pdf", "Simple Test PDF")) + .Submit(); + })!.InnerExceptions.Any(e => e.GetType() == typeof(SecurityException)).Should().BeTrue(); + } + + [Test] + public void CheckPublicKeyEncryption() { + _certificateHelper.ValidateCertificate(new JsonWebKey(_settings.PublicKeyEncryption)) + .Should().BeFalse(); + } + + [Test] + public void CheckPublicKeySignature() { + _certificateHelper + .ValidateCertificate(new JsonWebKey(_settings.PublicKeySignatureVerification)) + .Should().BeFalse(); + } + + [Test] + public void CheckPrivateKeyDecryption() { + _certificateHelper.ValidateCertificate(new JsonWebKey(_settings.PrivateKeyDecryption)) + .Should().BeTrue(); + } + + [Test] + public void CheckSetPublicKey() { + _certificateHelper.ValidateCertificate(new JsonWebKey(_settings.SetPublicKeys)) + .Should().BeTrue(); + } + + [Test] + public void CheckPrivateKeySigning() { + _certificateHelper.ValidateCertificate(new JsonWebKey(_settings.PrivateKeySigning)) + .Should().BeTrue(); + } + + [Test] + public void CheckPemFiles() { + var files = System.IO.Directory.GetFiles("./certificates"); + var success = 0; + var failed = 0; + var failedCerts = new List<string>(); + + foreach (var fileName in files.Where(f => !f.EndsWith("root.pem"))) { + _logger.LogInformation("Checking file: {FileName}", fileName); + + + if (fileName.EndsWith(".pem")) { + var certificate = X509Certificate2.CreateFromPem(File.ReadAllText(fileName)); + var valid = _certificateHelper.ValidateCertificate(certificate, out var states, + null); + if (valid) { + success++; + } + else { + failed++; + failedCerts.Add(fileName); + } + } + + if (fileName.EndsWith(".json")) { + var shouldFail = !fileName.Contains("/valid"); + var jwk = new JsonWebKey(File.ReadAllText(fileName)); + var valid = _certificateHelper.ValidateCertificate(jwk, + shouldFail ? LogLevel.Warning : LogLevel.Critical, + Directory.GetFiles("./certificates/roots") + .Select(file => new X509Certificate2(file)).ToArray()); + + if (shouldFail) + valid = !valid; + + if (valid) { + success++; + } + else { + failed++; + failedCerts.Add(fileName); + } + } + } + + _logger.LogWarning("Failed certificates: {Certs}", + failedCerts.Aggregate("\n", (a, b) => a + "\t - " + b + "\n")); + _logger.LogInformation("Success: {Success}, Failed: {Failed}", success, failed); + failed.Should().Be(0); + } +} diff --git a/IntegrationTests/IntegrationTests.csproj b/IntegrationTests/IntegrationTests.csproj index 03b4ca3e126b1295f920a3e256c2a8224df1816d..9b8b7191f7b8e445d2b4267c954870582561c3d6 100644 --- a/IntegrationTests/IntegrationTests.csproj +++ b/IntegrationTests/IntegrationTests.csproj @@ -30,6 +30,111 @@ <None Update="Test.pdf"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> + <None Update="certificates\www-amazon-de.pem"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\www-amazon-de-zertifikatskette.pem"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\validEncJW_KeyUse.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\validEncJWK.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\validSigJWK.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\revokedEncJWK.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="temp\readme.md"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.21636.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.30244.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\root.pem"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.17478.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29249.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29267.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29284.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29302.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29319.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29336.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29353.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29370.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.29387.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\root\ca.26281.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\root\ca.26305.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\root\ca.30244.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\root\root.pem"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1534.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1553.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1570.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1587.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1604.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1622.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1639.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1656.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.1673.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.26281.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + <None Update="certificates\roots\ca.26305.der"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> </ItemGroup> </Project> diff --git a/IntegrationTests/Sender/ThreadTest.cs b/IntegrationTests/Sender/ThreadTest.cs index c59372b206b870948ef118094a0edf47158f3e8e..2588b1db76c0418696901346514e9156392bab15 100644 --- a/IntegrationTests/Sender/ThreadTest.cs +++ b/IntegrationTests/Sender/ThreadTest.cs @@ -75,7 +75,7 @@ public class ThreadTest { foreach (var submission in submissions) subscriber - .RequestSubmission(submission.SubmissionId) + .RequestSubmission(submission.Id!) .AcceptSubmission(); submissions.Count.Should().Be(NumberOfThreads); diff --git a/IntegrationTests/Subscriber/SubscriberTestHappyPath.cs b/IntegrationTests/Subscriber/SubscriberTestHappyPath.cs index ee585fc6693fb33572091fb9dcfea7f372543d53..9255aed0061d7c5af65a71c18052bac6d2a418b9 100644 --- a/IntegrationTests/Subscriber/SubscriberTestHappyPath.cs +++ b/IntegrationTests/Subscriber/SubscriberTestHappyPath.cs @@ -42,7 +42,7 @@ public class SubscriberTestHappyPath : SubscriberTestBase { var submissions = Subscriber.GetAvailableSubmissions().ToList(); submissions.Count().Should().BeGreaterThan(0); var i = 0; - foreach (var submissionId in submissions.Select(s => s.SubmissionId)) { + foreach (var submissionId in submissions.Select(s => s.Id!)) { // Act Console.WriteLine($"Getting submission {submissionId}"); var dto = Subscriber.RequestSubmission(submissionId); @@ -91,8 +91,8 @@ public class SubscriberTestHappyPath : SubscriberTestBase { foreach (var submission in submissions) { Console.WriteLine( - $"Getting submission {submission.SubmissionId} - case {submission.CaseId}"); - var submissionId = submission.SubmissionId!; + $"Getting submission {submission.Id} - case {submission.CaseId}"); + var submissionId = submission.Id!; if (!Directory.Exists($"./attachments/{submissionId}/")) Directory.CreateDirectory($"./attachments/{submissionId}/"); diff --git a/IntegrationTests/temp/readme.md b/IntegrationTests/temp/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/MockContainer/MockContainer.cs b/MockContainer/MockContainer.cs index 364259bc889d4e7690cd01a555011d7c58fe40dc..81f9b7f91b6a42029106470c8e250117e7e9453d 100644 --- a/MockContainer/MockContainer.cs +++ b/MockContainer/MockContainer.cs @@ -16,7 +16,7 @@ namespace MockContainer; public record MockSettings(string PrivateKeyDecryption, string PrivateKeySigning, string PublicKeyEncryption, string PublicKeySignatureVerification, string SenderClientId, string SenderClientSecret, string SubscriberClientId, string SubscriberClientSecret, - string DestinationId, string LeikaKey, string CallbackSecret); + string DestinationId, string LeikaKey, string CallbackSecret, string SetPublicKeys); public class TestFile { public byte[] Content; @@ -63,6 +63,7 @@ public static class Container { var publicKeyEncryption = File.ReadAllText("./encryptionKeys/publicKey_encryption.json"); var publicKeySignature = File.ReadAllText("./encryptionKeys/publicKey_signature_verification.json"); + var setPublicKeys = File.ReadAllText("./encryptionKeys/set-public-keys.json"); var credentials = JsonConvert.DeserializeObject<dynamic>( @@ -81,7 +82,7 @@ public static class Container { publicKeyEncryption, publicKeySignature, senderClientId, senderClientSecret, subscriberClientId, subscriberClientSecret, - destinationId, leikaKey, callbackSecret)) + destinationId, leikaKey, callbackSecret, setPublicKeys)) .As<MockSettings>(); builder.Register(c => new KeySet { PrivateKeyDecryption = privateKeyDecryption,