From fbe3707b0e660a8340abcba0d493cca9b4c01434 Mon Sep 17 00:00:00 2001 From: Davide Date: Sun, 6 Feb 2022 18:12:42 +0100 Subject: [PATCH 1/7] Implementation of Work mode (preview) --- DgcReader.DgcTestData.Test/DgcDecoderTest.cs | 21 ++++++ .../CertifiateExtendedKeyUsageUtils.cs | 5 +- .../CertificateEntryExtensions.cs | 64 +++++++++++++++++++ .../DgcItalianRulesValidator.cs | 26 ++++++-- .../Models/ValidationMode.cs | 7 +- .../SdkConstants.cs | 5 ++ .../ItalianRulesValidatorTests.cs | 58 +++++++++++++++++ 7 files changed, 178 insertions(+), 8 deletions(-) diff --git a/DgcReader.DgcTestData.Test/DgcDecoderTest.cs b/DgcReader.DgcTestData.Test/DgcDecoderTest.cs index b45ef19..abec458 100644 --- a/DgcReader.DgcTestData.Test/DgcDecoderTest.cs +++ b/DgcReader.DgcTestData.Test/DgcDecoderTest.cs @@ -89,6 +89,27 @@ public async Task TestDecodeMethod() } + + [TestMethod] + public async Task TestGetBirthDates() + { + + foreach (var folder in TestEntries.Keys.Where(r => r != CertificatesTestsLoader.CommonTestDirName)) + { + foreach (var entry in TestEntries[folder]) + { + + var result = await DgcReader.Decode(entry.PREFIX); + + Assert.IsNotNull(result); + Assert.IsNotNull(result.Dgc); + + Debug.WriteLine(result.Dgc.DateOfBirth); + } + } + + } + [TestMethod] public async Task TestVerifyMethod() { diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/CertifiateExtendedKeyUsageUtils.cs b/RuleValidators/DgcReader.RuleValidators.Italy/CertifiateExtendedKeyUsageUtils.cs index c922976..62e7ebd 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/CertifiateExtendedKeyUsageUtils.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/CertifiateExtendedKeyUsageUtils.cs @@ -3,7 +3,6 @@ using System.Linq; using Microsoft.Extensions.Logging; using DgcReader.Models; -using System.Text; #if NETSTANDARD using System.Text; @@ -56,7 +55,7 @@ public static IEnumerable GetExtendedKeyUsages(SignatureValidationResult return Enumerable.Empty(); #else - var certificate = new X509Certificate2(signatureValidation.PublicKeyData.Certificate); + var certificate = new X509Certificate2(signatureValidation.PublicKeyData.Certificate); var enhancedKeyExtensions = certificate.Extensions.OfType(); if (enhancedKeyExtensions == null) @@ -84,7 +83,7 @@ private static byte[] AddPemHeaders(byte[] certificateData) const string PemFooter = "-----END CERTIFICATE-----"; var decoded = Convert.ToBase64String(certificateData); - + if (!decoded.StartsWith(PemHeader) && !decoded.EndsWith(PemFooter)) { decoded = PemHeader + "\n" + decoded + "\n" + PemFooter; diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/CertificateEntryExtensions.cs b/RuleValidators/DgcReader.RuleValidators.Italy/CertificateEntryExtensions.cs index 5c907a1..9c7984d 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/CertificateEntryExtensions.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/CertificateEntryExtensions.cs @@ -1,5 +1,8 @@ using GreenpassReader.Models; using DgcReader.RuleValidators.Italy.Const; +using System; +using System.Globalization; +using System.Linq; // Copyright (c) 2021 Davide Trevisan // Licensed under the Apache License, Version 2.0 @@ -27,5 +30,66 @@ public static bool IsBooster(this VaccinationEntry vaccination) return vaccination.DoseNumber >= 3; } + + /// + /// Parse the DateOfBirth of the certificate + /// Accepts: yyyy-MM-dd, yyyy-MM-ddTHH:mm:ss, yyy-MM, yyyy + /// + /// + /// + public static DateTime GetBirthDate(this EuDGC dgc) + { + return ParseDgcDateOfBirth(dgc.DateOfBirth); + } + + /// + /// Parse a date in pseudo-iso format + /// Accepts: yyyy-MM-dd, yyyy-MM-ddTHH:mm:ss, yyy-MM, yyyy + /// + /// + /// + /// + public static DateTime ParseDgcDateOfBirth(string dateString) + { + try + { + // Try ISO (yyyy-MM-dd or yyyy-MM-ddTHH:mm:ss) + if (DateTime.TryParseExact(dateString, "s", CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out DateTime date)) + { + return date.Date; + } + + // Split by allowed separators + var dateStrComponents = dateString.Trim() + .Split('-') + .Select(s => int.Parse(s.Trim())) + .ToArray(); + + var year = dateStrComponents[0]; + var month = Math.Max(1, dateStrComponents.Length > 1 ? dateStrComponents[1] : 1); + var day = Math.Max(1, dateStrComponents.Length > 2 ? dateStrComponents[2] : 1); + return new DateTime(year, month, day); + } + catch (Exception e) + { + throw new Exception($"Value {dateString} is not a valid Date: {e}", e); + } + } + + /// + /// Return the age between 2 dates in completed years + /// + /// + /// + /// + public static int GetAge(this DateTime birthDate, DateTime currentDate) + { + currentDate = currentDate.Date; + var age = currentDate.Year - birthDate.Year; + if (birthDate.Date > currentDate.AddYears(-age)) + return age - 1; + return age; + } + } } \ No newline at end of file diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/DgcItalianRulesValidator.cs b/RuleValidators/DgcReader.RuleValidators.Italy/DgcItalianRulesValidator.cs index 5c55e0c..28f8be4 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/DgcItalianRulesValidator.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/DgcItalianRulesValidator.cs @@ -431,8 +431,9 @@ private void CheckTests(EuDGC dgc, ItalianRulesValidationResult result, IEnumera { if (dgc.GetCertificateEntry() is TestEntry) { - Logger.LogWarning($"Test entries are considered not valid when validation mode is {validationMode}"); + result.StatusMessage = $"Test entries are considered not valid when validation mode is {validationMode}"; result.ItalianStatus = DgcItalianResultStatus.NotValid; + Logger.LogWarning(result.StatusMessage); return; } } @@ -453,8 +454,9 @@ private void CheckTests(EuDGC dgc, ItalianRulesValidationResult result, IEnumera endHours = rules.GetMolecularTestEndHour(); break; default: - Logger?.LogWarning($"Test type {test.TestType} not supported by current rules"); + result.StatusMessage = $"Test type {test.TestType} not supported by current rules"; result.ItalianStatus = DgcItalianResultStatus.NotValid; + Logger.LogWarning(result.StatusMessage); return; } @@ -467,13 +469,29 @@ private void CheckTests(EuDGC dgc, ItalianRulesValidationResult result, IEnumera else if (result.ValidUntil < result.ValidationInstant) result.ItalianStatus = DgcItalianResultStatus.NotValid; else - result.ItalianStatus = DgcItalianResultStatus.Valid; + { + if (validationMode == ValidationMode.Work && + dgc.GetBirthDate().GetAge(result.ValidationInstant.Date) >= SdkConstants.VaccineMandatoryAge) + { + result.StatusMessage = $"Test entries are considered not valid for people over the age of {SdkConstants.VaccineMandatoryAge} when validation mode is {validationMode}"; + result.ItalianStatus = DgcItalianResultStatus.NotValid; + Logger.LogWarning(result.StatusMessage); + } + else + { + result.ItalianStatus = DgcItalianResultStatus.Valid; + } + } } else { // Positive test or unknown result if (test.TestResult != TestResults.Detected) - Logger?.LogWarning($"Found test with unkwnown TestResult {test.TestResult}. The certificate is considered invalid"); + { + result.StatusMessage = $"Found test with unkwnown TestResult {test.TestResult}. The certificate is considered invalid"; + Logger?.LogWarning(result.StatusMessage); + + } result.ItalianStatus = DgcItalianResultStatus.NotValid; } diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Models/ValidationMode.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Models/ValidationMode.cs index efe4efb..778054b 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/Models/ValidationMode.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Models/ValidationMode.cs @@ -25,8 +25,13 @@ public enum ValidationMode Booster, /// - /// Validates certificate applying rules for school + /// Validates the certificate applying rules for school /// School, + + /// + /// Validates the certificate applying rules for work + /// + Work } } diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/SdkConstants.cs b/RuleValidators/DgcReader.RuleValidators.Italy/SdkConstants.cs index a52992b..5918036 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/SdkConstants.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/SdkConstants.cs @@ -24,5 +24,10 @@ internal class SdkConstants /// NOTE: this is the version of the android app using the of the SDK. The SDK version is not available in the settings right now. /// public const string ReferenceAppMinVersion = "1.2.0"; + + /// + /// Age from which the vaccine is mandatory when using work mode + /// + public const int VaccineMandatoryAge = 50; } } diff --git a/RuleValidators/Test/DgcReader.RuleValidators.Italy.Test/ItalianRulesValidatorTests.cs b/RuleValidators/Test/DgcReader.RuleValidators.Italy.Test/ItalianRulesValidatorTests.cs index 226daea..fb2e60c 100644 --- a/RuleValidators/Test/DgcReader.RuleValidators.Italy.Test/ItalianRulesValidatorTests.cs +++ b/RuleValidators/Test/DgcReader.RuleValidators.Italy.Test/ItalianRulesValidatorTests.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Threading.Tasks; using DgcReader.Interfaces.RulesValidators; +using System; #if NETFRAMEWORK using System.Net; @@ -58,6 +59,63 @@ public async Task TestSupportedCountry() Assert.IsTrue(supported); } + [TestMethod] + public void TestParseDateOfBirth() + { + var assertions = new (string Value, DateTime? Expected)[] + { + ("1990-01-01", new DateTime(1990,1,1)), + ("1998-02-26", new DateTime(1998,2,26)), + ("1978-01-26T00:00:00", new DateTime(1978,1,26)), + ("1978-01-26T23:59:41", new DateTime(1978,1,26)), + ("1978-01-26T00:04:12", new DateTime(1978,1,26)), + ("1964-01", new DateTime(1964,1,1)), + ("1963-00", new DateTime(1963,1,1)), + ("2004-11", new DateTime(2004,11,1)), + ("1963", new DateTime(1963,1,1)), + }; + + foreach (var a in assertions) + { + try + { + var current = CertificateEntryExtensions.ParseDgcDateOfBirth(a.Value); + Assert.AreEqual(a.Expected, current); + } + catch (Exception) + { + Assert.IsNull(a.Expected); + } + + } + } + + [TestMethod] + public void TestGetAge() + { + var birthDate = new DateTime(1990, 2, 18); + var assertions = new (DateTime CurrentDate, int Expected)[] + { + (DateTime.Parse("2022-02-17"), 31), + (DateTime.Parse("2022-02-18"), 32), + (DateTime.Parse("2022-02-19"), 32), + }; + + foreach (var a in assertions) + { + try + { + var current = birthDate.GetAge(a.CurrentDate); + Assert.AreEqual(a.Expected, current); + } + catch (Exception) + { + Assert.IsNull(a.Expected); + } + + } + } + #if !NET452 From 801d636a21dcb02b31590b5a57262c315601e787 Mon Sep 17 00:00:00 2001 From: Davide Date: Fri, 11 Feb 2022 14:21:21 +0100 Subject: [PATCH 2/7] Update projects version --- .../DgcReader.BlacklistProviders.Italy.csproj | 2 +- .../DgcReader.Providers.Abstractions.csproj | 2 +- DgcReader/DgcReader.csproj | 2 +- .../DgcReader.RuleValidators.Germany.csproj | 2 +- .../DgcReader.RuleValidators.Italy.csproj | 2 +- .../DgcReader.TrustListProviders.Abstractions.csproj | 2 +- .../DgcReader.TrustListProviders.Germany.csproj | 2 +- .../DgcReader.TrustListProviders.Italy.csproj | 2 +- .../DgcReader.TrustListProviders.Sweden.csproj | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BlacklistProviders/DgcReader.BlacklistProviders.Italy/DgcReader.BlacklistProviders.Italy.csproj b/BlacklistProviders/DgcReader.BlacklistProviders.Italy/DgcReader.BlacklistProviders.Italy.csproj index 41d5714..5cff5e1 100644 --- a/BlacklistProviders/DgcReader.BlacklistProviders.Italy/DgcReader.BlacklistProviders.Italy.csproj +++ b/BlacklistProviders/DgcReader.BlacklistProviders.Italy/DgcReader.BlacklistProviders.Italy.csproj @@ -12,7 +12,7 @@ https://github.com/DevTrevi/DGCReader DGC DCC DigitalGreenCertificate DigitalCovidCertificate Greenpass VerificaC19 Blacklist DRL Blacklist provider implementation using the official Italian backend APIs. This provider is included in the list of verified SDKs by Italian authorities - 2.4.0 + 2.5.0 Apache-2.0 True README.md diff --git a/DgcReader.Providers.Abstractions/DgcReader.Providers.Abstractions.csproj b/DgcReader.Providers.Abstractions/DgcReader.Providers.Abstractions.csproj index 6d81cbc..a386078 100644 --- a/DgcReader.Providers.Abstractions/DgcReader.Providers.Abstractions.csproj +++ b/DgcReader.Providers.Abstractions/DgcReader.Providers.Abstractions.csproj @@ -10,7 +10,7 @@ https://github.com/DevTrevi/DGCReader DGC DCC DigitalGreenCertificate DigitalCovidCertificate Greenpass Base classes for implementing providers for DgcReader - 2.4.0 + 2.5.0 Apache-2.0 True https://github.com/DevTrevi/DgcReader diff --git a/DgcReader/DgcReader.csproj b/DgcReader/DgcReader.csproj index a4e5163..49caef3 100644 --- a/DgcReader/DgcReader.csproj +++ b/DgcReader/DgcReader.csproj @@ -10,7 +10,7 @@ https://github.com/DevTrevi/DGCReader DGC DCC DigitalGreenCertificate DigitalCovidCertificate Greenpass Library for decoding and validating European Digital Green Certificates - 2.4.0 + 2.5.0 Apache-2.0 True README.md diff --git a/RuleValidators/DgcReader.RuleValidators.Germany/DgcReader.RuleValidators.Germany.csproj b/RuleValidators/DgcReader.RuleValidators.Germany/DgcReader.RuleValidators.Germany.csproj index 187a533..52fcf26 100644 --- a/RuleValidators/DgcReader.RuleValidators.Germany/DgcReader.RuleValidators.Germany.csproj +++ b/RuleValidators/DgcReader.RuleValidators.Germany/DgcReader.RuleValidators.Germany.csproj @@ -10,7 +10,7 @@ https://github.com/DevTrevi/DGCReader DGC DCC DigitalGreenCertificate DigitalCovidCertificate Greenpass Unofficial implementation of the German rules for validating a Digital Green Certificate - 2.4.0 + 2.5.0 Apache-2.0 True README.md diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/DgcReader.RuleValidators.Italy.csproj b/RuleValidators/DgcReader.RuleValidators.Italy/DgcReader.RuleValidators.Italy.csproj index f1d60d6..1007df3 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/DgcReader.RuleValidators.Italy.csproj +++ b/RuleValidators/DgcReader.RuleValidators.Italy/DgcReader.RuleValidators.Italy.csproj @@ -10,7 +10,7 @@ https://github.com/DevTrevi/DGCReader DGC DCC DigitalGreenCertificate DigitalCovidCertificate Greenpass VerificaC19 Implementation of the Italian rules for validating Digital Green Certificates. This provider is included in the list of verified SDKs by Italian authorities - 2.4.0 + 2.5.0 Apache-2.0 True README.md diff --git a/TrustListProviders/DgcReader.TrustListProviders.Abstractions/DgcReader.TrustListProviders.Abstractions.csproj b/TrustListProviders/DgcReader.TrustListProviders.Abstractions/DgcReader.TrustListProviders.Abstractions.csproj index 01a5016..04082c9 100644 --- a/TrustListProviders/DgcReader.TrustListProviders.Abstractions/DgcReader.TrustListProviders.Abstractions.csproj +++ b/TrustListProviders/DgcReader.TrustListProviders.Abstractions/DgcReader.TrustListProviders.Abstractions.csproj @@ -10,7 +10,7 @@ https://github.com/DevTrevi/DGCReader DGC DCC DigitalGreenCertificate DigitalCovidCertificate Greenpass Base classes for implementing trustlist providers for national backends - 2.4.0 + 2.5.0 Apache-2.0 True README.md diff --git a/TrustListProviders/DgcReader.TrustListProviders.Germany/DgcReader.TrustListProviders.Germany.csproj b/TrustListProviders/DgcReader.TrustListProviders.Germany/DgcReader.TrustListProviders.Germany.csproj index ef0a0fc..216a8cb 100644 --- a/TrustListProviders/DgcReader.TrustListProviders.Germany/DgcReader.TrustListProviders.Germany.csproj +++ b/TrustListProviders/DgcReader.TrustListProviders.Germany/DgcReader.TrustListProviders.Germany.csproj @@ -10,7 +10,7 @@ https://github.com/DevTrevi/DGCReader DGC DCC DigitalGreenCertificate DigitalCovidCertificate Greenpass Unofficial TrustList provider implementation using the German backend endpoints - 2.4.0 + 2.5.0 Apache-2.0 True README.md diff --git a/TrustListProviders/DgcReader.TrustListProviders.Italy/DgcReader.TrustListProviders.Italy.csproj b/TrustListProviders/DgcReader.TrustListProviders.Italy/DgcReader.TrustListProviders.Italy.csproj index df622bd..23ebb3e 100644 --- a/TrustListProviders/DgcReader.TrustListProviders.Italy/DgcReader.TrustListProviders.Italy.csproj +++ b/TrustListProviders/DgcReader.TrustListProviders.Italy/DgcReader.TrustListProviders.Italy.csproj @@ -10,7 +10,7 @@ https://github.com/DevTrevi/DGCReader DGC DCC DigitalGreenCertificate DigitalCovidCertificate Greenpass VerificaC19 Trustlist provider implementation using certificates from the official Italian backend. This provider is included in the list of verified SDKs by Italian authorities - 2.4.0 + 2.5.0 Apache-2.0 True README.md diff --git a/TrustListProviders/DgcReader.TrustListProviders.Sweden/DgcReader.TrustListProviders.Sweden.csproj b/TrustListProviders/DgcReader.TrustListProviders.Sweden/DgcReader.TrustListProviders.Sweden.csproj index 4d901f3..49c528d 100644 --- a/TrustListProviders/DgcReader.TrustListProviders.Sweden/DgcReader.TrustListProviders.Sweden.csproj +++ b/TrustListProviders/DgcReader.TrustListProviders.Sweden/DgcReader.TrustListProviders.Sweden.csproj @@ -10,7 +10,7 @@ https://github.com/DevTrevi/DGCReader DGC DCC DigitalGreenCertificate DigitalCovidCertificate Greenpass Unofficial TrustList provider implementation using the Swedish backend endpoints - 2.4.0 + 2.5.0 Apache-2.0 True README.md From 504b9591b457b1194ac4c2fcf0b9550cd0cc99ba Mon Sep 17 00:00:00 2001 From: Davide Date: Fri, 11 Feb 2022 14:22:18 +0100 Subject: [PATCH 3/7] Add prerelease pipeline configuration --- DgcReader.sln | 1 + azure-pipelines-prerelease.yml | 45 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 azure-pipelines-prerelease.yml diff --git a/DgcReader.sln b/DgcReader.sln index 6ebf0d6..921c7a3 100644 --- a/DgcReader.sln +++ b/DgcReader.sln @@ -5,6 +5,7 @@ VisualStudioVersion = 17.0.31717.71 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5541974F-21E6-4B6E-AB62-1A78A34CB87C}" ProjectSection(SolutionItems) = preProject + azure-pipelines-prerelease.yml = azure-pipelines-prerelease.yml azure-pipelines.yml = azure-pipelines.yml ItalianConfiguration.md = ItalianConfiguration.md LICENSE = LICENSE diff --git a/azure-pipelines-prerelease.yml b/azure-pipelines-prerelease.yml new file mode 100644 index 0000000..12cc7b8 --- /dev/null +++ b/azure-pipelines-prerelease.yml @@ -0,0 +1,45 @@ +trigger: + paths: + exclude: + - README.md + - '**/*.Test' + tags: + include: + - 'v2.*' +pr: none + +variables: +- group: NuGet +- name: nugetSource + value: 'https://api.nuget.org/v3/index.json' +- name: BuildConfiguration + value: 'release' + +pool: + vmImage: windows-latest + +steps: +- task: UseDotNet@2 + displayName: "Use .NET Sdk" + inputs: + version: 6.0.x + +- task: DotNetCoreCLI@2 + inputs: + command: 'pack' + packagesToPack: '**/*.csproj' + versioningScheme: 'byEnvVar' + versionEnvVar: 'prerelease-version' + verbosityPack: 'Normal' + +#- task: NuGetToolInstaller@1 +# displayName: 'Install NuGet' +#- script: nuget push $(Build.ArtifactStagingDirectory)\**\*.nupkg -Source $(nugetSource) -ApiKey $(nuget-org-apikey) -SkipDuplicate -NoSymbols +# displayName: 'Push to NuGet.org' + + +- task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)' + ArtifactName: 'drop' + publishLocation: 'Container' \ No newline at end of file From f96ceb986bc526e37a001dba42db9d0b69b35a94 Mon Sep 17 00:00:00 2001 From: Davide Date: Fri, 11 Feb 2022 14:29:02 +0100 Subject: [PATCH 4/7] disabling CI --- azure-pipelines-prerelease.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/azure-pipelines-prerelease.yml b/azure-pipelines-prerelease.yml index 12cc7b8..5f7d455 100644 --- a/azure-pipelines-prerelease.yml +++ b/azure-pipelines-prerelease.yml @@ -1,12 +1,4 @@ -trigger: - paths: - exclude: - - README.md - - '**/*.Test' - tags: - include: - - 'v2.*' -pr: none +trigger: none variables: - group: NuGet From 924dcd4dbdad96c07374c532f8f4f86c867ffca8 Mon Sep 17 00:00:00 2001 From: Davide Date: Fri, 11 Feb 2022 14:43:37 +0100 Subject: [PATCH 5/7] Enable push for prerelease packages --- azure-pipelines-prerelease.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/azure-pipelines-prerelease.yml b/azure-pipelines-prerelease.yml index 5f7d455..7e14a05 100644 --- a/azure-pipelines-prerelease.yml +++ b/azure-pipelines-prerelease.yml @@ -24,10 +24,10 @@ steps: versionEnvVar: 'prerelease-version' verbosityPack: 'Normal' -#- task: NuGetToolInstaller@1 -# displayName: 'Install NuGet' -#- script: nuget push $(Build.ArtifactStagingDirectory)\**\*.nupkg -Source $(nugetSource) -ApiKey $(nuget-org-apikey) -SkipDuplicate -NoSymbols -# displayName: 'Push to NuGet.org' +- task: NuGetToolInstaller@1 + displayName: 'Install NuGet' +- script: nuget push $(Build.ArtifactStagingDirectory)\**\*.nupkg -Source $(nugetSource) -ApiKey $(nuget-org-apikey) -SkipDuplicate -NoSymbols + displayName: 'Push to NuGet.org' - task: PublishBuildArtifacts@1 From ab65291be2d15e82c642ee10b62e81395c9842a9 Mon Sep 17 00:00:00 2001 From: Davide Date: Fri, 11 Feb 2022 16:35:37 +0100 Subject: [PATCH 6/7] Fix error management on downloading updates from remote server --- .../ItalianDrlBlacklistProvider.cs | 27 +++++++++++-- .../ThreadsafeMultiValueSetProvider.cs | 18 ++++++++- DgcReader/DgcReaderService.cs | 40 +++++++++++++------ DgcReader/Exceptions/DgcBlackListException.cs | 8 ++++ .../ItalianTrustListProvider.cs | 2 +- 5 files changed, 76 insertions(+), 19 deletions(-) diff --git a/BlacklistProviders/DgcReader.BlacklistProviders.Italy/ItalianDrlBlacklistProvider.cs b/BlacklistProviders/DgcReader.BlacklistProviders.Italy/ItalianDrlBlacklistProvider.cs index ca53b45..38467bd 100644 --- a/BlacklistProviders/DgcReader.BlacklistProviders.Italy/ItalianDrlBlacklistProvider.cs +++ b/BlacklistProviders/DgcReader.BlacklistProviders.Italy/ItalianDrlBlacklistProvider.cs @@ -129,8 +129,18 @@ public async Task IsBlacklisted(string certificateIdentifier, Cancellation var refreshTask = await RefreshBlacklistTaskRunner.RunSingleTask(cancellationToken); - // Wait for the task to complete - await refreshTask; + try + { + // Wait for the task to complete + await refreshTask; + } + catch (Exception e) + { + Logger?.LogError(e, $"Can not refresh ItalianDrlBlacklist from remote server. " + + $"Values from DRL version {status.CurrentVersion}, checked on {status.LastCheck} have reached MaxFileAge and can no longer be used."); + throw; + } + } else if (status.LastCheck.Add(Options.RefreshInterval) < DateTime.Now || status.HasPendingDownload()) @@ -143,8 +153,17 @@ public async Task IsBlacklisted(string certificateIdentifier, Cancellation var refreshTask = await RefreshBlacklistTaskRunner.RunSingleTask(cancellationToken); if (!Options.UseAvailableValuesWhileRefreshing) { - // Wait for the task to complete - await refreshTask; + try + { + // Wait for the task to complete + await refreshTask; + } + catch (Exception e) + { + // If refresh fail, continue until MaxFileAge + Logger?.LogWarning(e, $"Can not refresh ItalianDrlBlacklist from remote server: {e.Message}. Values from DRL version {status.CurrentVersion}, checked on {status.LastCheck} will be used"); + } + } } } diff --git a/DgcReader.Providers.Abstractions/ThreadsafeMultiValueSetProvider.cs b/DgcReader.Providers.Abstractions/ThreadsafeMultiValueSetProvider.cs index bf86b6d..29454b2 100644 --- a/DgcReader.Providers.Abstractions/ThreadsafeMultiValueSetProvider.cs +++ b/DgcReader.Providers.Abstractions/ThreadsafeMultiValueSetProvider.cs @@ -141,7 +141,23 @@ protected ThreadsafeMultiValueSetProvider(ILogger? logger) { // If not UseAvailableRulesWhileRefreshing, always wait for the task to complete Logger?.LogInformation($"Values for {GetValuesetName(key)} are expired, waiting for refresh to complete"); - return await refreshTask; + + try + { + valueSet = await refreshTask; + } + catch (Exception e) + { + if (valueSet != null) + { + var lastUpdate = GetLastUpdate(valueSet); + Logger?.LogWarning(e, $"Can not refresh {GetValuesetName(key)} from remote server: {e.Message}. Current values downloaded on {lastUpdate} will be used"); + } + else + { + Logger?.LogError(e, $"Can not refresh {GetValuesetName(key)} from remote server. No values available to be used"); + } + } } } diff --git a/DgcReader/DgcReaderService.cs b/DgcReader/DgcReaderService.cs index 2b8ee49..88f3cab 100644 --- a/DgcReader/DgcReaderService.cs +++ b/DgcReader/DgcReaderService.cs @@ -444,26 +444,40 @@ private async Task GetBlacklistValidationResult(EuDGC // Tracking the validated CertificateIdentifier context.CertificateIdentifier = certEntry.CertificateIdentifier; - - foreach (var blacklistProvider in BlackListProviders) + try { - var blacklisted = await blacklistProvider.IsBlacklisted(certEntry.CertificateIdentifier, cancellationToken); + foreach (var blacklistProvider in BlackListProviders) + { + var blacklisted = await blacklistProvider.IsBlacklisted(certEntry.CertificateIdentifier, cancellationToken); - // At least one check performed - context.BlacklistVerified = true; + // At least one check performed + context.BlacklistVerified = true; - if (blacklisted) - { - context.IsBlacklisted = true; - context.BlacklistMatchProviderType = blacklistProvider.GetType(); + if (blacklisted) + { + context.IsBlacklisted = true; + context.BlacklistMatchProviderType = blacklistProvider.GetType(); - Logger?.LogWarning($"The certificate is blacklisted"); - if (throwOnError) - throw new DgcBlackListException($"The certificate is blacklisted", context); + Logger?.LogWarning($"The certificate is blacklisted"); + if (throwOnError) + throw new DgcBlackListException($"The certificate is blacklisted", context); - return context; + return context; + } } } + catch (Exception e) + { + // Technical failure, stop blacklist check and set as not verified + context.BlacklistVerified = false; + context.IsBlacklisted = null; + Logger?.LogError(e, $"Error while checking blacklist: {e.Message}"); + + if (throwOnError) + throw new DgcBlackListException($"Error while checking blacklist: {e.Message}", context, e); + + return context; + } context.IsBlacklisted = false; } else diff --git a/DgcReader/Exceptions/DgcBlackListException.cs b/DgcReader/Exceptions/DgcBlackListException.cs index ad99f99..00e92f0 100644 --- a/DgcReader/Exceptions/DgcBlackListException.cs +++ b/DgcReader/Exceptions/DgcBlackListException.cs @@ -20,6 +20,14 @@ public DgcBlackListException(string message, { Result = result; } + + public DgcBlackListException(string message, + BlacklistValidationResult result, Exception innerException) : + base(message, innerException) + { + Result = result; + } + #pragma warning disable CS8618 public DgcBlackListException(SerializationInfo info, StreamingContext context) : base(info, context) diff --git a/TrustListProviders/DgcReader.TrustListProviders.Italy/ItalianTrustListProvider.cs b/TrustListProviders/DgcReader.TrustListProviders.Italy/ItalianTrustListProvider.cs index d73d260..592aa9a 100644 --- a/TrustListProviders/DgcReader.TrustListProviders.Italy/ItalianTrustListProvider.cs +++ b/TrustListProviders/DgcReader.TrustListProviders.Italy/ItalianTrustListProvider.cs @@ -306,7 +306,7 @@ private async Task> GetCertificatesFromServer(Cancel { var stringContent = await response.Content.ReadAsStringAsync(); var content = Convert.FromBase64String(stringContent); - + int? newToken = null; if (response.Headers.TryGetValues(HeaderResumeToken, out var newTokens)) { From 65b7fd6b6a904e6d9f404877f0e4fefda6bdd89f Mon Sep 17 00:00:00 2001 From: Davide Date: Mon, 14 Feb 2022 20:03:52 +0100 Subject: [PATCH 7/7] Feature/italy/sdk v1.1.5 (#82) * Moved files to subfolders * Update vaccine products list * Update Italian Certificate status enum * Improvements to extension methods * Add settings for EMA #79 * Refactoring of validation flow to match structure of official SDK * Implementation of Expired status, resolves #78 * Resolves #72 Implementation of Work mode Resolves #77 implementation of ENTRY_ITALY mode Resolves #78 Italian EXPIRED status Resolves #79 implementation of new EMA rules Resolves #80 unlock R-PV certs for booster mode Resolves #81 Update validation flow for 3G/2G/WORK --- DgcReader/EuDGCExtensionMethods.cs | 19 +- DgcReader/Models/EuDGC.cs | 4 +- .../DgcGermanRulesValidator.cs | 3 + .../Const/CountryCodes.cs | 27 + .../Const/SettingNames.cs | 3 + .../Const/VaccineProducts.cs | 30 ++ .../DgcItalianRulesValidator.cs | 476 ++++-------------- .../Models/ItalianDGC.cs | 2 +- .../Models/ItalianDGCExtensionMethods.cs | 23 +- .../Models/ItalianRulesValidationResult.cs | 37 +- .../ItalianRulesValidationResultExtensions.cs | 3 +- .../Models/ValidationMode.cs | 7 +- .../SdkConstants.cs | 2 +- .../DgcItalianRulesValidatorServiceBuilder.cs | 0 ...cItalianRulesValidatorServiceExtensions.cs | 0 .../CertifiateExtendedKeyUsageUtils.cs | 0 .../{ => Utils}/CertificateEntryExtensions.cs | 58 +++ .../ItalianValidationResultsExtensions.cs | 3 +- .../{ => Utils}/RulesExtensionMethods.cs | 102 ++-- .../Validation/BaseValidator.cs | 41 ++ .../Validation/ExemptionValidator.cs | 58 +++ .../Validation/ICertificateEntryValidator.cs | 24 + .../Validation/RecoveryValidator.cs | 93 ++++ .../Validation/TestValidator.cs | 106 ++++ .../Validation/VaccinationValidator.cs | 425 ++++++++++++++++ .../Validation/ValidationCertificateModel.cs | 32 ++ 26 files changed, 1137 insertions(+), 441 deletions(-) create mode 100644 RuleValidators/DgcReader.RuleValidators.Italy/Const/CountryCodes.cs rename RuleValidators/DgcReader.RuleValidators.Italy/{ => ServiceBuilder}/DgcItalianRulesValidatorServiceBuilder.cs (100%) rename RuleValidators/DgcReader.RuleValidators.Italy/{ => ServiceBuilder}/DgcItalianRulesValidatorServiceExtensions.cs (100%) rename RuleValidators/DgcReader.RuleValidators.Italy/{ => Utils}/CertifiateExtendedKeyUsageUtils.cs (100%) rename RuleValidators/DgcReader.RuleValidators.Italy/{ => Utils}/CertificateEntryExtensions.cs (59%) rename RuleValidators/DgcReader.RuleValidators.Italy/{ => Utils}/ItalianValidationResultsExtensions.cs (97%) rename RuleValidators/DgcReader.RuleValidators.Italy/{ => Utils}/RulesExtensionMethods.cs (61%) create mode 100644 RuleValidators/DgcReader.RuleValidators.Italy/Validation/BaseValidator.cs create mode 100644 RuleValidators/DgcReader.RuleValidators.Italy/Validation/ExemptionValidator.cs create mode 100644 RuleValidators/DgcReader.RuleValidators.Italy/Validation/ICertificateEntryValidator.cs create mode 100644 RuleValidators/DgcReader.RuleValidators.Italy/Validation/RecoveryValidator.cs create mode 100644 RuleValidators/DgcReader.RuleValidators.Italy/Validation/TestValidator.cs create mode 100644 RuleValidators/DgcReader.RuleValidators.Italy/Validation/VaccinationValidator.cs create mode 100644 RuleValidators/DgcReader.RuleValidators.Italy/Validation/ValidationCertificateModel.cs diff --git a/DgcReader/EuDGCExtensionMethods.cs b/DgcReader/EuDGCExtensionMethods.cs index 43cd274..fa8ef9c 100644 --- a/DgcReader/EuDGCExtensionMethods.cs +++ b/DgcReader/EuDGCExtensionMethods.cs @@ -15,26 +15,33 @@ public static class EuDGCExtensionMethods /// Return the single certificate entry from the EuDGC (RecoveryEntry, Test or Vaccination) /// /// + /// Restrict search to the specified disease agent /// - public static ICertificateEntry GetCertificateEntry(this EuDGC dgc) + public static ICertificateEntry? GetCertificateEntry(this EuDGC dgc, string? targetedDiseaseAgent = null) { var empty = Enumerable.Empty(); - return empty + + var q = empty .Union(dgc.Recoveries ?? empty) .Union(dgc.Tests ?? empty) - .Union(dgc.Vaccinations ?? empty) - .Last(); + .Union(dgc.Vaccinations ?? empty); + + if (!string.IsNullOrEmpty(targetedDiseaseAgent)) + q = q.Where(e => e.TargetedDiseaseAgent == targetedDiseaseAgent); + + return q.LastOrDefault(); } /// /// Return the single certificate entry from the EuDGC (RecoveryEntry, Test or Vaccination) /// /// + /// Restrict search to the specified disease agent /// - public static TCertificate? GetCertificateEntry(this EuDGC dgc) + public static TCertificate? GetCertificateEntry(this EuDGC dgc, string? targetedDiseaseAgent = null) where TCertificate : class, ICertificateEntry { - return dgc.GetCertificateEntry() as TCertificate; + return dgc.GetCertificateEntry(targetedDiseaseAgent) as TCertificate; } } } diff --git a/DgcReader/Models/EuDGC.cs b/DgcReader/Models/EuDGC.cs index 3b4d672..1988167 100644 --- a/DgcReader/Models/EuDGC.cs +++ b/DgcReader/Models/EuDGC.cs @@ -329,13 +329,13 @@ public partial class EuDGC /// /// /// - public static EuDGC? FromJson(string json) => JsonConvert.DeserializeObject(json, Converter.Settings); + public static EuDGC? FromJson(string json) => JsonConvert.DeserializeObject(json, EuDGCConverter.Settings); } /// /// Json Converter settings to be used for deserializing EuDGC /// - public static class Converter + public static class EuDGCConverter { /// /// Json Converter settings to be used for deserializing EuDGC diff --git a/RuleValidators/DgcReader.RuleValidators.Germany/DgcGermanRulesValidator.cs b/RuleValidators/DgcReader.RuleValidators.Germany/DgcGermanRulesValidator.cs index 5c427f8..62a63bd 100644 --- a/RuleValidators/DgcReader.RuleValidators.Germany/DgcGermanRulesValidator.cs +++ b/RuleValidators/DgcReader.RuleValidators.Germany/DgcGermanRulesValidator.cs @@ -177,6 +177,9 @@ public async Task GetRulesValidationResult(EuDGC? dgc, var certEntry = dgc.GetCertificateEntry(); + if (certEntry == null) + throw new Exception("Unable to get the certificate entry"); + var issuerCountryCode = certEntry.Country; var certificateType = dgc.GetCertificateType(); diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Const/CountryCodes.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Const/CountryCodes.cs new file mode 100644 index 0000000..d0f6661 --- /dev/null +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Const/CountryCodes.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2021 Davide Trevisan +// Licensed under the Apache License, Version 2.0 + +namespace DgcReader.RuleValidators.Italy.Const +{ + /// + /// Country codes for special behavior + /// + public static class CountryCodes + { + /// + /// Country code of Italy + /// + public const string Italy = "IT"; + + /// + /// Country code of San Marino + /// + public const string SanMarino = "SM"; + + /// + /// Country code for countries different from Italy + /// + public const string NotItaly = "NOT_IT"; + + } +} diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Const/SettingNames.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Const/SettingNames.cs index c1c139a..507da70 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/Const/SettingNames.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Const/SettingNames.cs @@ -27,6 +27,9 @@ public static class SettingNames public const string VaccineEndDayBoosterNotIT = "vaccine_end_day_booster_NOT_IT"; public const string VaccineEndDaySchool = "vaccine_end_day_school"; + + public const string VaccineEndDayCompleteExtendedEMA = "vaccine_end_day_complete_extended_EMA"; + public const string EMAVaccines = "EMA_vaccines"; #endregion #region Test diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Const/VaccineProducts.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Const/VaccineProducts.cs index 5456a52..7aacb8b 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/Const/VaccineProducts.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Const/VaccineProducts.cs @@ -17,5 +17,35 @@ public static class VaccineProducts /// Sputnik-V /// public const string Sputnik = "Sputnik-V"; + + /// + /// Moderna + /// + public const string Moderna = "EU/1/20/1507"; + + /// + /// Pfizer + /// + public const string Pfizer = "EU/1/20/1528"; + + /// + /// AstraZeneca + /// + public const string AstraZeneca = "EU/1/21/1529"; + + /// + /// Covishield + /// + public const string Covishield = "Covishield"; + + /// + /// R-Covi + /// + public const string R_Covi = "R-COVI"; + + /// + /// Covid-19-Recombinant + /// + public const string Covid19_Recombinant = "Covid-19-recombinant"; } } diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/DgcItalianRulesValidator.cs b/RuleValidators/DgcReader.RuleValidators.Italy/DgcItalianRulesValidator.cs index 28f8be4..8b8e8af 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/DgcItalianRulesValidator.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/DgcItalianRulesValidator.cs @@ -13,6 +13,7 @@ using DgcReader.RuleValidators.Italy.Providers; using DgcReader.Exceptions; using DgcReader.Models; +using DgcReader.RuleValidators.Italy.Validation; #if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER || NET47_OR_GREATER using Microsoft.Extensions.Options; @@ -30,82 +31,17 @@ namespace DgcReader.RuleValidators.Italy /// public class DgcItalianRulesValidator : IRulesValidator, IBlacklistProvider { - // File containing the business logic on the offical SDK repo: - // https://github.com/ministero-salute/it-dgc-verificac19-sdk-android/blob/develop/sdk/src/main/java/it/ministerodellasalute/verificaC19sdk/model/VerificationViewModel.kt - private readonly ILogger? Logger; private readonly DgcItalianRulesValidatorOptions Options; - private readonly RulesProvider _rulesProvider; -#if NET452 - /// - /// Constructor - /// - public DgcItalianRulesValidator(HttpClient httpClient, - DgcItalianRulesValidatorOptions? options = null, - ILogger? logger = null) - { - Options = options ?? new DgcItalianRulesValidatorOptions(); - Logger = logger; - - _rulesProvider = new RulesProvider(httpClient, Options, logger); - } - - /// - /// Factory method for creating an instance of - /// whithout using the DI mechanism. Useful for legacy applications - /// - /// The http client instance that will be used for requests to the server - /// The options for the provider - /// Instance of used by the provider (optional). - /// - public static DgcItalianRulesValidator Create(HttpClient httpClient, - DgcItalianRulesValidatorOptions? options = null, - ILogger? logger = null) - { - return new DgcItalianRulesValidator(httpClient, options, logger); - } - -#else - /// - /// Constructor - /// - public DgcItalianRulesValidator(HttpClient httpClient, - IOptions? options = null, - ILogger? logger = null) - { - Options = options?.Value ?? new DgcItalianRulesValidatorOptions(); - Logger = logger; - - _rulesProvider = new RulesProvider(httpClient, Options, logger); - } - - /// - /// Factory method for creating an instance of - /// whithout using the DI mechanism. Useful for legacy applications - /// - /// The http client instance that will be used for requests to the server - /// The options for the provider - /// Instance of used by the provider (optional). - /// - public static DgcItalianRulesValidator Create(HttpClient httpClient, - DgcItalianRulesValidatorOptions? options = null, - ILogger? logger = null) - { - return new DgcItalianRulesValidator(httpClient, - options == null ? null : Microsoft.Extensions.Options.Options.Create(options), - logger); - } -#endif - #region Implementation of IRulesValidator /// public async Task GetRulesValidationResult(EuDGC? dgc, string dgcJson, DateTimeOffset validationInstant, - string countryCode = "IT", + string countryCode = CountryCodes.Italy, SignatureValidationResult? signatureValidationResult = null, BlacklistValidationResult? blacklistValidationResult = null, CancellationToken cancellationToken = default) @@ -124,7 +60,7 @@ public async Task GetRulesValidationResult(EuDGC? dgc, { var result = new ItalianRulesValidationResult { - ItalianStatus = DgcItalianResultStatus.NotValidated, + ItalianStatus = DgcItalianResultStatus.NeedRulesVerification, StatusMessage = $"Rules validation for country {countryCode} is not supported by this provider", ValidationMode = validationMode, }; @@ -149,7 +85,7 @@ public Task RefreshRules(string? countryCode = null, CancellationToken cancellat /// public Task> GetSupportedCountries(CancellationToken cancellationToken = default) { - return Task.FromResult(new[] { "IT" }.AsEnumerable()); + return Task.FromResult(new[] { CountryCodes.Italy }.AsEnumerable()); } /// @@ -212,13 +148,15 @@ public async Task GetRulesValidationResult(EuDGC? dgc, BlacklistValidationResult? blacklistValidationResult = null, CancellationToken cancellationToken = default) { + // Check preconditions var result = new ItalianRulesValidationResult { ValidationInstant = validationInstant, ValidationMode = validationMode, + ItalianStatus = DgcItalianResultStatus.NeedRulesVerification, }; - if (dgc == null) + if (dgc == null || string.IsNullOrEmpty(dgcJson)) { result.ItalianStatus = DgcItalianResultStatus.NotEuDCC; return result; @@ -245,7 +183,7 @@ public async Task GetRulesValidationResult(EuDGC? dgc, var rules = rulesContainer?.Rules; if (rules == null) { - result.ItalianStatus = DgcItalianResultStatus.NotValidated; + result.ItalianStatus = DgcItalianResultStatus.NeedRulesVerification; result.StatusMessage = "Unable to get validation rules"; return result; } @@ -253,33 +191,43 @@ public async Task GetRulesValidationResult(EuDGC? dgc, // Checking min version: CheckMinSdkVersion(rules, validationInstant, validationMode); - if (dgc.Recoveries?.Any(r => r.TargetedDiseaseAgent == DiseaseAgents.Covid19) == true) - { - CheckRecoveryStatements(dgc, result, rules, signatureValidationResult, validationMode); - } - else if (dgc.Tests?.Any(r => r.TargetedDiseaseAgent == DiseaseAgents.Covid19) == true) - { - CheckTests(dgc, result, rules, signatureValidationResult, validationMode); - } - else if (dgc.Vaccinations?.Any(r => r.TargetedDiseaseAgent == DiseaseAgents.Covid19) == true) + // Preparing model for validators + var certificateModel = new ValidationCertificateModel { - CheckVaccinations(dgc, result, rules, signatureValidationResult, validationMode); - } - else + Dgc = dgc, + SignatureData = signatureValidationResult, + ValidationInstant = validationInstant, + }; + + // Try to deserialzie dgc from Json, to get the more specific ItalianDGC + if (signatureValidationResult.Issuer == CountryCodes.Italy) { - // Try to check for exemptions (custom for Italy) - var italianDgc = ItalianDGC.FromJson(dgcJson); - if (italianDgc?.Exemptions?.Any(r => r.TargetedDiseaseAgent == DiseaseAgents.Covid19) == true) + try { - CheckExemptionStatements(italianDgc, result, rules, signatureValidationResult, validationMode); + // Try to check for exemptions (custom for Italy) + var italianDGC = ItalianDGC.FromJson(dgcJson); + if (italianDGC == null) + throw new Exception($"Deserialized object is null"); + + // Replace the "original" dgc with the extended version + certificateModel.Dgc = italianDGC; } - else + catch (Exception e) { - // An EU DCC must have one of the sections above. - Logger?.LogWarning($"No vaccinations, tests, recovery or exemptions statements found in the certificate."); - result.ItalianStatus = DgcItalianResultStatus.NotEuDCC; + Logger?.LogWarning(e, $"Error while trying to deserialize json content to {nameof(ItalianDGC)}. The original {nameof(EuDGC)} will be used for validation: {e.Message}"); } } + + var validator = GetValidator(certificateModel); + if (validator == null) + { + // An EU DCC must have one of the sections above. + Logger?.LogWarning($"No vaccinations, tests, recovery or exemptions statements found in the certificate."); + result.ItalianStatus = DgcItalianResultStatus.NotEuDCC; + return result; + } + + return validator.CheckCertificate(certificateModel, rules, validationMode); } catch (DgcRulesValidationException e) { @@ -299,282 +247,19 @@ public async Task GetRulesValidationResult(EuDGC? dgc, #region Validation methods - /// - /// Computes the status by checking the vaccinations in the DCC - /// - /// - /// The output result compiled by the function - /// - /// The result from the signature validation step - /// - private void CheckVaccinations(EuDGC dgc, ItalianRulesValidationResult result, IEnumerable rules, - SignatureValidationResult? signatureValidation, ValidationMode validationMode) + private ICertificateEntryValidator? GetValidator(ValidationCertificateModel certificateModel) { - var vaccination = dgc.Vaccinations?.Last(r => r.TargetedDiseaseAgent == DiseaseAgents.Covid19); - if (vaccination == null) return; - - int startDay, endDay; - if (vaccination.DoseNumber > 0 && vaccination.TotalDoseSeries > 0) - { - // Calculate start/end days - if (vaccination.DoseNumber < vaccination.TotalDoseSeries) - { - // Vaccination is not completed (partial number of doses) - startDay = rules.GetVaccineStartDayNotComplete(vaccination.MedicinalProduct); - endDay = rules.GetVaccineEndDayNotComplete(vaccination.MedicinalProduct); - } - else - { - // Vaccination completed (full number of doses) - - // If mode is not basic, use always rules for Italy - var countryCode = validationMode == ValidationMode.Basic3G ? vaccination.Country : "IT"; - - - // Check rules for "BOOSTER" certificates - if (vaccination.IsBooster()) - { - startDay = rules.GetVaccineStartDayBoosterUnified(countryCode); - endDay = rules.GetVaccineEndDayBoosterUnified(countryCode); - } - else - { - startDay = rules.GetVaccineStartDayCompleteUnified(countryCode, vaccination.MedicinalProduct); - endDay = rules.GetVaccineEndDayCompleteUnified(countryCode, validationMode); - } - } - - // Calculate start/end dates - if (vaccination.MedicinalProduct == VaccineProducts.JeJVacineCode && - (vaccination.DoseNumber > vaccination.TotalDoseSeries || vaccination.DoseNumber >= 2)) - { - // For J&J booster, in case of more vaccinations than expected, the vaccine is immediately valid - result.ValidFrom = vaccination.Date.Date; - result.ValidUntil = vaccination.Date.Date.AddDays(endDay); - } - else - { - result.ValidFrom = vaccination.Date.Date.AddDays(startDay); - result.ValidUntil = vaccination.Date.Date.AddDays(endDay); - } - - // Calculate the status - - // Exception: Checking sputnik not from San Marino - if (vaccination.MedicinalProduct == VaccineProducts.Sputnik && vaccination.Country != "SM") - { - result.ItalianStatus = DgcItalianResultStatus.NotValid; - return; - } - - if (result.ValidFrom > result.ValidationInstant.Date) - result.ItalianStatus = DgcItalianResultStatus.NotValidYet; - else if (result.ValidUntil < result.ValidationInstant.Date) - result.ItalianStatus = DgcItalianResultStatus.NotValid; - else - { - if (vaccination.DoseNumber < vaccination.TotalDoseSeries) - { - // Incomplete cycle, invalid for BOOSTER and SCHOOL mode - result.ItalianStatus = new[] { - ValidationMode.Booster, - ValidationMode.School - }.Contains(validationMode) ? DgcItalianResultStatus.NotValid : DgcItalianResultStatus.Valid; - } - else - { - // Complete cycle - if (validationMode == ValidationMode.Booster) - { - if (vaccination.IsBooster()) - { - // If dose number is higher than total dose series, or minimum booster dose number reached - result.ItalianStatus = DgcItalianResultStatus.Valid; - } - else - { - // Otherwise, if less thant the minimum "booster" doses, requires a test - result.ItalianStatus = DgcItalianResultStatus.TestNeeded; - } - } - else - { - // Non-booster mode: valid - result.ItalianStatus = DgcItalianResultStatus.Valid; - } - } - - } - } + if (certificateModel.Dgc.HasVaccinations()) + return new VaccinationValidator(Logger); + if(certificateModel.Dgc.HasRecoveries()) + return new RecoveryValidator(Logger); + if (certificateModel.Dgc.HasTests()) + return new TestValidator(Logger); + if (certificateModel.Dgc.HasExemptions()) + return new ExemptionValidator(Logger); + return null; } - /// - /// Computes the status by checking the tests in the DCC - /// - /// - /// The output result compiled by the function - /// - /// The result from the signature validation step - /// - private void CheckTests(EuDGC dgc, ItalianRulesValidationResult result, IEnumerable rules, - SignatureValidationResult? signatureValidation, ValidationMode validationMode) - { - var test = dgc.Tests?.Last(r => r.TargetedDiseaseAgent == DiseaseAgents.Covid19); - if (test == null) return; - - // Super Greenpass check - if (new[] { - ValidationMode.Strict2G, - ValidationMode.Booster, - ValidationMode.School - }.Contains(validationMode)) - { - if (dgc.GetCertificateEntry() is TestEntry) - { - result.StatusMessage = $"Test entries are considered not valid when validation mode is {validationMode}"; - result.ItalianStatus = DgcItalianResultStatus.NotValid; - Logger.LogWarning(result.StatusMessage); - return; - } - } - - if (test.TestResult == TestResults.NotDetected) - { - // Negative test - int startHours, endHours; - - switch (test.TestType) - { - case TestTypes.Rapid: - startHours = rules.GetRapidTestStartHour(); - endHours = rules.GetRapidTestEndHour(); - break; - case TestTypes.Molecular: - startHours = rules.GetMolecularTestStartHour(); - endHours = rules.GetMolecularTestEndHour(); - break; - default: - result.StatusMessage = $"Test type {test.TestType} not supported by current rules"; - result.ItalianStatus = DgcItalianResultStatus.NotValid; - Logger.LogWarning(result.StatusMessage); - return; - } - - result.ValidFrom = test.SampleCollectionDate.AddHours(startHours); - result.ValidUntil = test.SampleCollectionDate.AddHours(endHours); - - // Calculate the status - if (result.ValidFrom > result.ValidationInstant) - result.ItalianStatus = DgcItalianResultStatus.NotValidYet; - else if (result.ValidUntil < result.ValidationInstant) - result.ItalianStatus = DgcItalianResultStatus.NotValid; - else - { - if (validationMode == ValidationMode.Work && - dgc.GetBirthDate().GetAge(result.ValidationInstant.Date) >= SdkConstants.VaccineMandatoryAge) - { - result.StatusMessage = $"Test entries are considered not valid for people over the age of {SdkConstants.VaccineMandatoryAge} when validation mode is {validationMode}"; - result.ItalianStatus = DgcItalianResultStatus.NotValid; - Logger.LogWarning(result.StatusMessage); - } - else - { - result.ItalianStatus = DgcItalianResultStatus.Valid; - } - } - } - else - { - // Positive test or unknown result - if (test.TestResult != TestResults.Detected) - { - result.StatusMessage = $"Found test with unkwnown TestResult {test.TestResult}. The certificate is considered invalid"; - Logger?.LogWarning(result.StatusMessage); - - } - - result.ItalianStatus = DgcItalianResultStatus.NotValid; - } - } - - /// - /// Computes the status by checking the recovery statements in the DCC - /// - /// - /// The output result compiled by the function - /// - /// The result from the signature validation step - /// - private void CheckRecoveryStatements(EuDGC dgc, ItalianRulesValidationResult result, IEnumerable rules, - SignatureValidationResult? signatureValidation, ValidationMode validationMode) - { - var recovery = dgc.Recoveries?.Last(r => r.TargetedDiseaseAgent == DiseaseAgents.Covid19); - if (recovery == null) return; - - // If mode is not basic, use always rules for Italy - var countryCode = validationMode == ValidationMode.Basic3G ? recovery.Country : "IT"; - - // Check if is PV (post-vaccination) recovery by checking signer certificate - var isPvRecovery = IsRecoveryPvSignature(signatureValidation); - - var startDaysToAdd = isPvRecovery ? rules.GetRecoveryPvCertStartDay() : rules.GetRecoveryCertStartDayUnified(countryCode); - var endDaysToAdd = - validationMode == ValidationMode.School ? rules.GetRecoveryCertEndDaySchool() : - isPvRecovery ? rules.GetRecoveryPvCertEndDay() : - rules.GetRecoveryCertEndDayUnified(countryCode); - - result.ValidFrom = recovery.ValidFrom.Date.AddDays(startDaysToAdd); - if(validationMode == ValidationMode.School) - { - // Take the more restrictive from end of "quarantine" after first positive test and the original expiration from the Recovery entry - result.ValidUntil = recovery.FirstPositiveTestResult.Date.AddDays(endDaysToAdd); - if (recovery.ValidUntil < result.ValidUntil) - result.ValidUntil = recovery.ValidUntil; - } - else - { - result.ValidUntil = result.ValidFrom.Value.AddDays(endDaysToAdd); - } - - if (result.ValidFrom > result.ValidationInstant.Date) - result.ItalianStatus = DgcItalianResultStatus.NotValidYet; - else if (result.ValidationInstant.Date > result.ValidFrom.Value.AddDays(endDaysToAdd)) - result.ItalianStatus = DgcItalianResultStatus.NotValid; - else - result.ItalianStatus = validationMode == ValidationMode.Booster ? DgcItalianResultStatus.TestNeeded : DgcItalianResultStatus.Valid; - } - - /// - /// Computes the status by checking the exemption statements in the Italian DCC - /// - /// - /// The output result compiled by the function - /// - /// The result from the signature validation step - /// - private void CheckExemptionStatements(ItalianDGC italianDgc, ItalianRulesValidationResult result, IEnumerable rules, - SignatureValidationResult? signatureValidation, ValidationMode validationMode) - { - var exemption = italianDgc.Exemptions?.Last(r => r.TargetedDiseaseAgent == DiseaseAgents.Covid19); - if (exemption == null) return; - - result.ValidFrom = exemption.ValidFrom; - result.ValidUntil = exemption.ValidUntil; - - if (exemption.ValidFrom.Date > result.ValidationInstant.Date) - result.ItalianStatus = DgcItalianResultStatus.NotValidYet; - else if (exemption.ValidUntil != null && result.ValidationInstant.Date > exemption.ValidUntil?.Date) - result.ItalianStatus = DgcItalianResultStatus.NotValid; - else - { - if (validationMode == ValidationMode.Booster) - result.ItalianStatus = DgcItalianResultStatus.TestNeeded; - else - result.ItalianStatus = DgcItalianResultStatus.Valid; - } - } - - /// /// Check the minimum version of the SDK implementation required. /// If is false, an exception will be thrown if the implementation is obsolete @@ -627,7 +312,7 @@ private void CheckMinSdkVersion(IEnumerable rules, DateTimeOffset v { ValidationInstant = validationInstant, ValidationMode = validationMode, - ItalianStatus = DgcItalianResultStatus.NotValidated, + ItalianStatus = DgcItalianResultStatus.NeedRulesVerification, StatusMessage = message, }; throw new DgcRulesValidationException(message, result); @@ -636,26 +321,71 @@ private void CheckMinSdkVersion(IEnumerable rules, DateTimeOffset v } + #endregion + + #region Constructor and factory methods +#if NET452 + /// + /// Constructor + /// + public DgcItalianRulesValidator(HttpClient httpClient, + DgcItalianRulesValidatorOptions? options = null, + ILogger? logger = null) + { + Options = options ?? new DgcItalianRulesValidatorOptions(); + Logger = logger; + _rulesProvider = new RulesProvider(httpClient, Options, logger); + } /// - /// Check if the signer certificate is one of the signer of post-vaccination certificates + /// Factory method for creating an instance of + /// whithout using the DI mechanism. Useful for legacy applications /// - /// + /// The http client instance that will be used for requests to the server + /// The options for the provider + /// Instance of used by the provider (optional). /// - private bool IsRecoveryPvSignature(SignatureValidationResult? signatureValidationResult) + public static DgcItalianRulesValidator Create(HttpClient httpClient, + DgcItalianRulesValidatorOptions? options = null, + ILogger? logger = null) { - var extendedKeyUsages = CertificateExtendedKeyUsageUtils.GetExtendedKeyUsages(signatureValidationResult, Logger); + return new DgcItalianRulesValidator(httpClient, options, logger); + } - if (signatureValidationResult == null) - return false; +#else + /// + /// Constructor + /// + public DgcItalianRulesValidator(HttpClient httpClient, + IOptions? options = null, + ILogger? logger = null) + { + Options = options?.Value ?? new DgcItalianRulesValidatorOptions(); + Logger = logger; - if (signatureValidationResult.Issuer != "IT") - return false; + _rulesProvider = new RulesProvider(httpClient, Options, logger); + } - return extendedKeyUsages.Any(usage => CertificateExtendedKeyUsageIdentifiers.RecoveryIssuersIds.Contains(usage)); + /// + /// Factory method for creating an instance of + /// whithout using the DI mechanism. Useful for legacy applications + /// + /// The http client instance that will be used for requests to the server + /// The options for the provider + /// Instance of used by the provider (optional). + /// + public static DgcItalianRulesValidator Create(HttpClient httpClient, + DgcItalianRulesValidatorOptions? options = null, + ILogger? logger = null) + { + return new DgcItalianRulesValidator(httpClient, + options == null ? null : Microsoft.Extensions.Options.Options.Create(options), + logger); } +#endif + #endregion } } \ No newline at end of file diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianDGC.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianDGC.cs index 969f97c..37e68f7 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianDGC.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianDGC.cs @@ -26,7 +26,7 @@ public class ItalianDGC : EuDGC /// /// /// - public static new ItalianDGC? FromJson(string json) => JsonConvert.DeserializeObject(json, Converter.Settings); + public static new ItalianDGC? FromJson(string json) => JsonConvert.DeserializeObject(json, EuDGCConverter.Settings); } /// diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianDGCExtensionMethods.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianDGCExtensionMethods.cs index cd65b16..cf81591 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianDGCExtensionMethods.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianDGCExtensionMethods.cs @@ -1,10 +1,11 @@ -using GreenpassReader.Models; +using DgcReader.RuleValidators.Italy.Models; +using GreenpassReader.Models; using System.Linq; // Copyright (c) 2021 Davide Trevisan // Licensed under the Apache License, Version 2.0 -namespace DgcReader.RuleValidators.Italy.Models +namespace DgcReader { /// /// Extension methods for @@ -15,27 +16,33 @@ public static class ItalianDGCExtensionMethods /// Return the single certificate entry from the EuDGC (RecoveryEntry, Test, Vaccination or Exemption) /// /// + /// Restrict search to the specified disease agent /// - public static ICertificateEntry GetCertificateEntry(this ItalianDGC dgc) + public static ICertificateEntry? GetCertificateEntry(this ItalianDGC dgc, string? targetedDiseaseAgent = null) { var empty = Enumerable.Empty(); - return empty + var q = empty .Union(dgc.Recoveries ?? empty) .Union(dgc.Tests ?? empty) .Union(dgc.Vaccinations ?? empty) - .Union(dgc.Exemptions ?? empty) - .Last(); + .Union(dgc.Exemptions ?? empty); + + if (!string.IsNullOrEmpty(targetedDiseaseAgent)) + q = q.Where(e => e.TargetedDiseaseAgent == targetedDiseaseAgent); + + return q.LastOrDefault(); } /// /// Return the single certificate entry from the EuDGC (RecoveryEntry, Test, Vaccination or Exemption) /// /// + /// Restrict search to the specified disease agent /// - public static TCertificate? GetCertificateEntry(this EuDGC dgc) + public static TCertificate? GetCertificateEntry(this ItalianDGC dgc, string? targetedDiseaseAgent = null) where TCertificate : class, ICertificateEntry { - return dgc.GetCertificateEntry() as TCertificate; + return dgc.GetCertificateEntry(targetedDiseaseAgent) as TCertificate; } } } diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianRulesValidationResult.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianRulesValidationResult.cs index 2b2f49c..2158a83 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianRulesValidationResult.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianRulesValidationResult.cs @@ -15,7 +15,7 @@ public class ItalianRulesValidationResult : IRulesValidationResult /// /// Validation status according to the official Italian SDK /// - public DgcItalianResultStatus ItalianStatus { get; internal set; } = DgcItalianResultStatus.NotValidated; + public DgcItalianResultStatus ItalianStatus { get; internal set; } = DgcItalianResultStatus.NeedRulesVerification; #region Implementation of IRulesValidationResult /// @@ -69,22 +69,14 @@ public enum DgcItalianResultStatus /// /// The certificate has not been validated by the Italian rules validator /// - NotValidated, - - /// - /// The certificate is not a valid EU DCC - /// - NotEuDCC, + NeedRulesVerification, /// /// The certificate has an invalid signature /// InvalidSignature, - /// - /// The certificate is blacklisted or temporarily revoked - /// - Revoked, + /// /// The certificate is not valid @@ -97,13 +89,28 @@ public enum DgcItalianResultStatus NotValidYet, /// - /// Certificate is not enough for the required validation mode, and needs to be integrated by a test + /// The certificate is valid /// - TestNeeded, + Valid, /// - /// The certificate is valid + /// The certificate is expired /// - Valid, + Expired, + + /// + /// The certificate is blacklisted or temporarily revoked + /// + Revoked, + + /// + /// The certificate is not a valid EU DCC + /// + NotEuDCC, + + /// + /// Certificate is not enough for the required validation mode, and needs to be integrated by a test + /// + TestNeeded, } } diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianRulesValidationResultExtensions.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianRulesValidationResultExtensions.cs index e99c099..29d4b9e 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianRulesValidationResultExtensions.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Models/ItalianRulesValidationResultExtensions.cs @@ -20,7 +20,7 @@ public static DgcResultStatus ToDgcResultStatus(this DgcItalianResultStatus stat { switch (status) { - case DgcItalianResultStatus.NotValidated: + case DgcItalianResultStatus.NeedRulesVerification: return DgcResultStatus.NeedRulesVerification; case DgcItalianResultStatus.NotEuDCC: @@ -35,6 +35,7 @@ public static DgcResultStatus ToDgcResultStatus(this DgcItalianResultStatus stat case DgcItalianResultStatus.NotValid: case DgcItalianResultStatus.NotValidYet: case DgcItalianResultStatus.TestNeeded: + case DgcItalianResultStatus.Expired: return DgcResultStatus.NotValid; case DgcItalianResultStatus.Valid: diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Models/ValidationMode.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Models/ValidationMode.cs index 778054b..76e3cac 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/Models/ValidationMode.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Models/ValidationMode.cs @@ -32,6 +32,11 @@ public enum ValidationMode /// /// Validates the certificate applying rules for work /// - Work + Work, + + /// + /// Validates the certificate applying rules needed for entry in Italy + /// + EntryItaly, } } diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/SdkConstants.cs b/RuleValidators/DgcReader.RuleValidators.Italy/SdkConstants.cs index 5918036..89cb66f 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/SdkConstants.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/SdkConstants.cs @@ -17,7 +17,7 @@ internal class SdkConstants /// /// The version of the sdk used as reference for implementing the rules. /// - public const string ReferenceSdkMinVersion = "1.1.4"; + public const string ReferenceSdkMinVersion = "1.1.5"; /// /// The version of the app used as reference for implementing the rules. diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/DgcItalianRulesValidatorServiceBuilder.cs b/RuleValidators/DgcReader.RuleValidators.Italy/ServiceBuilder/DgcItalianRulesValidatorServiceBuilder.cs similarity index 100% rename from RuleValidators/DgcReader.RuleValidators.Italy/DgcItalianRulesValidatorServiceBuilder.cs rename to RuleValidators/DgcReader.RuleValidators.Italy/ServiceBuilder/DgcItalianRulesValidatorServiceBuilder.cs diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/DgcItalianRulesValidatorServiceExtensions.cs b/RuleValidators/DgcReader.RuleValidators.Italy/ServiceBuilder/DgcItalianRulesValidatorServiceExtensions.cs similarity index 100% rename from RuleValidators/DgcReader.RuleValidators.Italy/DgcItalianRulesValidatorServiceExtensions.cs rename to RuleValidators/DgcReader.RuleValidators.Italy/ServiceBuilder/DgcItalianRulesValidatorServiceExtensions.cs diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/CertifiateExtendedKeyUsageUtils.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Utils/CertifiateExtendedKeyUsageUtils.cs similarity index 100% rename from RuleValidators/DgcReader.RuleValidators.Italy/CertifiateExtendedKeyUsageUtils.cs rename to RuleValidators/DgcReader.RuleValidators.Italy/Utils/CertifiateExtendedKeyUsageUtils.cs diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/CertificateEntryExtensions.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Utils/CertificateEntryExtensions.cs similarity index 59% rename from RuleValidators/DgcReader.RuleValidators.Italy/CertificateEntryExtensions.cs rename to RuleValidators/DgcReader.RuleValidators.Italy/Utils/CertificateEntryExtensions.cs index 9c7984d..cc91863 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/CertificateEntryExtensions.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Utils/CertificateEntryExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; using System.Linq; +using DgcReader.RuleValidators.Italy.Models; // Copyright (c) 2021 Davide Trevisan // Licensed under the Apache License, Version 2.0 @@ -14,6 +15,27 @@ namespace DgcReader.RuleValidators.Italy /// public static class CertificateEntryExtensions { + /// + /// Check if the validated entry was issued by Italy + /// + /// + /// + public static bool IsFromItaly(this ICertificateEntry entry) => entry.Country == CountryCodes.Italy; + + /// + /// Check if the validated entry was issued by San Marino + /// + /// + /// + public static bool IsFromSanMarino(this ICertificateEntry entry) => entry.Country == "SM"; + + /// + /// Check if the vaccination cycle is completed + /// + /// + /// + public static bool IsComplete(this VaccinationEntry vaccination) => vaccination.DoseNumber >= vaccination.TotalDoseSeries; + /// /// Check if the vaccination is considered a BOOSTER (more doses than initially required) /// @@ -91,5 +113,41 @@ public static int GetAge(this DateTime birthDate, DateTime currentDate) return age; } + + + /// + /// Try to cast the EuDGC as the ItalianDGG customization + /// + /// + /// + public static ItalianDGC? AsItalianDgc(this EuDGC dgc) => dgc as ItalianDGC; + + /// + /// Check if the certificate contains vaccination statements + /// + /// + /// + public static bool HasVaccinations(this EuDGC dgc) => dgc.Vaccinations?.Any() == true; + + /// + /// Check if the certificate contains recovery statements + /// + /// + /// + public static bool HasRecoveries(this EuDGC dgc) => dgc.Recoveries?.Any() == true; + + /// + /// Check if the certificate contains test statements + /// + /// + /// + public static bool HasTests(this EuDGC dgc) => dgc.Tests?.Any() == true; + + /// + /// Check if the certificate contains exemption statements + /// + /// + /// + public static bool HasExemptions(this EuDGC dgc) => dgc.AsItalianDgc()?.Exemptions?.Any() == true; } } \ No newline at end of file diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/ItalianValidationResultsExtensions.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Utils/ItalianValidationResultsExtensions.cs similarity index 97% rename from RuleValidators/DgcReader.RuleValidators.Italy/ItalianValidationResultsExtensions.cs rename to RuleValidators/DgcReader.RuleValidators.Italy/Utils/ItalianValidationResultsExtensions.cs index f51e017..a1775eb 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/ItalianValidationResultsExtensions.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Utils/ItalianValidationResultsExtensions.cs @@ -2,6 +2,7 @@ using DgcReader.Interfaces.RulesValidators; using DgcReader.Models; using DgcReader.RuleValidators.Italy; +using DgcReader.RuleValidators.Italy.Const; using DgcReader.RuleValidators.Italy.Models; using System; using System.Linq; @@ -71,7 +72,7 @@ public static async Task VerifyForItaly(this DgcReaderServi bool throwOnError = true, CancellationToken cancellationToken = default) { - return await dgcReaderService.Verify(qrCodeData, "IT", validationInstant, + return await dgcReaderService.Verify(qrCodeData, CountryCodes.Italy, validationInstant, async (dgc, dgcJson, countryCode, validationInstant, signatureValidation, blacklistValidation, cancellationToken) => { var italianValidator = dgcReaderService.RulesValidators.OfType().FirstOrDefault(); diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/RulesExtensionMethods.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Utils/RulesExtensionMethods.cs similarity index 61% rename from RuleValidators/DgcReader.RuleValidators.Italy/RulesExtensionMethods.cs rename to RuleValidators/DgcReader.RuleValidators.Italy/Utils/RulesExtensionMethods.cs index fc2de03..3b21500 100644 --- a/RuleValidators/DgcReader.RuleValidators.Italy/RulesExtensionMethods.cs +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Utils/RulesExtensionMethods.cs @@ -1,7 +1,9 @@ using DgcReader.RuleValidators.Italy.Const; using DgcReader.RuleValidators.Italy.Models; +using GreenpassReader.Models; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; // Copyright (c) 2021 Davide Trevisan @@ -11,6 +13,8 @@ namespace DgcReader.RuleValidators.Italy { internal static class RulesExtensionMethods { + const int NoValue = 0; + /// /// Search the rule with the specified type. /// If no match exists for the specified type, returns null. @@ -36,17 +40,22 @@ internal static class RulesExtensionMethods return null; } + /// + /// Returns the required integer value, or as fallback + /// + /// + /// + /// + /// public static int GetRuleInteger(this IEnumerable settings, string name, string type = SettingTypes.Generic) { - var rule = settings.GetRule(name, type); - if (rule == null) - throw new Exception($"No rules found for setting {name} and type {type}"); - - var value = rule.ToInteger(); + var value = settings.GetRuleNullableInteger(name, type); if (value == null) - throw new Exception($"Invalid value {rule.Value} in rule {name} type {type}"); + { + Debug.WriteLine($"No rules found for setting {name} and type {type}. Returning default value {NoValue}"); + } - return value.Value; + return value ?? NoValue; } public static int? GetRuleNullableInteger(this IEnumerable settings, string name, string type = SettingTypes.Generic) @@ -83,20 +92,20 @@ public static int GetRecoveryPvCertEndDay(this IEnumerable settings public static int GetRecoveryCertStartDayUnified(this IEnumerable settings, string issuerCountryCode) { - return issuerCountryCode.ToUpperInvariantNotNull() == "IT" ? - settings.GetRuleNullableInteger(SettingNames.RecoveryCertStartDayIT) ?? 0 : - settings.GetRuleNullableInteger(SettingNames.RecoveryCertStartDayNotIT) ?? 0; + return issuerCountryCode.ToUpperInvariantNotNull() == CountryCodes.Italy ? + settings.GetRuleInteger(SettingNames.RecoveryCertStartDayIT) : + settings.GetRuleInteger(SettingNames.RecoveryCertStartDayNotIT); } public static int GetRecoveryCertEndDayUnified(this IEnumerable settings, string issuerCountryCode) { - return issuerCountryCode.ToUpperInvariantNotNull() == "IT" ? - settings.GetRuleNullableInteger(SettingNames.RecoveryCertEndDayIT) ?? 180 : - settings.GetRuleNullableInteger(SettingNames.RecoveryCertEndDayNotIT) ?? 270; + return issuerCountryCode.ToUpperInvariantNotNull() == CountryCodes.Italy ? + settings.GetRuleInteger(SettingNames.RecoveryCertEndDayIT) : + settings.GetRuleInteger(SettingNames.RecoveryCertEndDayNotIT); } public static int GetRecoveryCertEndDaySchool(this IEnumerable settings) - => settings.GetRuleNullableInteger(SettingNames.RecoveryCertEndDaySchool) ?? 120; + => settings.GetRuleInteger(SettingNames.RecoveryCertEndDaySchool); #endregion #region Test @@ -117,7 +126,6 @@ public static int GetVaccineStartDayNotComplete(this IEnumerable se public static int GetVaccineEndDayNotComplete(this IEnumerable settings, string vaccineType) => settings.GetRuleInteger(SettingNames.VaccineEndDayNotComplete, vaccineType); - [Obsolete] public static int GetVaccineStartDayComplete(this IEnumerable settings, string vaccineType) => settings.GetRuleInteger(SettingNames.VaccineStartDayComplete, vaccineType); @@ -127,39 +135,69 @@ public static int GetVaccineEndDayComplete(this IEnumerable setting public static int GetVaccineStartDayCompleteUnified(this IEnumerable settings, string issuerCountryCode, string vaccineType) { - var daysToAdd = vaccineType == VaccineProducts.JeJVacineCode ? 15 : 0; + var daysToAdd = vaccineType == VaccineProducts.JeJVacineCode ? settings.GetVaccineStartDayComplete(VaccineProducts.JeJVacineCode) : NoValue; - var startDay = issuerCountryCode.ToUpperInvariantNotNull() == "IT" ? - settings.GetRuleNullableInteger(SettingNames.VaccineStartDayCompleteIT) ?? 0 : - settings.GetRuleNullableInteger(SettingNames.VaccineStartDayCompleteNotIT) ?? 0; + var startDay = issuerCountryCode.ToUpperInvariantNotNull() == CountryCodes.Italy ? + settings.GetRuleInteger(SettingNames.VaccineStartDayCompleteIT) : + settings.GetRuleInteger(SettingNames.VaccineStartDayCompleteNotIT); return startDay + daysToAdd; } - public static int GetVaccineEndDayCompleteUnified(this IEnumerable settings, string issuerCountryCode, ValidationMode validationMode) + public static int GetVaccineEndDayCompleteUnified(this IEnumerable settings, string issuerCountryCode) { - if (validationMode == ValidationMode.School) - return settings.GetRuleNullableInteger(SettingNames.VaccineEndDaySchool) ?? 120; - - return issuerCountryCode.ToUpperInvariantNotNull() == "IT" ? - settings.GetRuleNullableInteger(SettingNames.VaccineEndDayCompleteIT) ?? 180 : - settings.GetRuleNullableInteger(SettingNames.VaccineEndDayCompleteNotIT) ?? 270; + return issuerCountryCode.ToUpperInvariantNotNull() == CountryCodes.Italy ? + settings.GetRuleInteger(SettingNames.VaccineEndDayCompleteIT) : + settings.GetRuleInteger(SettingNames.VaccineEndDayCompleteNotIT); } public static int GetVaccineStartDayBoosterUnified(this IEnumerable settings, string issuerCountryCode) { - return issuerCountryCode.ToUpperInvariantNotNull() == "IT" ? - settings.GetRuleNullableInteger(SettingNames.VaccineStartDayBoosterIT) ?? 0 : - settings.GetRuleNullableInteger(SettingNames.VaccineStartDayBoosterNotIT) ?? 0; + return issuerCountryCode.ToUpperInvariantNotNull() == CountryCodes.Italy ? + settings.GetRuleInteger(SettingNames.VaccineStartDayBoosterIT) : + settings.GetRuleInteger(SettingNames.VaccineStartDayBoosterNotIT); } public static int GetVaccineEndDayBoosterUnified(this IEnumerable settings, string issuerCountryCode) { - return issuerCountryCode.ToUpperInvariantNotNull() == "IT" ? - settings.GetRuleNullableInteger(SettingNames.VaccineEndDayBoosterIT) ?? 180 : - settings.GetRuleNullableInteger(SettingNames.VaccineEndDayBoosterNotIT) ?? 270; + return issuerCountryCode.ToUpperInvariantNotNull() == CountryCodes.Italy ? + settings.GetRuleInteger(SettingNames.VaccineEndDayBoosterIT) : + settings.GetRuleInteger(SettingNames.VaccineEndDayBoosterNotIT); + } + + public static int GetVaccineEndDaySchool(this IEnumerable settings) + => settings.GetRuleInteger(SettingNames.VaccineEndDaySchool); + + public static int GetVaccineEndDayCompleteExtendedEMA(this IEnumerable settings) + => settings.GetRuleInteger(SettingNames.VaccineEndDayCompleteExtendedEMA); + + /// + /// The EMA approved list of vaccines + /// + /// + /// + public static string[] GetEMAVaccines(this IEnumerable settings) + { + return settings.GetRule(SettingNames.EMAVaccines, SettingTypes.Generic)?.Value.Split(';') ?? new string[0]; + } + + /// + /// Check if the vaccine product is considered valid by EMA + /// + /// + /// + /// + /// + public static bool IsEMA(this IEnumerable settings, string medicinalProduct, string countryOfVaccination) + { + // also Sputnik is EMA, but only if from San Marino + return settings.GetEMAVaccines().Contains(medicinalProduct) || + medicinalProduct == VaccineProducts.Sputnik && countryOfVaccination == CountryCodes.SanMarino; } + /// + public static bool IsEMA(this IEnumerable settings, VaccinationEntry vaccination) + => settings.IsEMA(vaccination.MedicinalProduct, vaccination.Country); #endregion #endregion diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Validation/BaseValidator.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/BaseValidator.cs new file mode 100644 index 0000000..8702bc5 --- /dev/null +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/BaseValidator.cs @@ -0,0 +1,41 @@ +using DgcReader.RuleValidators.Italy.Models; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; + +namespace DgcReader.RuleValidators.Italy.Validation +{ + /// + /// Base class for building ICertificateEntryValidators + /// + public abstract class BaseValidator : ICertificateEntryValidator + { + /// + public BaseValidator(ILogger? logger) + { + Logger = logger; + } + + /// + /// The logger instance (optional) + /// + protected ILogger? Logger { get; } + + /// + public abstract ItalianRulesValidationResult CheckCertificate(ValidationCertificateModel certificateModel, IEnumerable rules, ValidationMode validationMode); + + /// + /// Instantiate a RecoveryValidator in the initial state, including data that will always be returned + /// + /// + /// + protected virtual ItalianRulesValidationResult InitializeResult(ValidationCertificateModel certificateModel, ValidationMode validationMode) + { + return new ItalianRulesValidationResult + { + ValidationInstant = certificateModel.ValidationInstant, + ValidationMode = validationMode, + ItalianStatus = DgcItalianResultStatus.NeedRulesVerification, + }; + } + } +} diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Validation/ExemptionValidator.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/ExemptionValidator.cs new file mode 100644 index 0000000..3a51b40 --- /dev/null +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/ExemptionValidator.cs @@ -0,0 +1,58 @@ +using DgcReader.RuleValidators.Italy.Const; +using DgcReader.RuleValidators.Italy.Models; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; + +namespace DgcReader.RuleValidators.Italy.Validation +{ + /// + /// Validator for Italian Exemptions + /// + public class ExemptionValidator : BaseValidator + { + /// + public ExemptionValidator(ILogger? logger) : base(logger) + { + } + + /// + public override ItalianRulesValidationResult CheckCertificate( + ValidationCertificateModel certificateModel, + IEnumerable rules, + ValidationMode validationMode) + { + var result = InitializeResult(certificateModel, validationMode); + + var exemption = certificateModel.Dgc.AsItalianDgc()?.GetCertificateEntry(DiseaseAgents.Covid19); + if (exemption == null) + return result; + + result.ValidFrom = exemption.ValidFrom.Date; + result.ValidUntil = exemption.ValidUntil?.Date; + + if (exemption.ValidFrom.Date > result.ValidationInstant.Date) + result.ItalianStatus = DgcItalianResultStatus.NotValidYet; + else if (exemption.ValidUntil != null && result.ValidationInstant.Date > exemption.ValidUntil?.Date) + result.ItalianStatus = DgcItalianResultStatus.Expired; + else + { + switch (validationMode) + { + case ValidationMode.EntryItaly: + result.ItalianStatus = DgcItalianResultStatus.NotValid; + result.StatusMessage = $"Exemptions are not valid for entry in Italy"; + break; + case ValidationMode.Booster: + result.ItalianStatus = DgcItalianResultStatus.TestNeeded; + result.StatusMessage = $"Certificate is valid, but mode {validationMode} requires also a valid test"; + break; + default: + result.ItalianStatus = DgcItalianResultStatus.Valid; + break; + } + } + + return result; + } + } +} diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Validation/ICertificateEntryValidator.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/ICertificateEntryValidator.cs new file mode 100644 index 0000000..777f5db --- /dev/null +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/ICertificateEntryValidator.cs @@ -0,0 +1,24 @@ +using DgcReader.RuleValidators.Italy.Models; +using System.Collections.Generic; + +namespace DgcReader.RuleValidators.Italy.Validation +{ + + /// + /// Validator forItalian Rules + /// + public interface ICertificateEntryValidator + { + /// + /// Return the validation result for rules implemented by this type of validator + /// + /// + /// + /// + /// + ItalianRulesValidationResult CheckCertificate( + ValidationCertificateModel certificateModel, + IEnumerable rules, + ValidationMode validationMode); + } +} diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Validation/RecoveryValidator.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/RecoveryValidator.cs new file mode 100644 index 0000000..08ea18c --- /dev/null +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/RecoveryValidator.cs @@ -0,0 +1,93 @@ +using DgcReader.Models; +using DgcReader.RuleValidators.Italy.Const; +using DgcReader.RuleValidators.Italy.Models; +using GreenpassReader.Models; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Linq; + +namespace DgcReader.RuleValidators.Italy.Validation +{ + /// + /// Validator for Recovery entries + /// + public class RecoveryValidator : BaseValidator + { + /// + public RecoveryValidator(ILogger? logger) : base(logger) + { + } + + /// + public override ItalianRulesValidationResult CheckCertificate( + ValidationCertificateModel certificateModel, + IEnumerable rules, + ValidationMode validationMode) + { + var result = InitializeResult(certificateModel, validationMode); + + var recovery = certificateModel.Dgc.GetCertificateEntry(DiseaseAgents.Covid19); + if (recovery == null) + return result; + + // If mode is not 3G, use always rules for Italy + var countryCode = validationMode == ValidationMode.Basic3G ? recovery.Country : CountryCodes.Italy; + + // Check if is PV (post-vaccination) recovery by checking signer certificate + var isPvRecovery = IsRecoveryPvSignature(certificateModel.SignatureData); + + var startDaysToAdd = isPvRecovery ? rules.GetRecoveryPvCertStartDay() : rules.GetRecoveryCertStartDayUnified(countryCode); + var endDaysToAdd = + validationMode == ValidationMode.School ? rules.GetRecoveryCertEndDaySchool() : + isPvRecovery ? rules.GetRecoveryPvCertEndDay() : + rules.GetRecoveryCertEndDayUnified(countryCode); + + var startDate = + validationMode == ValidationMode.School ? recovery.FirstPositiveTestResult.Date : + recovery.ValidFrom.Date; + var endDate = startDate.AddDays(endDaysToAdd); + + + result.ValidFrom = startDate.AddDays(startDaysToAdd); + result.ValidUntil = endDate; + + if (result.ValidationInstant.Date < result.ValidFrom) + result.ItalianStatus = DgcItalianResultStatus.NotValidYet; + else if (result.ValidationInstant.Date > result.ValidUntil) + result.ItalianStatus = DgcItalianResultStatus.Expired; + else + { + if (validationMode == ValidationMode.Booster && !isPvRecovery) + { + result.ItalianStatus = DgcItalianResultStatus.TestNeeded; + result.StatusMessage = "Certificate is valid, but a test is also needed if mode is booster and the recovery certificate is not issued after a vaccination"; + } + else + result.ItalianStatus = DgcItalianResultStatus.Valid; + } + + return result; + } + + + /// + /// Check if the signer certificate is one of the signer of post-vaccination certificates + /// + /// + /// + private bool IsRecoveryPvSignature(SignatureValidationResult? signatureValidationResult) + { + var extendedKeyUsages = CertificateExtendedKeyUsageUtils.GetExtendedKeyUsages(signatureValidationResult, Logger); + + if (signatureValidationResult == null) + return false; + + if (signatureValidationResult.Issuer != CountryCodes.Italy) + return false; + + return extendedKeyUsages.Any(usage => CertificateExtendedKeyUsageIdentifiers.RecoveryIssuersIds.Contains(usage)); + } + } + + +} diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Validation/TestValidator.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/TestValidator.cs new file mode 100644 index 0000000..1e6288e --- /dev/null +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/TestValidator.cs @@ -0,0 +1,106 @@ +using DgcReader.RuleValidators.Italy.Const; +using DgcReader.RuleValidators.Italy.Models; +using GreenpassReader.Models; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.Linq; + +namespace DgcReader.RuleValidators.Italy.Validation +{ + /// + /// Validator for Test entries + /// + public class TestValidator : BaseValidator + { + /// + public TestValidator(ILogger? logger) : base(logger) + { + } + + /// + public override ItalianRulesValidationResult CheckCertificate( + ValidationCertificateModel certificateModel, + IEnumerable rules, + ValidationMode validationMode) + { + var result = InitializeResult(certificateModel, validationMode); + + var test = certificateModel.Dgc.GetCertificateEntry(DiseaseAgents.Covid19); + if (test == null) + return result; + + + // Super Greenpass check + if (new[] { + ValidationMode.Strict2G, + ValidationMode.Booster, + ValidationMode.School + }.Contains(validationMode)) + { + result.StatusMessage = $"Test entries are considered not valid when validation mode is {validationMode}"; + result.ItalianStatus = DgcItalianResultStatus.NotValid; + Logger?.LogWarning(result.StatusMessage); + return result; + } + + if (test.TestResult == TestResults.NotDetected) + { + // Negative test + switch (test.TestType) + { + case TestTypes.Rapid: + result.ValidFrom = test.SampleCollectionDate.AddHours(rules.GetRapidTestStartHour()); + result.ValidUntil = test.SampleCollectionDate.AddHours(rules.GetRapidTestEndHour()); + break; + case TestTypes.Molecular: + result.ValidFrom = test.SampleCollectionDate.AddHours(rules.GetMolecularTestStartHour()); + result.ValidUntil = test.SampleCollectionDate.AddHours(rules.GetMolecularTestEndHour()); + break; + default: + result.StatusMessage = $"Test type {test.TestType} not supported by current rules"; + result.ItalianStatus = DgcItalianResultStatus.NotValid; + Logger?.LogWarning(result.StatusMessage); + return result; + } + + // Calculate the status + if (result.ValidFrom > result.ValidationInstant) + result.ItalianStatus = DgcItalianResultStatus.NotValidYet; + else if (result.ValidUntil < result.ValidationInstant) + result.ItalianStatus = DgcItalianResultStatus.Expired; + else + { + if (validationMode == ValidationMode.Work && + certificateModel.Dgc.GetBirthDate().GetAge(result.ValidationInstant.Date) >= SdkConstants.VaccineMandatoryAge) + { + result.StatusMessage = $"Test entries are considered not valid for people over the age of {SdkConstants.VaccineMandatoryAge} when validation mode is {validationMode}"; + result.ItalianStatus = DgcItalianResultStatus.NotValid; + Logger?.LogWarning(result.StatusMessage); + } + else + { + result.ItalianStatus = DgcItalianResultStatus.Valid; + } + } + } + else + { + // Positive test or unknown result + if (test.TestResult != TestResults.Detected) + { + result.StatusMessage = $"Found test with unkwnown TestResult {test.TestResult}. The certificate is considered not valid"; + Logger?.LogWarning(result.StatusMessage); + + } + else + { + result.StatusMessage = "Test result is positive, certificate is not valid"; + } + + result.ItalianStatus = DgcItalianResultStatus.NotValid; + } + + return result; + } + } +} diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Validation/VaccinationValidator.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/VaccinationValidator.cs new file mode 100644 index 0000000..b16e95b --- /dev/null +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/VaccinationValidator.cs @@ -0,0 +1,425 @@ +using DgcReader.RuleValidators.Italy.Const; +using DgcReader.RuleValidators.Italy.Models; +using GreenpassReader.Models; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; + +namespace DgcReader.RuleValidators.Italy.Validation +{ + /// + /// Validator for Vaccination entries + /// + public class VaccinationValidator : BaseValidator + { + /// + public VaccinationValidator(ILogger? logger) : base(logger) + { + } + + /// + public override ItalianRulesValidationResult CheckCertificate( + ValidationCertificateModel certificateModel, + IEnumerable rules, + ValidationMode validationMode) + { + switch (validationMode) + { + case ValidationMode.Basic3G: + return ValidateFor3G(certificateModel, rules, validationMode); + case ValidationMode.Strict2G: + return ValidateFor2G(certificateModel, rules, validationMode); + case ValidationMode.Booster: + return ValidateForBooster(certificateModel, rules, validationMode); + case ValidationMode.School: + return ValidateForSchool(certificateModel, rules, validationMode); + case ValidationMode.Work: + return ValidateForWork(certificateModel, rules, validationMode); + case ValidationMode.EntryItaly: + return ValidateForEntryItaly(certificateModel, rules, validationMode); + default: + var result = InitializeResult(certificateModel, validationMode); + + result.ItalianStatus = DgcItalianResultStatus.NeedRulesVerification; + result.StatusMessage = $"Validation for mode {validationMode} is not implemented"; + return result; + } + } + + /// + /// Porting of vaccineStandardStrategy + /// + /// + /// + /// + /// + private ItalianRulesValidationResult ValidateFor3G( + ValidationCertificateModel certificateModel, + IEnumerable rules, + ValidationMode validationMode) + { + var result = InitializeResult(certificateModel, validationMode); + + var vaccination = certificateModel.Dgc.GetCertificateEntry(DiseaseAgents.Covid19); + if (vaccination == null) + return result; + + var validationDate = certificateModel.ValidationInstant.Date; + var vaccinationDate = vaccination.Date.Date; + + // startDate + if (vaccination.IsComplete()) + { + result.ValidFrom = vaccinationDate.AddDays( + vaccination.IsBooster() ? rules.GetVaccineStartDayBoosterUnified(CountryCodes.Italy) : + rules.GetVaccineStartDayCompleteUnified(CountryCodes.Italy, vaccination.MedicinalProduct)); + } + else + { + result.ValidFrom = vaccinationDate.AddDays(rules.GetVaccineStartDayNotComplete(vaccination.MedicinalProduct)); + } + + // endDate + if (vaccination.IsComplete()) + { + result.ValidUntil = vaccinationDate.AddDays( + vaccination.IsBooster() ? rules.GetVaccineEndDayBoosterUnified(CountryCodes.Italy) : + rules.GetVaccineEndDayCompleteUnified(CountryCodes.Italy)); + } + else + { + result.ValidUntil = vaccinationDate.AddDays(rules.GetVaccineEndDayNotComplete(vaccination.MedicinalProduct)); + } + + if (validationDate < result.ValidFrom) + result.ItalianStatus = DgcItalianResultStatus.NotValidYet; + else if (validationDate > result.ValidUntil) + result.ItalianStatus = DgcItalianResultStatus.Expired; + else if (!rules.IsEMA(vaccination)) + { + result.StatusMessage = $"Vaccination with {vaccination.MedicinalProduct} from country {vaccination.Country} are not considered valid by EMA"; + result.ItalianStatus = DgcItalianResultStatus.NotValid; + } + else + { + result.ItalianStatus = DgcItalianResultStatus.Valid; + } + + return result; + } + + /// + /// Porting of vaccineStrengthenedStrategy + /// + /// + /// + /// + /// + private ItalianRulesValidationResult ValidateFor2G( + ValidationCertificateModel certificateModel, + IEnumerable rules, + ValidationMode validationMode) + { + var result = InitializeResult(certificateModel, validationMode); + + var vaccination = certificateModel.Dgc.GetCertificateEntry(DiseaseAgents.Covid19); + if (vaccination == null) + return result; + + if (vaccination.IsFromItaly()) + return ValidateFor3G(certificateModel, rules, validationMode); + + var validationDate = certificateModel.ValidationInstant.Date; + var vaccinationDate = vaccination.Date.Date; + + DateTime? extendedDate = null; + if (!vaccination.IsComplete()) + { + result.ValidFrom = vaccinationDate.AddDays(rules.GetVaccineStartDayNotComplete(vaccination.MedicinalProduct)); + result.ValidUntil = vaccinationDate.AddDays(rules.GetVaccineEndDayNotComplete(vaccination.MedicinalProduct)); + + if (!rules.IsEMA(vaccination)) + { + result.ItalianStatus = DgcItalianResultStatus.NotValid; + result.StatusMessage = $"Vaccination with {vaccination.MedicinalProduct} from country {vaccination.Country} are not considered valid by EMA"; + return result; + } + } + else + { + // Complete + var startDaysToAdd = vaccination.IsBooster() ? + rules.GetVaccineStartDayBoosterUnified(CountryCodes.Italy) : + rules.GetVaccineStartDayCompleteUnified(CountryCodes.Italy, vaccination.MedicinalProduct); + + var endDaysToAdd = vaccination.IsBooster() ? + rules.GetVaccineEndDayBoosterUnified(CountryCodes.Italy) : + rules.GetVaccineEndDayCompleteUnified(CountryCodes.Italy); + + var extendedDaysToAdd = rules.GetVaccineEndDayCompleteExtendedEMA(); + + result.ValidFrom = vaccinationDate.AddDays(startDaysToAdd); + result.ValidUntil = vaccinationDate.AddDays(endDaysToAdd); + extendedDate = vaccinationDate.AddDays(extendedDaysToAdd); + } + + if (!vaccination.IsComplete()) + { + if (!rules.IsEMA(vaccination)) + { + result.ItalianStatus = DgcItalianResultStatus.NotValid; + result.StatusMessage = $"Vaccination with {vaccination.MedicinalProduct} from country {vaccination.Country} are not considered valid by EMA"; + } + else if (validationDate < result.ValidFrom) + result.ItalianStatus = DgcItalianResultStatus.NotValidYet; + else if (validationDate > result.ValidUntil) + result.ItalianStatus = DgcItalianResultStatus.Expired; + else + result.ItalianStatus = DgcItalianResultStatus.Valid; + } + else if (vaccination.IsBooster()) + { + if (validationDate < result.ValidFrom) + result.ItalianStatus = DgcItalianResultStatus.NotValidYet; + else if (validationDate > result.ValidUntil) + result.ItalianStatus = DgcItalianResultStatus.Expired; + else if (rules.IsEMA(vaccination)) + result.ItalianStatus = DgcItalianResultStatus.Valid; + else + { + result.ItalianStatus = DgcItalianResultStatus.TestNeeded; + result.StatusMessage = $"Test is needed for vaccines not certified by EMA"; + } + } + else + { + if (rules.IsEMA(vaccination)) + { + if (validationDate < result.ValidFrom) + result.ItalianStatus = DgcItalianResultStatus.NotValidYet; + else if (validationDate <= result.ValidUntil) + result.ItalianStatus = DgcItalianResultStatus.Valid; + else if (validationDate <= extendedDate && extendedDate != null) + { + result.ItalianStatus = DgcItalianResultStatus.TestNeeded; + result.StatusMessage = $"Test is needed for expired vaccination within the extended date ({extendedDate:d})"; + } + else + result.ItalianStatus = DgcItalianResultStatus.Expired; + } + else + { + if (validationDate < result.ValidFrom) + result.ItalianStatus = DgcItalianResultStatus.NotValidYet; + else if (validationDate <= extendedDate && extendedDate != null) + { + result.ItalianStatus = DgcItalianResultStatus.TestNeeded; + result.StatusMessage = $"Test is needed for vaccination not certifid by EMA, that are still in the extended date period ({extendedDate:d})"; + } + else + result.ItalianStatus = DgcItalianResultStatus.Expired; + } + } + + + return result; + } + + + /// + /// Porting of vaccineBoosterStrategy + /// + /// + /// + /// + /// + private ItalianRulesValidationResult ValidateForBooster( + ValidationCertificateModel certificateModel, + IEnumerable rules, + ValidationMode validationMode) + { + var result = InitializeResult(certificateModel, validationMode); + + var vaccination = certificateModel.Dgc.GetCertificateEntry(DiseaseAgents.Covid19); + if (vaccination == null) + return result; + + var validationDate = certificateModel.ValidationInstant.Date; + var vaccinationDate = vaccination.Date.Date; + + var startDaysToAdd = + vaccination.IsBooster() ? rules.GetVaccineStartDayBoosterUnified(CountryCodes.Italy) : + !vaccination.IsComplete() ? rules.GetVaccineStartDayNotComplete(vaccination.MedicinalProduct) : + rules.GetVaccineStartDayCompleteUnified(CountryCodes.Italy, vaccination.MedicinalProduct); + + var endDaysToAdd = + vaccination.IsBooster() ? rules.GetVaccineEndDayBoosterUnified(CountryCodes.Italy) : + !vaccination.IsComplete() ? rules.GetVaccineEndDayNotComplete(vaccination.MedicinalProduct) : + rules.GetVaccineEndDayCompleteUnified(CountryCodes.Italy); + + result.ValidFrom = vaccinationDate.AddDays(startDaysToAdd); + result.ValidUntil = vaccinationDate.AddDays(endDaysToAdd); + + if (validationDate < result.ValidFrom) + result.ItalianStatus = DgcItalianResultStatus.NotValidYet; + else if (validationDate > result.ValidUntil) + result.ItalianStatus = DgcItalianResultStatus.Expired; + else if (vaccination.IsComplete()) + { + if (vaccination.IsBooster() && rules.IsEMA(vaccination)) + result.ItalianStatus = DgcItalianResultStatus.Valid; + else + { + result.StatusMessage = $"Test is needed for non-booster vaccination and vaccines not certifid by EMA"; + result.ItalianStatus = DgcItalianResultStatus.TestNeeded; + } + } + else + result.ItalianStatus = DgcItalianResultStatus.NotValid; + + return result; + } + + + /// + /// Porting of vaccineSchoolStrategy + /// + /// + /// + /// + /// + private ItalianRulesValidationResult ValidateForSchool( + ValidationCertificateModel certificateModel, + IEnumerable rules, + ValidationMode validationMode) + { + var result = InitializeResult(certificateModel, validationMode); + + var vaccination = certificateModel.Dgc.GetCertificateEntry(DiseaseAgents.Covid19); + if (vaccination == null) + return result; + + var validationDate = certificateModel.ValidationInstant.Date; + var vaccinationDate = vaccination.Date.Date; + + var startDaysToAdd = + vaccination.IsBooster() ? rules.GetVaccineStartDayBoosterUnified(CountryCodes.Italy) : + !vaccination.IsComplete() ? rules.GetVaccineStartDayNotComplete(vaccination.MedicinalProduct) : + rules.GetVaccineStartDayCompleteUnified(CountryCodes.Italy, vaccination.MedicinalProduct); + + var endDaysToAdd = + vaccination.IsBooster() ? rules.GetVaccineEndDayBoosterUnified(CountryCodes.Italy) : + !vaccination.IsComplete() ? rules.GetVaccineEndDayNotComplete(vaccination.MedicinalProduct) : + rules.GetVaccineEndDaySchool(); + + result.ValidFrom = vaccinationDate.AddDays(startDaysToAdd); + result.ValidUntil = vaccinationDate.AddDays(endDaysToAdd); + + if (validationDate < result.ValidFrom) + result.ItalianStatus = DgcItalianResultStatus.NotValidYet; + else if (validationDate > result.ValidUntil) + result.ItalianStatus = DgcItalianResultStatus.Expired; + else if (!rules.IsEMA(vaccination)) + { + result.ItalianStatus = DgcItalianResultStatus.NotValid; + result.StatusMessage = $"Vaccination with {vaccination.MedicinalProduct} from country {vaccination.Country} are not considered valid by EMA"; + } + else if (!vaccination.IsComplete()) + { + result.ItalianStatus = DgcItalianResultStatus.NotValid; + result.StatusMessage = $"Vaccination is not complete"; + } + + else + result.ItalianStatus = DgcItalianResultStatus.Valid; + + return result; + } + + /// + /// Porting of vaccineWorkStrategy + /// + /// + /// + /// + /// + private ItalianRulesValidationResult ValidateForWork( + ValidationCertificateModel certificateModel, + IEnumerable rules, + ValidationMode validationMode) + { + var result = InitializeResult(certificateModel, validationMode); + + var vaccination = certificateModel.Dgc.GetCertificateEntry(DiseaseAgents.Covid19); + if (vaccination == null) + return result; + + var validationDate = certificateModel.ValidationInstant.Date; + + var birthDate = certificateModel.Dgc.GetBirthDate(); + var age = birthDate.GetAge(validationDate); + + if (age >= SdkConstants.VaccineMandatoryAge) + { + Logger?.LogDebug($"Age {age} is above {SdkConstants.ValidationRulesUrl}, using strict mode"); + return ValidateFor2G(certificateModel, rules, validationMode); + } + Logger?.LogDebug($"Age {age} is below {SdkConstants.ValidationRulesUrl}, using standard mode"); + return ValidateFor3G(certificateModel, rules, validationMode); + } + + /// + /// Porting of vaccineEntryItalyStrategy + /// + /// + /// + /// + /// + private ItalianRulesValidationResult ValidateForEntryItaly( + ValidationCertificateModel certificateModel, + IEnumerable rules, + ValidationMode validationMode) + { + var result = InitializeResult(certificateModel, validationMode); + + var vaccination = certificateModel.Dgc.GetCertificateEntry(DiseaseAgents.Covid19); + if (vaccination == null) + return result; + + var validationDate = certificateModel.ValidationInstant.Date; + var vaccinationDate = vaccination.Date.Date; + + var startDaysToAdd = + vaccination.IsBooster() ? rules.GetVaccineStartDayBoosterUnified(CountryCodes.NotItaly) : + !vaccination.IsComplete() ? rules.GetVaccineStartDayNotComplete(vaccination.MedicinalProduct) : + rules.GetVaccineStartDayCompleteUnified(CountryCodes.NotItaly, vaccination.MedicinalProduct); + + var endDaysToAdd = + vaccination.IsBooster() ? rules.GetVaccineEndDayBoosterUnified(CountryCodes.NotItaly) : + !vaccination.IsComplete() ? rules.GetVaccineEndDayNotComplete(vaccination.MedicinalProduct) : + rules.GetVaccineEndDayCompleteUnified(CountryCodes.NotItaly); + + result.ValidFrom = vaccinationDate.AddDays(startDaysToAdd); + result.ValidUntil = vaccinationDate.AddDays(endDaysToAdd); + + if (validationDate < result.ValidFrom) + result.ItalianStatus = DgcItalianResultStatus.NotValidYet; + else if (validationDate > result.ValidUntil) + result.ItalianStatus = DgcItalianResultStatus.Expired; + else if (!rules.IsEMA(vaccination)) + { + result.ItalianStatus = DgcItalianResultStatus.NotValid; + result.StatusMessage = $"Vaccination with {vaccination.MedicinalProduct} from country {vaccination.Country} are not considered valid by EMA"; + } + else if (!vaccination.IsComplete()) + { + result.ItalianStatus = DgcItalianResultStatus.NotValid; + result.StatusMessage = $"Vaccination is not complete"; + } + else + result.ItalianStatus = DgcItalianResultStatus.Valid; + + return result; + } + } +} diff --git a/RuleValidators/DgcReader.RuleValidators.Italy/Validation/ValidationCertificateModel.cs b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/ValidationCertificateModel.cs new file mode 100644 index 0000000..9310d76 --- /dev/null +++ b/RuleValidators/DgcReader.RuleValidators.Italy/Validation/ValidationCertificateModel.cs @@ -0,0 +1,32 @@ +using DgcReader.Models; +using DgcReader.RuleValidators.Italy.Models; +using GreenpassReader.Models; +using System; +using System.Collections.Generic; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + +namespace DgcReader.RuleValidators.Italy.Validation +{ + /// + /// Model for + /// + public class ValidationCertificateModel + { + /// + /// The DGC to be vsalidated. + /// Note that this could be the decoded by , or if issued by Italy + /// + public EuDGC Dgc { get; set; } + + /// + /// The date/time of validation + /// + public DateTimeOffset ValidationInstant { get; set; } + + /// + /// Signature validation data + /// + public SignatureValidationResult SignatureData { get; set; } + } +}