这个解决方案对我有用。
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using System;
using System.IO;
using Com.AugustCellars.COSE;
using PeterO.Cbor;
using Newtonsoft.Json;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Collections.Generic;
namespace DGCVerification.Services {
public class DGCVerificationService: IDGCVerificationService {
private readonly string _certificatePrefix = "HC1:";
private readonly ILogger < DGCVerificationService > _logger;
public DGCVerificationService(ILogger < DGCVerificationService > logger) {
_logger = logger;
}
private bool TryTrimPrefix(string certificateString, out string result) {
if (!certificateString.StartsWith(_certificatePrefix)) {
result = null;
return false;
}
result = certificateString.Substring(_certificatePrefix.Length);
return true;
}
private byte[] DecompressFromZlib(byte[] input) {
using(var memoryStream = new MemoryStream(input)) {
using(var decompressionStream = new InflaterInputStream(memoryStream)) {
using(var resultStream = new MemoryStream()) {
decompressionStream.CopyTo(resultStream);
var result = resultStream.ToArray();
return result;
}
}
}
}
private bool VerifyCoseSignature(Sign1Message coseMessage, string signature) {
var signatureAsBytes = Convert.FromBase64String(signature);
var publicKey = OneKey.FromX509(signatureAsBytes);
var result = coseMessage.Validate(publicKey);
_logger.LogInformation($"cose message signature {signature} verified as {result}");
return result;
}
private DGCPayload DecodeAndDeserialize(string certificateString, out Sign1Message coseMessage) {
coseMessage = null;
if (!TryTrimPrefix(certificateString, out
var trimmedPrefixString)) {
_logger.LogInformation($"certificate {certificateString} didn't have proper prefix {_certificatePrefix}");
return null;
}
var bytesBase256 = Base45Encoding.Decode(trimmedPrefixString);
_logger.LogInformation($"certificate {certificateString} base45 decoded to {Convert.ToBase64String(bytesBase256)}");
var decompressedFromZlib = DecompressFromZlib(bytesBase256);
_logger.LogDebug($"certificate {certificateString} zlib decompressed to {Convert.ToBase64String(decompressedFromZlib)}");
coseMessage = Message.DecodeFromBytes(decompressedFromZlib) as Sign1Message;
var coseMessagePayload = coseMessage.GetContent();
var cborResult = CBORObject.DecodeFromBytes(coseMessagePayload);
var jsonResult = cborResult.ToJSONString();
var result = JsonConvert.DeserializeObject < DGCPayloadCzechVersionRoot > (jsonResult);
return result.DGCPayloadWrap.DGCPayload;
}
private bool IsNotExpiredDGC(DGCPayload dGCPayload, UzisData uzisData) {
var vaccine = dGCPayload.Vaccination?.FirstOrDefault();
if (vaccine != null) {
var vaccinationValidityRules = uzisData.DGCValidityCzechRules.Rules
.FirstOrDefault()
.PlatnostiVakcinace;
var rule = vaccinationValidityRules.FirstOrDefault(x => x.VaccineMedicinalProduct == vaccine.Mp) ??
vaccinationValidityRules.FirstOrDefault(x => x.VaccineMedicinalProduct == null);
if (!DateTime.TryParse(vaccine.Dt, out
var vaccinatedOnDate)) {
_logger.LogError($"couldn't parse date of vaccination for DGC : {JsonConvert.SerializeObject(dGCPayload)}");
return false;
}
var result = DateTime.Now.Date <= vaccinatedOnDate.AddMonths(rule.OdolnostMesicDo);
return result;
}
var test = dGCPayload.Test?.FirstOrDefault();
if (test != null) {
var testValidityRule = uzisData.DGCValidityCzechRules.Rules
.FirstOrDefault()
.PlatnostiTestu
.FirstOrDefault(x => x.TypeOfTest == test.Tt);
if (!DateTime.TryParse(test.Tr, out
var testedOnDate)) {
_logger.LogError($"couldn't parse date of test for DGC : {JsonConvert.SerializeObject(dGCPayload)}");
return false;
}
var result = DateTime.Now.Date <= testedOnDate.AddHours(testValidityRule.PlatnostHod);
return result;
}
var recovery = dGCPayload.Recovery?.FirstOrDefault();
if (recovery != null) {
if (!DateTime.TryParse(recovery.Du, out
var recoveryValidUntil)) {
_logger.LogError($"couldn't parse recovert valid until for DGC : {JsonConvert.SerializeObject(dGCPayload)}");
return false;
}
var result = DateTime.Now.Date <= recoveryValidUntil;
return result;
}
return false;
}
private string GetCountryFromDGC(DGCPayload dGCPayload) {
var result = dGCPayload.Vaccination?.FirstOrDefault()?.Co ??
dGCPayload.Test?.FirstOrDefault()?.Co ??
dGCPayload.Recovery?.FirstOrDefault()?.Co;
if (result == null) {
throw new ArgumentException($"couldn't retrieve country from DGC. dgc : {JsonConvert.SerializeObject(dGCPayload)}");
}
return result;
}
private List < SignatureCertificate > GetFittingSignatures(DGCPayload dGCPayload, UzisData uzisData) {
try {
var country = GetCountryFromDGC(dGCPayload);
var result = uzisData.NationalCertificateSignatures.SignatureCertificate
.Where(x => x.Active)
.Where(x => x.Country == country)
.Where(x => x.CertificateType == "DSC")
.ToList();
return result;
} catch (Exception e) {
_logger.LogError(e, $"Filtering signatures from UZIS failed with exception");
return null;
}
}
private bool IsProperlySignedDGC(string certificateString, DGCPayload dGCPayload, Sign1Message coseMessage, UzisData uzisData) {
var fittingSignatures = GetFittingSignatures(dGCPayload, uzisData);
var result = false;
foreach(var signature in fittingSignatures) {
try {
var signatureVerificationResult = VerifyCoseSignature(coseMessage, signature.RawData);
_logger.LogInformation($"certificate {certificateString} signature validation against signature {signature} resulted in {signatureVerificationResult}");
result |= signatureVerificationResult;
} catch (Exception e) {
_logger.LogError(e, $"certificate {certificateString} signature validation against signature {signature} failed with exception");
}
}
return result;
}
public bool IsValidDgc(string certificateString, UzisData uzisData, out DGCPayload decodedDGCPayload, out VerificationResult verificationResult) {
decodedDGCPayload = null;
Sign1Message coseMessage = null;
try {
decodedDGCPayload = DecodeAndDeserialize(certificateString, out coseMessage);
if (coseMessage == null) {
_logger.LogInformation($"certificate {certificateString} decoded to null COSE");
verificationResult = VerificationResult.UnableToDecode;
return false;
}
} catch (Exception e) {
_logger.LogError(e, $"certificate {certificateString} decoding failed with exception");
verificationResult = VerificationResult.UnableToDecode;
return false;
}
var isProperlySigned = IsProperlySignedDGC(certificateString, decodedDGCPayload, coseMessage, uzisData);
if (!isProperlySigned) {
verificationResult = VerificationResult.InvalidSignature;
return false;
}
var isNotExpired = IsNotExpiredDGC(decodedDGCPayload, uzisData);
if (!isNotExpired) {
verificationResult = VerificationResult.Expired;
return false;
}
verificationResult = VerificationResult.Valid;
return true;
}
}
}
还有棘手的base45
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace DGCVerification
{
public class Base45Encoding
{
private static readonly Dictionary<byte, char> _encodingTable = new Dictionary<byte, char>
{
{0x0,'0'},
{0x1,'1'},
{0x2,'2'},
{0x3,'3'},
{0x4,'4'},
{0x5,'5'},
{0x6,'6'},
{0x7,'7'},
{0x8,'8'},
{0x9,'9'},
{0xA,'A'},
{0xB,'B'},
{0xC,'C'},
{0xD,'D'},
{0xE,'E'},
{0xF,'F'},
{0x10,'G'},
{0x11,'H'},
{0x12,'I'},
{0x13,'J'},
{0x14,'K'},
{0x15,'L'},
{0x16,'M'},
{0x17,'N'},
{0x18,'O'},
{0x19,'P'},
{0x1A,'Q'},
{0x1B,'R'},
{0x1C,'S'},
{0x1D,'T'},
{0x1E,'U'},
{0x1F,'V'},
{0x20,'W'},
{0x21,'X'},
{0x22,'Y'},
{0x23,'Z'},
{0x24,' '},
{0x25,'$'},
{0x26,'%'},
{0x27,'*'},
{0x28,'+'},
{0x29,'-'},
{0x2A,'.'},
{0x2B,'/'},
{0x2C,':'},
};
private static readonly Dictionary<char, byte> _decodingTable;
static Base45Encoding()
{
_decodingTable = _encodingTable.ToDictionary(x => x.Value, x => x.Key);
}
private static List<byte> ToBytesBase45(string charsBase45)
{
var result = new List<byte>(charsBase45.Length);
foreach (var character in charsBase45)
{
if (!_decodingTable.TryGetValue(character, out byte asByte))
{
throw new FormatException($"input string contains {character} with numeric value {char.GetNumericValue(character)} on index {charsBase45.IndexOf(character)} which is not valid base45 character");
}
result.Add(asByte);
}
return result;
}
private static List<ushort> ToShortsBase10(List<byte> bytesBase45)
{
var result = new List<ushort>(bytesBase45.Count);
ushort num = 0;
double pow = 0;
for (int i = 0; i != bytesBase45.Count; ++i, ++pow)
{
num += (ushort)(bytesBase45[i] * Math.Pow(45, pow));
if (pow == 2 || i == (bytesBase45.Count -1))
{
result.Add(num);
num = 0;
pow = -1;
}
}
return result;
}
private static List<byte> ToBytesBase256(List<ushort> shortsBase10, int charBase45Length)
{
var result = new List<byte>(shortsBase10.Count);
for (int i = 0; i != shortsBase10.Count; ++i)
{
var num = (byte)(shortsBase10[i] / 256);
if(!(i == (shortsBase10.Count - 1)
&& charBase45Length % 3 != 0
&& num == 0))
{
result.Add(num);
}
result.Add((byte)(shortsBase10[i] % 256));
}
return result;
}
public static byte[] Decode(string charsBase45)
{
if (charsBase45.Length % 3 == 1)
{
throw new FormatException("input string does not have correct length. mod 3 == 1. it isnt base45");
}
var bytesBase45 = ToBytesBase45(charsBase45);
var shortsBase10 = ToShortsBase10(bytesBase45);
var bytesBase256 = ToBytesBase256(shortsBase10, charsBase45.Length);
return bytesBase256.ToArray();
}
}
}