diff --git a/.gitignore b/.gitignore index 46d572ccd238a27143d93968afb8f8a797b1f7ed..3585f2b73ecf1c9ddc9c46a21d91f18dbe79b937 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*private.env.json + ### VisualStudioCode template .vscode/* !.vscode/settings.json diff --git a/DummyClient/.dockerignore b/DummyClient/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..38bece4e1ed9968d70beb5815ba4dcead8b592d5 --- /dev/null +++ b/DummyClient/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/DummyClient/Dockerfile b/DummyClient/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2e2e405808c74fafa716b3126ca341d547c3dbfb --- /dev/null +++ b/DummyClient/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["DummyClient/DummyClient.csproj", "DummyClient/"] +RUN dotnet restore "DummyClient/DummyClient.csproj" +COPY . . +WORKDIR "/src/DummyClient" +RUN dotnet build "DummyClient.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "DummyClient.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "DummyClient.dll"] diff --git a/DummyClient/DummyClient.csproj b/DummyClient/DummyClient.csproj new file mode 100644 index 0000000000000000000000000000000000000000..e449bbc26417523e8a0a2dc0ac0878d7f2f50811 --- /dev/null +++ b/DummyClient/DummyClient.csproj @@ -0,0 +1,15 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net6.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\FitConnect\FitConnect.csproj"/> + </ItemGroup> + +</Project> diff --git a/DummyClient/DummyClient.csproj.DotSettings b/DummyClient/DummyClient.csproj.DotSettings new file mode 100644 index 0000000000000000000000000000000000000000..453288bf2b5f62addc7e0fb4b6705154705069ae --- /dev/null +++ b/DummyClient/DummyClient.csproj.DotSettings @@ -0,0 +1,2 @@ +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:String x:Key="/Default/CodeInspection/Highlighting/UsageCheckingInspectionLevel/@EntryValue">Off</s:String></wpf:ResourceDictionary> \ No newline at end of file diff --git a/DummyClient/Program.cs b/DummyClient/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..b5d49cb0fc4ae21f7144eeac2beedb70b2a60202 --- /dev/null +++ b/DummyClient/Program.cs @@ -0,0 +1,45 @@ +using System; +using FitConnect; +using FitConnect.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +Client client; + + +void FluentSenderCall() { + client.Sender + .WithDestination("destinationId") + .WithAttachments(Array.Empty<Attachment>()) + .WithData(@"{""data"":""content""}") + .Submit(); + + client.Sender + .WithDestination("destinationId") + .WithAttachments(Array.Empty<Attachment>()) + .Submit(); +} + +void FluentSubscriberCall() { + var submissions = client.Subscriber + .GetAvailableSubmissions("destinationId"); + + client.Subscriber.RequestSubmission("submissionId") + .GetAttachments((attachments => { + // Check if the attachments are valid + return true; + })); +} + + +ILogger logger = new Logger<AppDomain>(new NullLoggerFactory()); +client = new Client( + FitConnectEnvironments.Create(FitConnectEnvironments.EndpointType.Development), + "clientId", "clientSecret", + logger); + +Console.WriteLine( + "This is a dummy client to demonstrate the usage of the FitConnect SDK for .NET"); + +FluentSenderCall(); +FluentSubscriberCall(); diff --git a/EncryptionTests/assets/attachment.pdf b/EncryptionTests/assets/attachment.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a24cb7fcc27b3f3269b6892336a149d145dcc2a6 Binary files /dev/null and b/EncryptionTests/assets/attachment.pdf differ diff --git a/FitConnect.sln b/FitConnect.sln index c5c0855687791447a470f8dedf8d42b4fa5b25e8..d5a5bcffde12205d47fcacd2dc4bfc6b02fc6927 100644 --- a/FitConnect.sln +++ b/FitConnect.sln @@ -2,9 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FitConnect", "FitConnect\FitConnect.csproj", "{DFF6A0D9-5AA1-4640-B26C-4A0A28E42FA1}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D382641C-B027-411A-814C-C8C20A9505F3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InitializationTests", "InitializationTests\InitializationTests.csproj", "{73CE5625-4C13-458E-B524-0DAA850F4041}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DummyClient", "DummyClient\DummyClient.csproj", "{DEF51494-6BCD-4441-8D76-6769BBA2C089}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -16,12 +14,11 @@ Global {DFF6A0D9-5AA1-4640-B26C-4A0A28E42FA1}.Debug|Any CPU.Build.0 = Debug|Any CPU {DFF6A0D9-5AA1-4640-B26C-4A0A28E42FA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {DFF6A0D9-5AA1-4640-B26C-4A0A28E42FA1}.Release|Any CPU.Build.0 = Release|Any CPU - {73CE5625-4C13-458E-B524-0DAA850F4041}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {73CE5625-4C13-458E-B524-0DAA850F4041}.Debug|Any CPU.Build.0 = Debug|Any CPU - {73CE5625-4C13-458E-B524-0DAA850F4041}.Release|Any CPU.ActiveCfg = Release|Any CPU - {73CE5625-4C13-458E-B524-0DAA850F4041}.Release|Any CPU.Build.0 = Release|Any CPU + {DEF51494-6BCD-4441-8D76-6769BBA2C089}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEF51494-6BCD-4441-8D76-6769BBA2C089}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEF51494-6BCD-4441-8D76-6769BBA2C089}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEF51494-6BCD-4441-8D76-6769BBA2C089}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution - {73CE5625-4C13-458E-B524-0DAA850F4041} = {D382641C-B027-411A-814C-C8C20A9505F3} EndGlobalSection EndGlobal diff --git a/FitConnect/Client.cs b/FitConnect/Client.cs new file mode 100644 index 0000000000000000000000000000000000000000..3146b465a5595f130f35e76764bb7a5669d2467e --- /dev/null +++ b/FitConnect/Client.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace FitConnect; + +/// <summary> +/// The FitConnect API Client +/// </summary> +// ReSharper disable once UnusedType.Global +public class Client { + internal string? ClientId; + internal string? ClientSecret; + + public IFluentSender Sender { get; } + + public IFluentSubscriber Subscriber { get; } + // private Routing Routing { get; } + + + /// <summary> + /// Constructor for the FitConnect API Client + /// </summary> + /// <param name="environments">Choose one endpoint or create your own one</param> + /// <param name="clientId">Your client id</param> + /// <param name="clientSecret">Your client secret</param> + /// <param name="logger">Optional logger</param> + public Client( + FitConnectEnvironments environments, + string clientId, + string clientSecret, + ILogger? logger = null) { + ClientId = clientId; + ClientSecret = clientSecret; + } +} diff --git a/FitConnect/FitConnect.csproj b/FitConnect/FitConnect.csproj index e68ef46d3248e87d41a64e110a51b05c2fb89495..25b093e1c63712f8f48c97e92133e5d0f37be8f1 100644 --- a/FitConnect/FitConnect.csproj +++ b/FitConnect/FitConnect.csproj @@ -11,7 +11,20 @@ </PropertyGroup> <ItemGroup> + <PackageReference Include="Autofac" Version="6.4.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" /> + <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.20.0" /> + </ItemGroup> + + <ItemGroup> + <None Remove="metadata.schema.json" /> + <EmbeddedResource Include="metadata.schema.json" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Services\Services.csproj" /> </ItemGroup> </Project> diff --git a/FitConnect/FitConnect.csproj.DotSettings b/FitConnect/FitConnect.csproj.DotSettings new file mode 100644 index 0000000000000000000000000000000000000000..f82351d60d72e7de02dd6aab4adca153a75297f6 --- /dev/null +++ b/FitConnect/FitConnect.csproj.DotSettings @@ -0,0 +1,2 @@ +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=interfaces/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file diff --git a/FitConnect/FunctionalBaseClass.cs b/FitConnect/FunctionalBaseClass.cs index 4c318964010911cfa6b51f277684993778297b46..e9cb4b2de5a4df239d7e6cb5f2f8cd839808f50e 100644 --- a/FitConnect/FunctionalBaseClass.cs +++ b/FitConnect/FunctionalBaseClass.cs @@ -1,20 +1,40 @@ +using System.Net.Http.Headers; using System.Net.Http.Json; +using System.Security.Cryptography; using System.Text; +using FitConnect.Models; using Microsoft.Extensions.Logging; namespace FitConnect; -public class FitConnectEndpoints { - public FitConnectEndpoints(string tokenUrl, string submissionApi, string routingApi) { +public class FitConnectEnvironments { + /// <summary> + /// Default constructor. + /// </summary> + /// <param name="tokenUrl">URL for receiving the OAuth token</param> + /// <param name="submissionApi">URL for the submission API</param> + /// <param name="routingApi">URL for the routing API</param> + public FitConnectEnvironments(string tokenUrl, string[] submissionApi, string routingApi) { TokenUrl = tokenUrl; SubmissionApi = submissionApi; RoutingApi = routingApi; } - public string TokenUrl { get; } - public string SubmissionApi { get; } + /// <summary> + /// URL for receiving the OAuth token. + /// </summary> + public string TokenUrl { get; } + + /// <summary> + /// URL for the submission API. + /// </summary> + public string[] SubmissionApi { get; } + + /// <summary> + /// URL for the routing API. + /// </summary> public string RoutingApi { get; } - + public enum EndpointType { Development, @@ -22,46 +42,122 @@ public class FitConnectEndpoints { Production } - public static FitConnectEndpoints Create(EndpointType endpointType) { + /// <summary> + /// Creates the endpoints for the given environment. + /// </summary> + /// <param name="endpointType">Environment to get endpoints for</param> + /// <returns></returns> + /// <exception cref="ArgumentException">Not all environments are ready to use</exception> + public static FitConnectEnvironments Create(EndpointType endpointType) { return endpointType switch { - EndpointType.Development => DevEndpoints, + EndpointType.Development => DevEnvironments, EndpointType.Testing => throw new ArgumentException("Not approved for online testing"), EndpointType.Production => throw new ArgumentException("NOT PRODUCTION READY"), _ => throw new ArgumentOutOfRangeException(nameof(endpointType), endpointType, null) }; } - private static readonly FitConnectEndpoints DevEndpoints = new( - "https://auth-testing.fit-connect.fitko.dev/token", - "https://submission-api-testing.fit-connect.fitko.dev", - "https://routing-api-testing.fit-connect.fitko.dev" + private static readonly FitConnectEnvironments DevEnvironments = 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" ); - private static readonly FitConnectEndpoints TestEndpoints = new( + private static readonly FitConnectEnvironments TestEnvironments = new( "https://auth-testing.fit-connect.fitko.dev/token", - "https://submission-api-testing.fit-connect.fitko.dev", + new []{"https://submission-api-testing.fit-connect.fitko.dev"}, "https://routing-api-testing.fit-connect.fitko.dev" ); - private static readonly FitConnectEndpoints ProductionEndpoints = new( + private static readonly FitConnectEnvironments ProductionEnvironments = new( "https://auth-testing.fit-connect.fitko.dev/token", - "https://submission-api-testing.fit-connect.fitko.dev", + new []{"https://submission-api-testing.fit-connect.fitko.dev"}, "https://routing-api-testing.fit-connect.fitko.dev" ); } -public class FunctionalBaseClass { - private readonly ILogger? logger; - public FitConnectEndpoints Endpoints { get; } +public abstract class FunctionalBaseClass { + protected readonly ILogger? logger; + private RSA _rsa; + public FitConnectEnvironments Environments { get; } - protected FunctionalBaseClass(ILogger? logger, FitConnectEndpoints? endpoints) { - Endpoints = endpoints ?? - FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development); + protected FunctionalBaseClass(ILogger? logger, FitConnectEnvironments? endpoints) { + Environments = endpoints ?? + FitConnectEnvironments.Create(FitConnectEnvironments.EndpointType.Development); this.logger = logger; } + /// <summary> + /// Requesting an OAuth token from the FitConnect API. + /// + /// You can get the Client ID and Client Secret from the FitConnect Self Service portal + /// under https://portal.auth-testing.fit-connect.fitko.dev + /// </summary> + /// <param name="clientId">Your client Id</param> + /// <param name="clientSecret">Your client Secret</param> + /// <param name="scope">Scope if needed</param> + /// <returns></returns> + public async Task<OAuthAccessToken?> GetTokenAsync(string clientId, string clientSecret, + string? scope = null) { + var client = new HttpClient(); + client.DefaultRequestHeaders.Accept.Add( + MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var requestContent = new Dictionary<string, string> { + { "grant_type", "client_credentials" }, + { "client_id", clientId }, + { "client_secret", clientSecret } + }; + + if (scope != null) + requestContent["scope"] = scope; + + var content = new FormUrlEncodedContent(requestContent); + + var request = new HttpRequestMessage(HttpMethod.Post, Environments.TokenUrl) { + Content = content, + Method = HttpMethod.Post + }; + + var response = await client.SendAsync(request); + + return await response.Content.ReadFromJsonAsync<OAuthAccessToken>(); + } + + public Task<SecurityEventToken> GetSetDataAsync() { + throw new NotImplementedException(); + } + + public byte[] EncryptDataAsync(byte[] data, + byte[]? publicKey, + string? password, + int numberOfIterations) { + _rsa = RSA.Create(2048); + // _rsa.ImportRSAPublicKey(publicKey, out var read); + + logger?.LogInformation( + "Encrypting data with public key: {}", + Convert.ToBase64String(_rsa.ExportRSAPublicKey())); + + // var keyParams = new PbeParameters( + // PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, numberOfIterations); + // + // var privateKey = + // rsa.ExportEncryptedPkcs8PrivateKey(Encoding.UTF8.GetBytes(password), keyParams); + // + // logger?.LogInformation( + // "Private key: {}", privateKey); + + return _rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256); + } + + public byte[] DecryptDataAsync(byte[] data, byte[] privateKey) { + return _rsa.Decrypt(data, RSAEncryptionPadding.OaepSHA256); + } + + private async Task<T?> RestCall<T>(Uri uri, HttpMethod method, string body) { var client = new HttpClient(); client.DefaultRequestHeaders.Add("Accept", "application/json"); diff --git a/FitConnect/Interfaces/IBaseFunctionality.cs b/FitConnect/Interfaces/IBaseFunctionality.cs new file mode 100644 index 0000000000000000000000000000000000000000..14665ab9662ba3662f28bca67de41159ec4162c0 --- /dev/null +++ b/FitConnect/Interfaces/IBaseFunctionality.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using FitConnect.Models; + +namespace FitConnect; + +public interface IBaseFunctionality { + Task<OAuthAccessToken> GetTokenAsync(string clientId, string clientSecret, + string? scope = null); + + Task<SecurityEventToken> GetSetDataAsync(); + + // Receive SecurityEventToken and check signature +} \ No newline at end of file diff --git a/FitConnect/Interfaces/IFluentApi.cs b/FitConnect/Interfaces/IFluentApi.cs new file mode 100644 index 0000000000000000000000000000000000000000..3205566d23fe10946a9741c7ecedf8b7ca5a6d32 --- /dev/null +++ b/FitConnect/Interfaces/IFluentApi.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using FitConnect.Models; + +namespace FitConnect; + +public interface IFluentSender { + /// <summary> + /// Configures the client for the given destination and loads the public key + /// </summary> + /// <param name="destinationId">unique identifier of the clients destination</param> + /// <returns>the upload step for attachments</returns> + public IFluentSenderWithDestination WithDestination(string destinationId); + + public IFluentSenderWithData WithData(string data); + + /// <summary> + /// Send submission to FIT-Connect API. + /// </summary> + /// <returns>submitted submission</returns> + public Submission Submit(); +} + +public interface IFluentSenderWithDestination { + /// <summary> + /// Sends the submission with a list of attachments + /// </summary> + /// <param name="attachments">that are sent with the submission</param> + /// <returns>the step where additional data can be added to the submission</returns> + public IFluentSenderWithAttachments WithAttachments(IEnumerable<Attachment> attachments); +} + +public interface IFluentSenderWithAttachments : IFluentSenderReady { + /// <summary> + /// Data as string. + /// </summary> + /// <param name="data">json or xml as string</param> + /// <returns>next step to submit the data</returns> + public IFluentSenderWithData WithData(string data); +} + +public interface IFluentSenderWithData : IFluentSenderReady { +} + +public interface IFluentSenderReady { + /// <summary> + /// Send submission to FIT-Connect API. + /// </summary> + /// <returns>submitted submission</returns> + public IFluentSender Submit(); +} + +public interface IFluentSubscriber { + /// <summary> + /// Loads a list of available Submissions that were submitted to the subscriber. + /// </summary> + /// <returns>List of available submissions for pickup</returns> + public IEnumerable<string> GetAvailableSubmissions(string? destinationId = null); + + /// <summary> + /// Loads a single Submission by id. + /// </summary> + /// <param name="submissionId">unique identifier of a <see cref="Submission"/></param> + /// <returns></returns> + public IFluentSubscriberWithSubmission RequestSubmission(string submissionId); +} + +public interface IFluentSubscriberWithSubmission { + public Submission Submission { get; } + + /// <summary> + /// Loads the <see cref="Attachment"/>s for the given <see cref="Submission"/>. + /// </summary> + /// <param name="canSubmitSubmission">Function that returns a boolean if the <see cref="Submission"/> can be confirmed</param> + /// <returns></returns> + public IFluentSenderWithAttachments GetAttachments( + Func<IEnumerable<Attachment>, bool> canSubmitSubmission); +} diff --git a/FitConnect/Interfaces/ISender.cs b/FitConnect/Interfaces/ISender.cs new file mode 100644 index 0000000000000000000000000000000000000000..ed47036ac5e3501093bb6a180658c06e74e3ffe3 --- /dev/null +++ b/FitConnect/Interfaces/ISender.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace FitConnect; + +public interface ISender : IBaseFunctionality { + // Check public keys + Task<bool> CheckPublicKeyAsync(string publicKey); + + // Encrypt Data (Fachdaten) + byte[] EncryptDataAsync(string data, string publicKey); + + // Encrypt attachments (Anhänge) + Task<string> EncryptAttachmentAsync(string attachment, string publicKey); + + // Create Metadata incl. Hash + Task<string> CreateMetadataAsync(string data, string attachment, string publicKey); +} \ No newline at end of file diff --git a/FitConnect/Interfaces/ISubscriber.cs b/FitConnect/Interfaces/ISubscriber.cs new file mode 100644 index 0000000000000000000000000000000000000000..6456dab3cceef9c0b81a6a15d5f0df22fab547b6 --- /dev/null +++ b/FitConnect/Interfaces/ISubscriber.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using FitConnect.Models; + +namespace FitConnect; + +public interface ISubscriber : IBaseFunctionality { + /// <summary> + /// Decrypt Data (Fachdaten) + /// </summary> + /// <param name="data">(Fachdaten)</param> + /// <param name="privateKey">Your private key for decryption</param> + /// <returns></returns> + Task<string> DecryptDataAsync(string data, string privateKey); + + // Decrypt attachments (Anhänge) + Task<string> DecryptAttachmentAsync(string attachment, string privateKey); + + /// <summary> + /// Checks the validity of the given metadata against the schema. + /// </summary> + /// <param name="jsonMetaData">JSON meta data</param> + /// <returns></returns> + Task<bool> CheckMetadataAsync(string jsonMetaData); + + // Check Hash from Metadata + Task<bool> CheckHashAsync(string metadata); + + // Create SecurityEventToken and signature + Task<SecurityEventToken> CreateSecurityEventTokenAsync(string data, string attachment, + string privateKey); +} \ No newline at end of file diff --git a/FitConnect/Models/Area.cs b/FitConnect/Models/Area.cs new file mode 100644 index 0000000000000000000000000000000000000000..71215cc66b6cdcad6252b9b5a102c0d93d836386 --- /dev/null +++ b/FitConnect/Models/Area.cs @@ -0,0 +1,3 @@ +namespace FitConnect.Models; + +public record Area(string Id, string Name, string Type); diff --git a/FitConnect/Models/Attachment.cs b/FitConnect/Models/Attachment.cs new file mode 100644 index 0000000000000000000000000000000000000000..2c1e5da5761e07a7d46931324df81c8fc778d1f0 --- /dev/null +++ b/FitConnect/Models/Attachment.cs @@ -0,0 +1,3 @@ +namespace FitConnect.Models; + +public record Attachment(string Id, byte[] Content, string Hash, string Filename); diff --git a/FitConnect/Models/Callback.cs b/FitConnect/Models/Callback.cs new file mode 100644 index 0000000000000000000000000000000000000000..cd99183d815dd08497737a0749127d1aee6e2a37 --- /dev/null +++ b/FitConnect/Models/Callback.cs @@ -0,0 +1,6 @@ + + +namespace FitConnect.Models; + +public record Callback(string? Url, string? Secret) { + } diff --git a/FitConnect/Models/FitConnectException.cs b/FitConnect/Models/FitConnectException.cs new file mode 100644 index 0000000000000000000000000000000000000000..fbd8c908b35a8e5fe7bdeb29912b2382866b61ce --- /dev/null +++ b/FitConnect/Models/FitConnectException.cs @@ -0,0 +1,19 @@ +using System; + +namespace FitConnect.Models; + +/// <summary> +/// Representation of FitConnect error responses +/// </summary> +public class FitConnectException : Exception { + public enum ErrorTypeEnum { + Unknown + } + + public FitConnectException(string message, ErrorTypeEnum errorType = ErrorTypeEnum.Unknown, + Exception? innerException = null) : base(message, innerException) { + ErrorType = errorType; + } + + public ErrorTypeEnum ErrorType { get; set; } +} diff --git a/FitConnect/Models/Metadata.cs b/FitConnect/Models/Metadata.cs new file mode 100644 index 0000000000000000000000000000000000000000..e5268ab7532bf0baedcb5ea7832dd4039884641d --- /dev/null +++ b/FitConnect/Models/Metadata.cs @@ -0,0 +1,9 @@ +using System.Security.Cryptography.X509Certificates; + +namespace FitConnect.Models; + +public class Metadata { +} + +public class Data { +} diff --git a/FitConnect/Models/OAuthAccessToken.cs b/FitConnect/Models/OAuthAccessToken.cs new file mode 100644 index 0000000000000000000000000000000000000000..87ac3872e52a9a9e88e1df33eace09bb8fbd3ed1 --- /dev/null +++ b/FitConnect/Models/OAuthAccessToken.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Models; + +public class OAuthAccessToken { + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } + + [JsonPropertyName("scope")] + public string Scope { get; set; } + + [JsonPropertyName("token_type")] + public string TokenType { get; set; } + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } +} diff --git a/FitConnect/Models/Route.cs b/FitConnect/Models/Route.cs new file mode 100644 index 0000000000000000000000000000000000000000..9c58fe8111327d6687f78168273113aecc19e3fb --- /dev/null +++ b/FitConnect/Models/Route.cs @@ -0,0 +1,3 @@ +namespace FitConnect.Models; + +public record Route; diff --git a/FitConnect/Models/SecurityEventToken.cs b/FitConnect/Models/SecurityEventToken.cs new file mode 100644 index 0000000000000000000000000000000000000000..88a700524c1c8270c9d06685d8e0552cafb0345d --- /dev/null +++ b/FitConnect/Models/SecurityEventToken.cs @@ -0,0 +1,3 @@ +namespace FitConnect.Models; + +public record SecurityEventToken; diff --git a/FitConnect/Models/ServiceType.cs b/FitConnect/Models/ServiceType.cs new file mode 100644 index 0000000000000000000000000000000000000000..1de00dd23cfbe49e8c1f9adeebef196b72a394ec --- /dev/null +++ b/FitConnect/Models/ServiceType.cs @@ -0,0 +1,15 @@ + + +namespace FitConnect.Models; + +public class ServiceType { + public string? Name { get; set; } + + public string? Description { get; set; } + public string? Identifier { get; set; } + + + public bool IsValid() { + return !string.IsNullOrWhiteSpace(Name) && !string.IsNullOrWhiteSpace(Identifier); + } +} diff --git a/FitConnect/Models/Submission.cs b/FitConnect/Models/Submission.cs new file mode 100644 index 0000000000000000000000000000000000000000..a6f18fcb3aed374406c1df5a7833a71040b8635d --- /dev/null +++ b/FitConnect/Models/Submission.cs @@ -0,0 +1,59 @@ + + +using System.Collections.Generic; + +namespace FitConnect.Models; + +public class Destination { + public string LeikaKey { get; set; } + public string? Ags { get; set; } + public string? Ars { get; set; } + public string? AreaId { get; set; } + public string DestinationId { get; set; } + + public bool Valid => !((string.IsNullOrWhiteSpace(Ags) + && string.IsNullOrWhiteSpace(Ars) + && string.IsNullOrWhiteSpace(AreaId)) || + string.IsNullOrWhiteSpace(LeikaKey)); +} + +public class Submission { + public string Id { get; set; } + public string? CaseId { get; set; } + public Destination Destination { get; set; } = new Destination(); + + public string DestinationId { + get => Destination.DestinationId; + set => Destination.DestinationId = value; + } + + public List<Attachment> Attachments { get; set; } = new(); + + public ServiceType ServiceType { get; init; } + + public Callback? Callback { get; set; } + public Metadata? Metadata { get; set; } + public Data? Data { get; set; } + public string EncryptedMetadata { get; set; } + public string EncryptedData { get; set; } + + public bool IsSubmissionReadyToAdd(out string? error) { + var innerError = ""; + if (string.IsNullOrEmpty(DestinationId)) innerError += "DestinationId is required\r\n"; + + if (ServiceType.IsValid()) innerError += "ServiceType is invalid\r\n"; + + if (string.IsNullOrWhiteSpace(innerError)) { + error = null; + return true; + } + + error = innerError.Trim(); + return false; + } + + public bool IsSubmissionReadyToSend() { + return true; + } + +} diff --git a/FitConnect/Security/IEncryption.cs b/FitConnect/Security/IEncryption.cs new file mode 100644 index 0000000000000000000000000000000000000000..92bf328814bab3acb703c1c0bc28d018a5d6302a --- /dev/null +++ b/FitConnect/Security/IEncryption.cs @@ -0,0 +1,31 @@ +using System; +using System.Security.Cryptography.X509Certificates; + +namespace FitConnect.Security; + +public interface IEncryption { + /// <summary> + /// Just for Proof of Concept + /// </summary> + /// <returns></returns> + string GetTestToken(); + + void ImportCertificate(X509Certificate2 cert); + + /// <summary> + /// Import a public key from a PEM file + /// </summary> + /// <param name="certificatePath"></param> + /// <param name="password">Password for the certificate</param> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="Exception"></exception> + void ImportCertificate(string certificatePath, string password); + + byte[] DecryptData(byte[] data); + string DecryptData(string data); + + byte[] ExportPublicKey(); + byte[] ExportPrivateKey(); + byte[] EncryptData(byte[] data); + byte[] EncryptData(byte[] data, byte[] publicKey); +} diff --git a/FitConnect/Security/RsaEncryption.cs b/FitConnect/Security/RsaEncryption.cs new file mode 100644 index 0000000000000000000000000000000000000000..570622b28626b12b6e9b7a45e7f817a9265be2cc --- /dev/null +++ b/FitConnect/Security/RsaEncryption.cs @@ -0,0 +1,167 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.IO; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.Tokens; + +namespace FitConnect.Security; + +public class RsaEncryption : IEncryption { + private readonly X509Certificate2? _certificate; + private readonly ILogger? _logger; + private readonly RSA _rsa; + private RSA? _privateKey; + private RSA? _publicKey; + + + public RsaEncryption(X509Certificate2? certificate, ILogger? logger) { + _logger = logger; + _rsa = RSA.Create(4096); + + if (certificate != null) { + _certificate = certificate; + ImportCertificate(certificate); + } + } + + + /// <summary> + /// Just for Proof of Concept + /// </summary> + /// <returns></returns> + public string GetTestToken() { + var handler = new JwtSecurityTokenHandler(); + var token = new SecurityTokenDescriptor { + Issuer = "FitConnect", + Audience = "FitConnect", + EncryptingCredentials = + new X509EncryptingCredentials(_certificate ?? + new X509Certificate2(CreateSelfSignedCertificate())), + Subject = new ClaimsIdentity(new Claim[] { + new("Content", "Unencrypted content") + }) + }; + return handler.CreateEncodedJwt(token); + } + + + public void ImportCertificate(X509Certificate2 cert) { + if (!CheckCertificate(cert)) throw new Exception("Invalid certificate"); + + _publicKey = cert.GetRSAPublicKey(); + + if ((_publicKey?.KeySize ?? 0) == 0) + throw new Exception("Invalid certificate, no public key"); + + if (cert.HasPrivateKey) _privateKey = cert.GetRSAPrivateKey(); + + if (_privateKey != null) + _logger?.LogInformation("Certificate with private key imported"); + else + _logger?.LogInformation("Certificate has no private key"); + } + + /// <summary> + /// Import a public key from a PEM file + /// </summary> + /// <param name="certificatePath"></param> + /// <param name="password">Password for the certificate</param> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="Exception"></exception> + public void ImportCertificate(string certificatePath, string password) { + _logger?.LogInformation("Importing certificate {CertPath}", certificatePath); + var cert = + new X509Certificate2(certificatePath, password, X509KeyStorageFlags.MachineKeySet); + + if (!CheckCertificate(cert)) throw new ArgumentException("Certificate is not valid"); + + var parameters = cert.PublicKey.GetRSAPublicKey()?.ExportParameters(true); + if (parameters == null) throw new Exception("Could not get public key from certificate"); + + _rsa.ImportParameters(parameters.Value); + } + + public byte[] DecryptData(byte[] data) { + return (_privateKey ?? _rsa).Decrypt(data, RSAEncryptionPadding.OaepSHA256); + } + + public string DecryptData(string data) { + return Encoding.UTF8.GetString(DecryptData(Convert.FromBase64String(data))); + } + + public byte[] ExportPublicKey() { + return _rsa.ExportRSAPublicKey(); + } + + public byte[] ExportPrivateKey() { + return _rsa.ExportRSAPrivateKey(); + } + + public byte[] EncryptData(byte[] data) { + return (_publicKey ?? _rsa).Encrypt(data, RSAEncryptionPadding.OaepSHA256); + } + + public byte[] EncryptData(byte[] data, byte[] publicKey) { + _logger?.LogInformation( + "Encrypting data with public key: {}", + Convert.ToBase64String(_rsa.ExportRSAPublicKey())); + + _rsa.ImportRSAPublicKey(publicKey, out var read); + return _rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256); + } + + /// <summary> + /// Creating a self signed certificate + /// </summary> + /// <param name="exportPath">Location for storing the certificate files</param> + /// <returns></returns> + /// <exception cref="Exception"></exception> + public static X509Certificate2 CreateSelfSignedCertificate(string? exportPath = null) { + var rsa = RSA.Create(4096); + + var req = new CertificateRequest("c=DE, cn=fitconnect.de", + rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + var cert = req.CreateSelfSigned(DateTimeOffset.Now.AddSeconds(-5), + DateTimeOffset.Now.AddYears(5)); + + if (cert.GetRSAPublicKey() == null) + throw new Exception("Certificate does not contain a public key"); + + if (cert.GetRSAPrivateKey() == null) + throw new Exception("Certificate does not contain a private key"); + + // Export the certificate to a PEM file, just for + // additional external testing + if (exportPath != null) ExportCertificateToFile(exportPath, cert); + + return cert; + } + + private static void ExportCertificateToFile(string exportPath, X509Certificate cert) { + // Create PFX (PKCS #12) with private key + File.WriteAllBytes($"{exportPath}/certificate.pfx", + cert.Export(X509ContentType.Pfx, "")); + + // Create Base 64 encoded CER (public key only) + File.WriteAllText($"{exportPath}/certificate.cer", + "-----BEGIN CERTIFICATE-----\r\n" + + Convert.ToBase64String(cert.Export(X509ContentType.Cert, ""), + Base64FormattingOptions.InsertLineBreaks) + + "\r\n-----END CERTIFICATE-----"); + } + + + /// <summary> + /// Checking the certificate for validity + /// </summary> + /// <param name="cert">Certificate to check</param> + /// <returns>true for a fully approved certificate</returns> + private bool CheckCertificate(X509Certificate2 cert) { + return true; + } +} diff --git a/FitConnect/Sender.cs b/FitConnect/Sender.cs deleted file mode 100644 index 365bc2899fb05721d40a2500e10bb1ff6d64d687..0000000000000000000000000000000000000000 --- a/FitConnect/Sender.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace FitConnect; - -public class Sender : FunctionalBaseClass { - public Sender(ILogger? logger, FitConnectEndpoints endpoints) : base(logger,endpoints) { - } -} \ No newline at end of file diff --git a/FitConnect/Subscriber.cs b/FitConnect/Subscriber.cs deleted file mode 100644 index cde32d1fcc8c774f37b8983b0524d2dec89623c7..0000000000000000000000000000000000000000 --- a/FitConnect/Subscriber.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace FitConnect; - -public class Subscriber : FunctionalBaseClass { - public Subscriber(ILogger logger, FitConnectEndpoints endpoints) : base(logger, endpoints) { - } -} diff --git a/FitConnect/metadata.schema.json b/FitConnect/metadata.schema.json new file mode 100644 index 0000000000000000000000000000000000000000..0f730cd32e5411cf4d8d898e589307102f9c40b5 --- /dev/null +++ b/FitConnect/metadata.schema.json @@ -0,0 +1,431 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schema.fitko.de/fit-connect/metadata/1.0.0/metadata.schema.json", + "type": "object", + "title": "Metadaten", + "description": "", + "required": [ + "contentStructure" + ], + "properties": { + "contentStructure": { + "description": "Beschreibt die Struktur der zusätzlichen Inhalte der Einreichung, wie Anlagen oder Fachdaten.", + "type": "object", + "required": [ + "attachments" + ], + "properties": { + "data": { + "description": "Definiert das Schema und die Signatur(-art), die für die Fachdaten verwendet werden.", + "type": "object", + "required": [ + "hash", + "submissionSchema" + ], + "properties": { + "signature": { + "type": "object", + "description": "Beschreibt das Signaturformt und Profile", + "examples": [], + "properties": { + "signatureFormat": { + "type": "string", + "description": "Beschreibt, welches Signaturformat die genutzte Signatur / das genutzte Siegel nutzt. Aktuell wird die Hinterlegung folgender Signaturformate unterstützt: CMS = Cryptographic Message Syntax, Asic = Associated Signature Containers, PDF = PDF Signatur, XML = XML-Signature, JSON = JSON Web Signature. ", + "enum": [ + "cms", + "xml", + "pdf", + "asic", + "json" + ] + }, + "eidasAdesProfile": { + "type": "string", + "description": "Referenziert ein eindeutiges Profil einer AdES (advanced electronic signature/seal) gemäß eIDAS-Verordnung über eine URI gemäß [ETSI TS 119 192](https://www.etsi.org/deliver/etsi_ts/119100_119199/119192/01.01.01_60/ts_119192v010101p.pdf).\n\nFür die Details zur Verwendung und Validierung von Profilen siehe auch https://ec.europa.eu/cefdigital/DSS/webapp-demo/doc/dss-documentation.html#_signatures_profile_simplification", + "enum": [ + "http://uri.etsi.org/ades/191x2/level/baseline/B-B#", + "http://uri.etsi.org/ades/191x2/level/baseline/B-T#", + "http://uri.etsi.org/ades/191x2/level/baseline/B-LT#", + "http://uri.etsi.org/ades/191x2/level/baseline/B-LTA#" + ] + }, + "detachedSignature": { + "type": "boolean", + "description": "Beschreibt, ob die Signatur als seperate (detached) Signatur (`true`) oder als Teil des Fachdatensatzes bzw. der Anlage (`false`) übertragen wird. Wenn der Wert `true` ist, dann wird die Signatur Base64- oder Base64Url-kodiert im Feld `content` übertragen." + }, + "content": { + "type": "string", + "description": "Hier wird die Signatur im Falle einer Detached-Signatur als Base64- oder Base64Url-kodierte Zeichenkette hinterlegt. Eine Base64Url-Kodierung kommt nur bei Einsatz von JSON Web Signatures (JWS / JAdES) zum Einsatz.", + "pattern": "^[a-zA-Z0-9+/=]+|[a-zA-Z0-9_-]+$" + } + }, + "required": [ + "signatureFormat", + "detachedSignature" + ] + }, + "hash": { + "title": "Hashwert", + "description": "Der Hashwert der unverschlüsselten Fachdaten. Die Angabe des Hashwertes dient der Integritätssicherung des Gesamtantrags und schützt vor einem Austausch der Fachdaten durch Systeme zwischen Sender und Subscriber (z.B. dem Zustelldienst).", + "type": "object", + "required": [ + "type", + "content" + ], + "properties": { + "type": { + "type": "string", + "description": "Der verwendete Hash-Algorithmus. Derzeit ist nur `sha512` erlaubt.", + "enum": [ + "sha512" + ] + }, + "content": { + "type": "string", + "description": "Der Hex-kodierte Hashwert gemäß des angegebenen Algorithmus.", + "pattern": "^[a-f0-9]{128}$" + } + } + }, + "submissionSchema": { + "title": "Fachdatenschema", + "description": "Referenz auf ein Schema, das die Struktur der Fachdaten einer Einreichung beschreibt.", + "type": "object", + "required": [ + "schemaUri", + "mimeType" + ], + "properties": { + "schemaUri": { + "type": "string", + "format": "uri", + "description": "URI des Fachschemas. Wird hier eine URL verwendet, sollte das Schema unter der angegebenen URL abrufbar sein. Eine Verfügbarkeit des Schemas unter der angegebenen URL darf jedoch nicht vorausgesetzt werden." + }, + "mimeType": { + "type": "string", + "description": "Mimetype (z.B. application/json oder application/xml) des referenzierten Schemas (z.B. XSD- oder JSON-Schema).", + "enum": [ + "application/json", + "application/xml" + ] + } + } + } + } + }, + "attachments": { + "type": "array", + "items": { + "type": "object", + "description": "Eine in der Einreichung enthaltene Anlage.", + "required": [ + "hash", + "purpose", + "mimeType", + "attachmentId" + ], + "properties": { + "hash": { + "title": "Hashwert", + "description": "Der Hashwert der unverschlüsselten Anlage. Die Angabe des Hashwertes dient der Integritätssicherung des Gesamtantrags und schützt vor einem Austausch der Anlage durch Systeme zwischen Sender und Subscriber (z.B. dem Zustelldienst).", + "type": "object", + "required": [ + "type", + "content" + ], + "properties": { + "type": { + "type": "string", + "description": "Der verwendete Hash-Algorithmus. Derzeit ist nur `sha512` erlaubt.", + "enum": [ + "sha512" + ] + }, + "content": { + "type": "string", + "description": "Der Hex-kodierte Hashwert gemäß des angegebenen Algorithmus.", + "pattern": "^[a-f0-9]{128}$" + } + } + }, + "signature": { + "$ref": "#/properties/contentStructure/properties/data/properties/signature" + }, + "purpose": { + "description": "Zweck/Art der Anlage\n- form: Automatisch generierte PDF-Repräsentation des vollständigen Antragsformulars\n- attachment: Anlage, die von einem Bürger hochgeladen wurde\n- report: Vom Onlinedienst, nachträglich erzeugte Unterlage", + "type": "string", + "enum": [ + "form", + "attachment", + "report" + ] + }, + "filename": { + "type": "string", + "description": "Ursprünglicher Dateiname bei Erzeugung oder Upload" + }, + "description": { + "type": "string", + "description": "Optionale Beschreibung der Anlage" + }, + "mimeType": { + "type": "string", + "title": "MIME Type", + "description": "Internet Media Type gemäß RFC 2045, z. B. application/pdf.", + "examples": [ + "application/xml" + ], + "pattern": "^[-\\w.]+/[-\\w.+]+$" + }, + "attachmentId": { + "type": "string", + "description": "Innerhalb einer Einreichung eindeutige Id der Anlage im Format einer UUIDv4.", + "format": "uuid", + "minLength": 32, + "maxLength": 36 + } + } + } + } + } + }, + "publicServiceType": { + "type": "object", + "title": "Verwaltungsleistung", + "description": "Beschreibung der Art der Verwaltungsleistung. Eine Verwaltungsleistung sollte immer mit einer LeiKa-Id beschrieben werden. Ist für die gegebene Verwaltungsleistung keine LeiKa-Id vorhanden, kann die Verwaltungsleistung übergangsweise über die Angabe einer anderen eindeutigen Schema-URN beschrieben werden.", + "properties": { + "name": { + "type": "string", + "description": "Name/Bezeichnung der Verwaltungsleistung" + }, + "description": { + "type": "string", + "description": "(Kurz-)Beschreibung der Verwaltungsleistung" + }, + "identifier": { + "title": "Leistungs-Identifikator", + "description": "URN einer Leistung. Im Falle einer Leistung aus dem Leistungskatalog sollte hier `urn:de:fim:leika:leistung:` vorangestellt werden.\n", + "type": "string", + "minLength": 7, + "maxLength": 255, + "pattern": "^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,.:=@;$_!*'%/?#-]+$" + } + }, + "required": [ + "identifier" + ] + }, + "authenticationInformation": { + "description": "Eine Liste aller Identifikationsnachweise der Einreichung.", + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "description": "Eine Struktur, die einen Identifikationsnachweis beschreibt.", + "properties": { + "type": { + "description": "Definiert die Art des Identifikationsnachweises.", + "type": "string", + "enum": [ + "identificationReport" + ] + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$", + "description": "semver kompatible Versionsangabe des genutzten Nachweistyps." + }, + "content": { + "type": "string", + "description": "Der Nachweis wird als Base64Url-kodierte Zeichenkette angegeben.", + "pattern": "^[a-zA-Z0-9_\\-.]+$" + } + }, + "required": [ + "type", + "version", + "content" + ] + } + }, + "paymentInformation": { + "description": "Dieses Objekt enthält die Informationen vom Bezahldienst.", + "type": "object", + "required": [ + "transactionReference", + "transactionId", + "paymentMethod", + "status" + ], + "properties": { + "transactionUrl": { + "type": "string", + "format": "uri", + "minLength": 1, + "examples": [ + "https://payment.bundesland.zzzz/api/v1/paymenttransaction/12002312/MELD-ANT-FORM-4711/9xxd-432x-6543-xfd6-gfdx-fd27" + ], + "description": "Die Rest-URL der Payment Transaction für die Statusabfrage." + }, + "transactionId": { + "type": "string", + "minLength": 1, + "maxLength": 36, + "pattern": "^[\\w\\d-]+$", + "examples": [ + "9xxd-432x-6543-xfd6-gfdx-fd27" + ], + "description": "Eine vom Bezahldienst vergebene Transaktions-Id." + }, + "transactionReference": { + "type": "string", + "description": "Bezahlreferenz bzw. Verwendungszweck, wie z. B. ein Kassenzeichen." + }, + "transactionTimestamp": { + "type": "string", + "format": "date-time", + "description": "Zeitstempel der erfolgreichen Durchführung der Bezahlung." + }, + "paymentMethod": { + "type": "string", + "enum": [ + "GIROPAY", + "PAYDIRECT", + "CREDITCARD", + "PAYPAL", + "INVOICE", + "OTHER" + ], + "examples": [ + "CREDITCARD" + ], + "description": "Die vom Benutzer ausgewählte Zahlart. Das Feld ist nur bei einer erfolgreichen Zahlung vorhanden / befüllt." + }, + "paymentMethodDetail": { + "type": "string", + "minLength": 1, + "maxLength": 36, + "pattern": "^[\\w\\d-]+$", + "examples": [ + "Visa" + ], + "description": "Weitere Erläuterung zur gewählten Zahlart." + }, + "status": { + "type": "string", + "enum": [ + "INITIAL", + "BOOKED", + "FAILED", + "CANCELED" + ], + "description": "- INITIAL - der Einreichung hat einen Payment-Request ausgelöst und eine Payment-Transaction wurde angelegt. Der Nutzer hat aber im Bezahldienst noch keine Wirkung erzeugt.\n- BOOKED - der Nutzer hat die Bezahlung im Bezahldienst autorisiert.\n- FAILED - der Vorgang wurde vom Bezahldienst aufgrund der Nutzereingaben abgebrochen.\n- CANCELED - der Nutzer hat die Bezahlung im Bezahldienst abgebrochen." + }, + "grossAmount": { + "type": "number", + "minimum": 0.01, + "multipleOf": 0.01, + "description": "Bruttobetrag" + } + } + }, + "replyChannel": { + "type": "object", + "minProperties": 1, + "maxProperties": 1, + "properties": { + "eMail": { + "type": "object", + "properties": { + "address": { + "type": "string", + "format": "email" + }, + "pgpPublicKey": { + "type": "string", + "description": "Hilfe zur Erstellung gibt es in der Dokumentation unter https://docs.fitko.de/fit-connect/details/pgp-export", + "pattern": "^-----BEGIN PGP PUBLIC KEY BLOCK-----\\n\\n" + } + }, + "required": [ + "address" + ] + }, + "deMail": { + "type": "object", + "description": "Akkreditierte Anbieter siehe https://www.bsi.bund.de/DE/Themen/Oeffentliche-Verwaltung/Moderner-Staat/De-Mail/Akkreditierte-DMDA/akkreditierte-dmda_node.html", + "properties": { + "address": { + "type": "string", + "format": "email" + } + }, + "required": [ + "address" + ] + }, + "fink": { + "type": "object", + "description": "Postfachadresse in einem interoperablen Servicekonto (FINK.PFISK)", + "properties": { + "finkPostfachRef": { + "type": "string", + "description": "FINK Postfachadresse", + "examples": [ + "hh/by/12345" + ], + "maxLength": 150, + "pattern": "^[-._a-z0-9~/]*$" + }, + "host": { + "type": "string", + "description": "URL des Servicekontos, in dem das Ziel-Postfach liegt", + "format": "uri", + "examples": [ + "https://servicekonto1.example.com/" + ] + } + }, + "required": [ + "finkPostfachRef" + ] + }, + "elster": { + "type": "object", + "description": "Siehe https://www.elster.de/elsterweb/infoseite/elstertransfer_hilfe_schnittstellen", + "properties": { + "accountId": { + "type": "string", + "pattern": "^\\d{10}$" + }, + "lieferTicket": { + "type": "string" + }, + "geschaeftszeichen": { + "type": "string", + "maxLength": 10 + } + }, + "required": [ + "accountId" + ] + } + } + }, + "additionalReferenceInfo": { + "type": "object", + "description": "Eine Struktur, um zusätzliche Informationen zu hinterlegen", + "properties": { + "senderReference": { + "type": "string", + "description": "Eine Referenz zum Vorgang im sendenden System, um bei Problemen und Rückfragen außerhalb von FIT-Connect den Vorgang im dortigen System schneller zu identifizieren." + }, + "applicationDate": { + "type": "string", + "format": "date", + "description": "Das Datum der Antragstellung. Das Datum muss nicht zwingend identisch mit dem Datum der Einreichung des Antrags über FIT-Connect sein." + } + } + } + } +} \ No newline at end of file diff --git a/InitializationTests/BaseClassConstructorTests.cs b/InitializationTests/BaseClassConstructorTests.cs deleted file mode 100644 index 0c8379673e7a10913e7ac97e9fe3867410c0928a..0000000000000000000000000000000000000000 --- a/InitializationTests/BaseClassConstructorTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using FitConnect; -using FluentAssertions; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; - -namespace InitializationTests; - -public class BaseClassConstructorTests { - private ILogger? _logger; - - [SetUp] - public void Setup() { - _logger = Mock.Of<ILogger>(); - } - - [Test] - public void CreateCustomEndpoints() { - var dut = new Sender(_logger, new FitConnectEndpoints( - "http://localhost:5050/token", - "http://localhost:5050/submission", - "http://localhost:5050/routing")); - - dut.Endpoints.RoutingApi.Should().Be("http://localhost:5050/routing"); - dut.Endpoints.SubmissionApi.Should().Be("http://localhost:5050/submission"); - dut.Endpoints.TokenUrl.Should().Be("http://localhost:5050/token"); - } - - [Test] - public void CreateDevEndpoints() { - var dut = new Sender(_logger, - FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development)); - - dut.Endpoints.TokenUrl.Should().Be("https://auth-testing.fit-connect.fitko.dev/token"); - dut.Endpoints.SubmissionApi.Should() - .Be("https://submission-api-testing.fit-connect.fitko.dev"); - dut.Endpoints.RoutingApi.Should().Be("https://routing-api-testing.fit-connect.fitko.dev"); - } - - [Test] - public void CreateTestingEndpoints() { - Assert.Throws<ArgumentException>(() => { - var sender = new Subscriber(_logger, - FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Testing)); - }); - } - - [Test] - public void CreateProductionEndpoints() { - Assert.Throws<ArgumentException>(() => { - var sender = new Sender(_logger, - FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Production)); - }); - } -} diff --git a/InitializationTests/InitializationTests.csproj b/InitializationTests/InitializationTests.csproj deleted file mode 100644 index 43b1dc07278ee1dd87ce10971772ba841c4ece36..0000000000000000000000000000000000000000 --- a/InitializationTests/InitializationTests.csproj +++ /dev/null @@ -1,23 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <PropertyGroup> - <TargetFramework>net6.0</TargetFramework> - <Nullable>enable</Nullable> - - <IsPackable>false</IsPackable> - </PropertyGroup> - - <ItemGroup> - <PackageReference Include="FluentAssertions" Version="6.7.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> - <PackageReference Include="Moq" Version="4.18.1" /> - <PackageReference Include="NUnit" Version="3.13.2" /> - <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> - <PackageReference Include="coverlet.collector" Version="3.1.0" /> - </ItemGroup> - - <ItemGroup> - <ProjectReference Include="..\FitConnect\FitConnect.csproj" /> - </ItemGroup> - -</Project> diff --git a/readme.md b/readme.md index a23bafc08735c4ee46fa94ac451cc456f207a1de..901c707ae22453f6b40f4c80d75c515cd9f48a1d 100644 --- a/readme.md +++ b/readme.md @@ -4,14 +4,25 @@ **IN DEVELOPMENT NOT FOR PRODUCTION USE** +For an implementation example take a look at the [DummyClient](DummyClient/Program.cs) -## Links +## Ignored Files -- [Project management](https://wiki.fit-connect.fitko.dev/de/PM_PUBLIC/Projektvorgehensmodell) -- [Wiki SDK Description](https://wiki.fit-connect.fitko.dev/de/PM_PUBLIC/Epics/SDK_Initialisierung) -- [Containing GitLab](https://git.fitko.de/) -- [Board filtered for SDK](https://git.fitko.de/fit-connect/planning/-/boards/44?label_name%5B%5D=component%3A%3ASDK) -- [Documentation](https://docs.fitko.de/fit-connect/docs/getting-started/first-steps/) +You need a secret file for e2e test like: + +```json +{ + "sender": { + "id": "00000000-0000-0000-0000-000000000000", + "secret": "0000000000000000000000000000000000000000000", + "scope": "send:region:DE" + } +} +``` + +[glossary](https://docs.fitko.de/fit-connect/docs/glossary/) ### Tickets - - [Method signatures](https://git.fitko.de/fit-connect/planning/-/issues/438) \ No newline at end of file + +- [Method signatures](https://git.fitko.de/fit-connect/planning/-/issues/438) +- [Java SDK](https://git.fitko.de/fit-connect/planning/-/issues/413) \ No newline at end of file diff --git a/working_notes.md b/working_notes.md new file mode 100644 index 0000000000000000000000000000000000000000..517d433b53dbfa873b286a70452895f17e60a3eb --- /dev/null +++ b/working_notes.md @@ -0,0 +1,31 @@ +# Notes + +# TODOS + +| interface | implemented | tested | scope | description | +|:---------:|:-----------:|:------:|:-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| X | X | X | Sender,Subscriber | Abruf von OAuth-Tokens | +| X | | | Sender | Prüfung von öffentlichen Schlüsseln und Zertifikatsketten + OCSP-Check (vgl. [#119](https://git.fitko.de/fit-connect/planning/-/issues/119)) | +| X | | | Sender | Verschlüsselung von Fachdaten (JSON, XML) mittels JWE | +| X | | | Sender | Verschlüsselung von Anhängen (Binärdaten) mittels JWE | +| X | | | Sender | Korrekte Erzeugung eines Metadatensatzes inkl. [Hashwerte](https://docs.fitko.de/fit-connect/docs/sending/metadata#integrity) | +| X | | | Subscriber | Entschlüsselung von Fachdaten (JSON oder XML) mittels JWE | +| X | | | Subscriber | Entschlüsselung von Anhängen (Binärdaten) mittels JWE | +| X | X | | Subscriber | Prüfung der empfangenen Metadaten gegen das zugehörige JSON-Schema | +| X | | | Subscriber | [Prüfung der Hashwerte](https://docs.fitko.de/fit-connect/docs/receiving/verification#integrity) aus dem Metadatensatz. | +| X | | | Subscriber | SET-Erstellung inkl. Signaturerzeugung | +| X | | | Sender, Subscriber | SET-Empfang inkl. Signaturprüfung | +| X | | | Sender, Subscriber | Unterstüzung / Abstraktion der API-Nutzung (`fitconnect.sendSubmission(metadata, destinationID, ...)` o.ä.) für die oben genannten Use-Cases | +| X | | | Sender, Subscriber | Logging (Logging-Modul muss von außen kommen) | + +## Links + +- [SDK-Konzept im Wiki](https://wiki.fit-connect.fitko.dev/de/Konzeption/Konzeption_SDK) +- [inoffizielles Python-SDK](https://github.com/codedust/fitconnect-sdk-python) +- [Project management](https://wiki.fit-connect.fitko.dev/de/PM_PUBLIC/Projektvorgehensmodell) +- [Wiki SDK Description](https://wiki.fit-connect.fitko.dev/de/PM_PUBLIC/Epics/SDK_Initialisierung) +- [Containing GitLab](https://git.fitko.de/) +- [Board filtered for SDK](https://git.fitko.de/fit-connect/planning/-/boards/44?label_name%5B%5D=component%3A%3ASDK) +- [Documentation](https://docs.fitko.de/fit-connect/docs/getting-started/first-steps/) +- [Security Event Token Requirements](https://wiki.fit-connect.fitko.dev/de/Konzeption/Security_Event_Token_Future) +- [glossary](https://docs.fitko.de/fit-connect/docs/glossary/)