Implement test and recovery certificates

This commit is contained in:
Marvin Sextro 2021-07-09 03:05:57 +02:00
parent 728211a338
commit c632d2c2e8
11 changed files with 307 additions and 198 deletions

View File

@ -1,62 +0,0 @@
version: "3.3"
services:
traefik:
image: "traefik:v2.4"
command:
#- "--log.level=DEBUG"
- "--api.insecure=false"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"
#- "--certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
- "--certificatesresolvers.myresolver.acme.email=marvin.sextro@gmail.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
- "--entrypoints.web.http.redirections.entrypoint.permanent=true"
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- "./letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
covidpass-api:
image: "marvinsxtr/covidpass-api:latest"
restart: "unless-stopped"
environment:
- NODE_ENV=production
ports:
- "8000:8000"
labels:
- "traefik.enable=true"
- "traefik.http.routers.covidpass-api.rule=Host(`api.covidpass.marvinsextro.de`)"
- "traefik.http.routers.covidpass-api.entrypoints=websecure"
- "traefik.http.routers.covidpass-api.tls.certresolver=myresolver"
secrets:
- env
covidpass:
image: "marvinsxtr/covidpass:latest"
restart: "unless-stopped"
environment:
- NODE_ENV=production
ports:
- "3000:3000"
labels:
- "traefik.enable=true"
- "traefik.http.routers.covidpass.rule=Host(`covidpass.marvinsextro.de`)"
- "traefik.http.routers.covidpass.entrypoints=websecure"
- "traefik.http.routers.covidpass.tls.certresolver=myresolver"
depends_on:
- covidpass-api
secrets:
env:
file: ./.env

View File

@ -1,7 +1,7 @@
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'de', 'de-DE'],
locales: ['en', 'de', 'de-DE', 'de-AT', 'de-LI', 'de-LU', 'de-CH'],
localeExtension: 'yml',
},
};

1
public/locales/de-AT Symbolic link
View File

@ -0,0 +1 @@
de

1
public/locales/de-CH Symbolic link
View File

@ -0,0 +1 @@
de

1
public/locales/de-LI Symbolic link
View File

@ -0,0 +1 @@
de

1
public/locales/de-LU Symbolic link
View File

@ -0,0 +1 @@
de

View File

@ -2,7 +2,7 @@ noFileOrQrCode: Bitte scanne einen QR-Code oder wähle eine Datei aus
signatureFailed: Fehler beim Signieren der Karte auf dem Server
decodingFailed: Dekodierung der QR-Code-Daten fehlgeschlagen
invalidColor: Ungültige Farbe
vaccinationInfo: Impfinformationen konnten nicht gelesen werden
certificateData: Zertifikats-Daten konnten nicht gelesen werden
nameMissing: Name konnte nicht gelesen werden
dobMissing: Geburtsdatum konnte nicht gelesen werden
invalidMedicalProduct: Ungültiges Medizinprodukt
@ -12,3 +12,6 @@ invalidFileType: Ungültiger Dateityp
couldNotDecode: Dekodierung aus QR-Code fehlgeschlagen
couldNotFindQrCode: QR-Code konnte in der ausgewählten Datei nicht gefunden werden
invalidQrCode: Ungültiger QR-Code
certificateType: Kein gültiger Zertifikatstyp gefunden
invalidTestResult: Ungültiges Testergebnis
invalidTestType: Ungültiger Testtyp

View File

@ -2,7 +2,7 @@ noFileOrQrCode: Please scan a QR Code, or select a file
signatureFailed: Error while signing pass on server
decodingFailed: Failed to decode QR code payload
invalidColor: Invalid color
vaccinationInfo: Failed to read vaccination information
certificateData: Failed to read certificate data
nameMissing: Failed to read name
dobMissing: Failed to read date of birth
invalidMedicalProduct: Invalid medical product
@ -12,3 +12,6 @@ invalidFileType: Invalid file type
couldNotDecode: Could not decode QR code from file
couldNotFindQrCode: Could not find QR Code in provided file
invalidQrCode: Invalid QR code
certificateType: No valid certificate type found
invalidTestResult: Invalid test result
invalidTestType: Invalid test type

View File

@ -2,7 +2,7 @@ import {toBuffer as createZip} from 'do-not-zip';
import {v4 as uuid4} from 'uuid';
import {Constants} from "./constants";
import {Payload, PayloadBody} from "./payload";
import {Payload, PayloadBody, PassDictionary} from "./payload";
import {ValueSets} from "./value_sets";
const crypto = require('crypto')
@ -21,21 +21,6 @@ interface QrCode {
messageEncoding: Encoding;
}
interface Field {
key: string;
label: string;
value: string;
textAlignment?: string;
}
interface PassStructureDictionary {
headerFields: Array<Field>;
primaryFields: Array<Field>;
secondaryFields: Array<Field>;
auxiliaryFields: Array<Field>;
backFields: Array<Field>;
}
interface SignData {
PassJsonHash: string;
useBlackVersion: boolean;
@ -56,7 +41,7 @@ export class PassData {
serialNumber: string;
barcodes: Array<QrCode>;
barcode: QrCode;
generic: PassStructureDictionary;
generic: PassDictionary;
// Generates a sha1 hash from a given buffer
private static getBufferHash(buffer: Buffer | string): string {
@ -157,74 +142,6 @@ export class PassData {
this.serialNumber = uuid4(); // Generate random UUID v4
this.barcodes = [qrCode];
this.barcode = qrCode;
this.generic = {
headerFields: [
{
key: "type",
label: "Certificate Type",
value: payload.certificateType
}
],
primaryFields: [
{
key: "name",
label: "Name",
value: payload.name
}
],
secondaryFields: [
{
key: "dose",
label: "Dose",
value: payload.dose
},
{
key: "dov",
label: "Date of Vaccination",
value: payload.dateOfVaccination,
textAlignment: "PKTextAlignmentRight"
}
],
auxiliaryFields: [
{
key: "vaccine",
label: "Vaccine",
value: payload.vaccineName
},
{
key: "dob",
label: "Date of Birth",
value: payload.dateOfBirth,
textAlignment: "PKTextAlignmentRight"
}
],
backFields: [
{
key: "uvci",
label: "Unique Certificate Identifier (UVCI)",
value: payload.uvci
},
{
key: "issuer",
label: "Certificate Issuer",
value: payload.certificateIssuer
},
{
key: "country",
label: "Country of Vaccination",
value: payload.countryOfVaccination
},
{
key: "manufacturer",
label: "Manufacturer",
value: payload.manufacturer
},
{
key: "disclaimer",
label: "Disclaimer",
value: "This certificate is only valid in combination with the ID card of the certificate holder and expires one year + 14 days after the last dose. The validity of this certificate was not checked by CovidPass."
}
]
};
this.generic = payload.generic;
}
}

View File

@ -1,6 +1,31 @@
import {ValueSets} from "./value_sets";
import {Constants} from "./constants";
enum CertificateType {
Vaccination = 'Vaccination',
Test = 'Test',
Recovery = 'Recovery',
}
enum TextAlignment {
right = 'PKTextAlignmentRight',
}
interface Field {
key: string;
label: string;
value: string;
textAlignment?: string;
}
export interface PassDictionary {
headerFields: Array<Field>;
primaryFields: Array<Field>;
secondaryFields: Array<Field>;
auxiliaryFields: Array<Field>;
backFields: Array<Field>;
}
export interface PayloadBody {
color: string;
rawData: string;
@ -8,7 +33,7 @@ export interface PayloadBody {
}
export class Payload {
certificateType: string = 'Vaccination';
certificateType: CertificateType;
rawData: string;
@ -19,17 +44,7 @@ export class Payload {
img2x: Buffer;
dark: boolean;
name: string;
dose: string;
dateOfVaccination: string;
dateOfBirth: string;
uvci: string;
certificateIssuer: string;
medicalProductKey: string;
countryOfVaccination: string;
vaccineName: string;
manufacturer: string;
generic: PassDictionary;
constructor(body: PayloadBody, valueSets: ValueSets) {
@ -41,39 +56,99 @@ export class Payload {
const dark = body.color != 'white'
const healthCertificate = body.decodedData['-260'];
const covidCertificate = healthCertificate['1']; // Version number subject to change
// Get Vaccine, Name and Date of Birth information
const vaccinationInformation = body.decodedData['-260']['1']['v'][0];
const nameInformation = body.decodedData['-260']['1']['nam'];
const dateOfBirthInformation = body.decodedData['-260']['1']['dob'];
if (vaccinationInformation == undefined) {
throw new Error('vaccinationInfo');
if (covidCertificate == undefined) {
throw new Error('certificateData');
}
// Get name and date of birth information
const nameInformation = covidCertificate['nam'];
const dateOfBirthInformation = covidCertificate['dob'];
if (nameInformation == undefined) {
throw new Error('nameMissing');
}
if (dateOfBirthInformation == undefined) {
throw new Error('dobMissing');
}
// Get Medical, country and manufacturer information
const medialProductKey = vaccinationInformation['mp'];
const countryCode = vaccinationInformation['co'];
const manufacturerKey = vaccinationInformation['ma'];
const name = `${nameInformation['fn']}, ${nameInformation['gn']}`;
const dateOfBirth = dateOfBirthInformation;
if (!(medialProductKey in valueSets.medicalProducts)) {
throw new Error('invalidMedicalProduct');
let properties: object;
// Set certificate type and properties
if (covidCertificate['v'] !== undefined) {
this.certificateType = CertificateType.Vaccination;
properties = covidCertificate['v'][0];
}
if (covidCertificate['t'] !== undefined) {
this.certificateType = CertificateType.Test;
properties = covidCertificate['t'][0];
}
if (covidCertificate['r'] !== undefined) {
this.certificateType = CertificateType.Recovery;
properties = covidCertificate['r'][0];
}
if (this.certificateType == undefined) {
throw new Error('certificateType')
}
// Get country code, identifier and issuer
const countryCode = properties['co'];
const uvci = properties['ci'];
const certificateIssuer = properties['is'];
if (!(countryCode in valueSets.countryCodes)) {
throw new Error('invalidCountryCode')
}
if (!(manufacturerKey in valueSets.manufacturers)) {
throw new Error('invalidManufacturer')
throw new Error('invalidCountryCode');
}
const country = valueSets.countryCodes[countryCode].display;
const generic: PassDictionary = {
headerFields: [
{
key: "type",
label: "Certificate Type",
value: this.certificateType
}
],
primaryFields: [
{
key: "name",
label: "Name",
value: name
}
],
secondaryFields: [],
auxiliaryFields: [
{
key: "dob",
label: "Date of Birth",
value: dateOfBirth,
textAlignment: TextAlignment.right
}
],
backFields: [
{
key: "uvci",
label: "Unique Certificate Identifier (UVCI)",
value: uvci
},
{
key: "issuer",
label: "Certificate Issuer",
value: certificateIssuer
},
{
key: "country",
label: "Country",
value: country
}
]
}
// Set Values
this.rawData = body.rawData;
@ -85,16 +160,158 @@ export class Payload {
this.img2x = dark ? Constants.img2xWhite : Constants.img2xBlack
this.dark = dark;
this.name = `${nameInformation['fn']}, ${nameInformation['gn']}`;
this.dose = `${vaccinationInformation['dn']}/${vaccinationInformation['sd']}`;
this.dateOfVaccination = vaccinationInformation['dt'];
this.dateOfBirth = dateOfBirthInformation;
this.uvci = vaccinationInformation['ci'];
this.certificateIssuer = vaccinationInformation['is'];
this.countryOfVaccination = valueSets.countryCodes[countryCode].display;
this.vaccineName = valueSets.medicalProducts[medialProductKey].display;
this.manufacturer = valueSets.manufacturers[manufacturerKey].display;
this.generic = Payload.fillPassData(this.certificateType, generic, properties, valueSets);
}
static fillPassData(type: CertificateType, data: PassDictionary, properties: Object, valueSets: ValueSets): PassDictionary {
switch (type) {
case CertificateType.Vaccination:
const dose = `${properties['dn']}/${properties['sd']}`;
const dateOfVaccination = properties['dt'];
const medialProductKey = properties['mp'];
const manufacturerKey = properties['ma'];
if (!(medialProductKey in valueSets.medicalProducts)) {
throw new Error('invalidMedicalProduct');
}
if (!(manufacturerKey in valueSets.manufacturers)) {
throw new Error('invalidManufacturer')
}
const vaccineName = valueSets.medicalProducts[medialProductKey].display;
const manufacturer = valueSets.manufacturers[manufacturerKey].display;
data.secondaryFields.push(...[
{
key: "dose",
label: "Dose",
value: dose
},
{
key: "dov",
label: "Date of Vaccination",
value: dateOfVaccination,
textAlignment: TextAlignment.right
}
]);
data.auxiliaryFields.splice(0, 0, {
key: "vaccine",
label: "Vaccine",
value: vaccineName
});
data.backFields.push(...[
{
key: "manufacturer",
label: "Manufacturer",
value: manufacturer
},
{
key: "disclaimer",
label: "Disclaimer",
value: "This certificate is only valid in combination with the ID card of the certificate holder and expires one year + 14 days after the last dose. The validity of this certificate was not checked by CovidPass."
}
]);
break;
case CertificateType.Test:
const testTypeKey = properties['tt'];
const testDateTimeString = properties['sc'];
const testResultKey = properties['tr'];
const testingCentre = properties['tc'];
if (!(testResultKey in valueSets.testResults)) {
throw new Error('invalidTestResult');
}
if (!(testTypeKey in valueSets.testTypes)) {
throw new Error('invalidTestType')
}
const testResult = valueSets.testResults[testResultKey].display;
const testType = valueSets.testTypes[testTypeKey].display;
const testTime = testDateTimeString.replace(/.*T/, '').replace('Z', ' ') + 'UTC';
const testDate = testDateTimeString.replace(/T.*/,'');
data.secondaryFields.push(...[
{
key: "result",
label: "Result",
value: testResult
},
{
key: "dot",
label: "Date of Test",
value: testDate,
textAlignment: TextAlignment.right
}
]);
data.auxiliaryFields.pop();
data.auxiliaryFields.push(...[
{
key: "test",
label: "Test Type",
value: testType
},
{
key: "time",
label: "Time of Test",
value: testTime,
textAlignment: TextAlignment.right
},
]);
if (testingCentre !== undefined)
data.backFields.push({
key: "centre",
label: "Testing Centre",
value: testingCentre
});
data.backFields.push({
key: "disclaimer",
label: "Disclaimer",
value: "This certificate is only valid in combination with the ID card of the certificate holder and may expire 24h after the test. The validity of this certificate was not checked by CovidPass."
});
break;
case CertificateType.Recovery:
const firstPositiveTestDate = properties['fr'];
const validFrom = properties['df'];
const validUntil = properties['du'];
data.secondaryFields.push(...[
{
key: "result",
label: "Test Result",
value: "Detected"
},
{
key: "from",
label: "Valid From",
value: validFrom,
textAlignment: TextAlignment.right
}
]);
data.auxiliaryFields.pop();
data.auxiliaryFields.push(...[
{
key: "testdate",
label: "Test Date",
value: firstPositiveTestDate
},
{
key: "until",
label: "Valid Until",
value: validUntil,
textAlignment: TextAlignment.right
},
]);
data.backFields.push({
key: "disclaimer",
label: "Disclaimer",
value: "This certificate is only valid in combination with the ID card of the certificate holder. The validity of this certificate was not checked by CovidPass."
});
break;
default:
throw new Error('certificateType');
}
return data;
}
}

View File

@ -2,6 +2,8 @@ interface ValueTypes {
medicalProducts: string;
countryCodes: string;
manufacturers: string;
testResults: string;
testTypes: string;
}
export class ValueSets {
@ -10,25 +12,50 @@ export class ValueSets {
medicalProducts: 'vaccine-medicinal-product.json',
countryCodes: 'country-2-codes.json',
manufacturers: 'vaccine-mah-manf.json',
testResults: 'test-result.json',
testTypes: 'test-type.json',
}
medicalProducts: object;
countryCodes: object;
manufacturers: object;
testResults: object;
testTypes: object;
private constructor(medicalProducts: object, countryCodes: object, manufacturers: object) {
private constructor(
medicalProducts: object,
countryCodes: object,
manufacturers: object,
testResults: object,
testTypes: object
) {
this.medicalProducts = medicalProducts;
this.countryCodes = countryCodes;
this.manufacturers = manufacturers;
this.testResults = testResults;
this.testTypes = testTypes;
}
private static async fetchValueSet(file: string): Promise<object> {
return await (await fetch(ValueSets.VALUE_SET_BASE_URL + file)).json();
}
public static async loadValueSets(): Promise<ValueSets> {
// Load all Value Sets from GitHub
let medicalProducts = await (await fetch(ValueSets.VALUE_SET_BASE_URL + ValueSets.VALUE_TYPES.medicalProducts)).json();
let countryCodes = await (await fetch(ValueSets.VALUE_SET_BASE_URL + ValueSets.VALUE_TYPES.countryCodes)).json();
let manufacturers = await (await fetch(ValueSets.VALUE_SET_BASE_URL + ValueSets.VALUE_TYPES.manufacturers)).json();
let [medicalProducts, countryCodes, manufacturers, testResults, testTypes] = await Promise.all([
ValueSets.fetchValueSet(ValueSets.VALUE_TYPES.medicalProducts),
ValueSets.fetchValueSet(ValueSets.VALUE_TYPES.countryCodes),
ValueSets.fetchValueSet(ValueSets.VALUE_TYPES.manufacturers),
ValueSets.fetchValueSet(ValueSets.VALUE_TYPES.testResults),
ValueSets.fetchValueSet(ValueSets.VALUE_TYPES.testTypes)
]);
return new ValueSets(medicalProducts['valueSetValues'], countryCodes['valueSetValues'], manufacturers['valueSetValues']);
return new ValueSets(
medicalProducts['valueSetValues'],
countryCodes['valueSetValues'],
manufacturers['valueSetValues'],
testResults['valueSetValues'],
testTypes['valueSetValues']
);
}
}
}