├── .github └── workflows │ └── simple.yml ├── .gitignore ├── .gitmodules ├── LICENSE.txt ├── README.md ├── allowed-licenses.json ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── Versions.kt ├── demo.html ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── karma.config.d └── patch.js ├── node-demo ├── index.js └── package.json ├── settings.gradle.kts ├── src ├── commonMain │ ├── kotlin │ │ └── ehn │ │ │ └── techiop │ │ │ └── hcert │ │ │ └── kotlin │ │ │ ├── chain │ │ │ ├── Base45Service.kt │ │ │ ├── CborService.kt │ │ │ ├── CertificateRepository.kt │ │ │ ├── Chain.kt │ │ │ ├── ChainDecodeResult.kt │ │ │ ├── ChainResult.kt │ │ │ ├── CompressorService.kt │ │ │ ├── ContextIdentifierService.kt │ │ │ ├── CoseService.kt │ │ │ ├── CryptoService.kt │ │ │ ├── CwtService.kt │ │ │ ├── DecodeResult.kt │ │ │ ├── DefaultChain.kt │ │ │ ├── DelegatingChain.kt │ │ │ ├── Error.kt │ │ │ ├── Extensions.kt │ │ │ ├── FixedClock.kt │ │ │ ├── HigherOrderValidationService.kt │ │ │ ├── IChain.kt │ │ │ ├── SchemaValidationService.kt │ │ │ ├── TwoDimCodeService.kt │ │ │ ├── VerificationException.kt │ │ │ ├── VerificationResult.kt │ │ │ ├── debug │ │ │ │ ├── DebugChain.kt │ │ │ │ ├── DebugContextIdentifierService.kt │ │ │ │ ├── DebugCoseService.kt │ │ │ │ ├── DebugCwtService.kt │ │ │ │ ├── DebugHigherOrderValidationService.kt │ │ │ │ └── DebugSchemaValidationService.kt │ │ │ └── impl │ │ │ │ ├── Base45Encoder.kt │ │ │ │ ├── CompressorAdapter.kt │ │ │ │ ├── DefaultBase45Service.kt │ │ │ │ ├── DefaultCborService.kt │ │ │ │ ├── DefaultCompressorService.kt │ │ │ │ ├── DefaultContextIdentifierService.kt │ │ │ │ ├── DefaultCoseService.kt │ │ │ │ ├── DefaultCryptoService.kt │ │ │ │ ├── DefaultCwtService.kt │ │ │ │ ├── DefaultHigherOrderValidationService.kt │ │ │ │ ├── DefaultSchemaValidationService.kt │ │ │ │ ├── FileBasedCryptoService.kt │ │ │ │ ├── PrefilledCertificateRepository.kt │ │ │ │ ├── RandomEcKeyCryptoService.kt │ │ │ │ ├── RandomRsaKeyCryptoService.kt │ │ │ │ ├── SchemaValidationAdapter.kt │ │ │ │ └── TrustListCertificateRepository.kt │ │ │ ├── crypto │ │ │ ├── CertificateAdapter.kt │ │ │ ├── CryptoAdapter.kt │ │ │ ├── Datatypes.kt │ │ │ ├── KeyType.kt │ │ │ └── PkiUtils.kt │ │ │ ├── data │ │ │ ├── CborObject.kt │ │ │ ├── GreenCertificate.kt │ │ │ ├── LenientInstantParser.kt │ │ │ ├── LenientLocalDateParser.kt │ │ │ ├── Person.kt │ │ │ ├── RecoveryStatement.kt │ │ │ ├── Serializers.kt │ │ │ ├── Test.kt │ │ │ ├── Vaccination.kt │ │ │ ├── VaccinationExemption.kt │ │ │ └── ValueSets.kt │ │ │ ├── log │ │ │ └── LoggingUtils.kt │ │ │ ├── rules │ │ │ ├── BusinessRule.kt │ │ │ ├── BusinessRulesContainer.kt │ │ │ ├── BusinessRulesDecodeService.kt │ │ │ └── BusinessRulesV1EncodeService.kt │ │ │ ├── trust │ │ │ ├── ContentType.kt │ │ │ ├── CoseAdapter.kt │ │ │ ├── CoseCreationAdapter.kt │ │ │ ├── CwtAdapter.kt │ │ │ ├── CwtCreationAdapter.kt │ │ │ ├── Hash.kt │ │ │ ├── SignedData.kt │ │ │ ├── SignedDataDecodeService.kt │ │ │ ├── SignedDataEncodeService.kt │ │ │ ├── SignedDataParsed.kt │ │ │ ├── TrustListDecodeService.kt │ │ │ ├── TrustListV2.kt │ │ │ ├── TrustListV2EncodeService.kt │ │ │ ├── TrustedCertificate.kt │ │ │ └── TrustedCertificateV2.kt │ │ │ └── valueset │ │ │ ├── ValueSet.kt │ │ │ ├── ValueSetContainer.kt │ │ │ ├── ValueSetDecodeService.kt │ │ │ └── ValueSetV1EncodeService.kt │ └── resources │ │ ├── json │ │ └── schema │ │ │ ├── 1.0.0 │ │ │ └── DCC.combined-schema.json │ │ │ ├── 1.0.1 │ │ │ └── DCC.combined-schema.json │ │ │ ├── 1.1.0 │ │ │ └── DCC.combined-schema.json │ │ │ ├── 1.2.0 │ │ │ └── DCC.combined-schema.json │ │ │ ├── 1.2.1 │ │ │ └── DCC.combined-schema.json │ │ │ ├── 1.3.0 │ │ │ └── DCC.combined-schema.json │ │ │ ├── 1.3.1 │ │ │ └── DCC.combined-schema.json │ │ │ ├── 1.3.2 │ │ │ └── DCC.combined-schema.json │ │ │ ├── AT-1.0.0 │ │ │ └── DCC.combined-schema.json │ │ │ └── fallback │ │ │ └── DCC.combined-schema.json │ │ └── value-sets │ │ ├── country-2-codes.json │ │ ├── disease-agent-targeted.json │ │ ├── test-manf.json │ │ ├── test-result.json │ │ ├── test-type.json │ │ ├── vaccine-mah-manf.json │ │ ├── vaccine-medicinal-product.json │ │ └── vaccine-prophylaxis.json ├── commonShared │ └── kotlin │ │ └── SampleData.kt ├── commonTest │ ├── kotlin │ │ └── ehn │ │ │ └── techiop │ │ │ └── hcert │ │ │ └── kotlin │ │ │ ├── 000InitTestContext.kt │ │ │ ├── chain │ │ │ ├── ContentTypeChainTest.kt │ │ │ ├── RsaCryptoServiceHolder.kt │ │ │ ├── SimpleChainTest.kt │ │ │ ├── ext │ │ │ │ ├── ExtendedTestRunner.kt │ │ │ │ ├── TestCase.kt │ │ │ │ ├── TestContext.kt │ │ │ │ └── TestExpectedResults.kt │ │ │ └── impl │ │ │ │ ├── Base45EncoderTest.kt │ │ │ │ ├── CompressorServiceTest.kt │ │ │ │ ├── ContentTypeTest.kt │ │ │ │ ├── DefaultCwtServiceTest.kt │ │ │ │ ├── FileBasedCryptoServiceTest.kt │ │ │ │ └── RandomCryptoServiceTest.kt │ │ │ ├── data │ │ │ └── ValueSetHolderTest.kt │ │ │ ├── rules │ │ │ └── BusinessRulesTest.kt │ │ │ ├── trust │ │ │ └── TrustListTest.kt │ │ │ └── valueset │ │ │ └── VauleSetTest.kt │ └── resources │ │ ├── ve_bad_hc1.json │ │ ├── ve_bad_multi_ve.json │ │ ├── ve_bad_oid_in_cert.json │ │ ├── ve_bad_vac_in_at1.json │ │ ├── ve_bad_ve_in_hc1.json │ │ ├── ve_bad_wrongcert.json │ │ └── ve_good.json ├── jsMain │ └── kotlin │ │ ├── JsInterface.kt │ │ ├── ehn │ │ └── techiop │ │ │ └── hcert │ │ │ └── kotlin │ │ │ ├── chain │ │ │ ├── DecodeResultJs.kt │ │ │ ├── Extensions.kt │ │ │ ├── VerificationResultJs.kt │ │ │ └── impl │ │ │ │ ├── CompressorAdapter.kt │ │ │ │ ├── DefaultTwoDimCodeService.kt │ │ │ │ └── SchemaValidationAdapter.kt │ │ │ ├── crypto │ │ │ ├── CertificateAdapter.kt │ │ │ ├── Cose.kt │ │ │ ├── CryptoAdapter.kt │ │ │ ├── JsEcPrivKey.kt │ │ │ ├── JsEcPubKey.kt │ │ │ ├── JsRsaPrivKey.kt │ │ │ ├── JsRsaPubKey.kt │ │ │ └── PkiUtils.kt │ │ │ ├── data │ │ │ ├── JsDateSerializer.kt │ │ │ └── ValueSetsInstanceHolder.kt │ │ │ ├── log │ │ │ └── BasicLogger.kt │ │ │ └── trust │ │ │ ├── CoseAdapter.kt │ │ │ ├── CoseCreationAdapter.kt │ │ │ ├── CwtAdapter.kt │ │ │ ├── CwtCreationAdapter.kt │ │ │ └── Hash.kt │ │ └── external │ │ ├── Buffer.kt │ │ ├── Wrappers.kt │ │ ├── ajv.kt │ │ ├── cbor.kt │ │ ├── cose-js.kt │ │ ├── crypto │ │ ├── elliptic.kt │ │ ├── index.curve.module_elliptic.kt │ │ ├── index.curves.module_elliptic.kt │ │ ├── index.module_bn.js.kt │ │ └── index.module_node-rsa.kt │ │ ├── index.Pako.module_pako.kt │ │ ├── pkijs │ │ ├── index.Asn1js.module_asn1js.kt │ │ ├── index.PvUtils.module_pvutils.kt │ │ ├── index.module_pkijs.kt │ │ ├── index.pkijs.src.AlgorithmIdentifier.module_pkijs.kt │ │ ├── index.pkijs.src.AltName.module_pkijs.kt │ │ ├── index.pkijs.src.Attribute.module_pkijs.kt │ │ ├── index.pkijs.src.AttributeTypeAndValue.module_pkijs.kt │ │ ├── index.pkijs.src.AuthorityKeyIdentifier.module_pkijs.kt │ │ ├── index.pkijs.src.BasicConstraints.module_pkijs.kt │ │ ├── index.pkijs.src.CRLDistributionPoints.module_pkijs.kt │ │ ├── index.pkijs.src.Certificate.module_pkijs.kt │ │ ├── index.pkijs.src.CertificateRevocationList.module_pkijs.kt │ │ ├── index.pkijs.src.DistributionPoint.module_pkijs.kt │ │ ├── index.pkijs.src.ECPrivateKey.module_pkijs.kt │ │ ├── index.pkijs.src.ECPublicKey.module_pkijs.kt │ │ ├── index.pkijs.src.ExtKeyUsage.module_pkijs.kt │ │ ├── index.pkijs.src.Extension.module_pkijs.kt │ │ ├── index.pkijs.src.Extensions.module_pkijs.kt │ │ ├── index.pkijs.src.GeneralName.module_pkijs.kt │ │ ├── index.pkijs.src.GeneralNames.module_pkijs.kt │ │ ├── index.pkijs.src.IssuerAndSerialNumber.module_pkijs.kt │ │ ├── index.pkijs.src.IssuingDistributionPoint.module_pkijs.kt │ │ ├── index.pkijs.src.OtherPrimeInfo.module_pkijs.kt │ │ ├── index.pkijs.src.PrivateKeyInfo.module_pkijs.kt │ │ ├── index.pkijs.src.PrivateKeyUsagePeriod.module_pkijs.kt │ │ ├── index.pkijs.src.PublicKeyInfo.module_pkijs.kt │ │ ├── index.pkijs.src.RSAPrivateKey.module_pkijs.kt │ │ ├── index.pkijs.src.RSAPublicKey.module_pkijs.kt │ │ ├── index.pkijs.src.RSASSAPSSParams.module_pkijs.kt │ │ ├── index.pkijs.src.RelativeDistinguishedNames.module_pkijs.kt │ │ ├── index.pkijs.src.RevokedCertificate.module_pkijs.kt │ │ ├── index.pkijs.src.Signature.module_pkijs.kt │ │ ├── index.pkijs.src.Time.module_pkijs.kt │ │ └── lib.dom.kt │ │ └── qrcode.kt ├── jsTest │ └── kotlin │ │ └── ehn │ │ └── techiop │ │ └── hcert │ │ └── kotlin │ │ └── chain │ │ ├── ext │ │ └── loadResource.kt │ │ └── impl │ │ └── DefaultTwoDimCodeServiceJsTest.kt ├── jvmMain │ ├── datagen │ │ └── ehn │ │ │ └── techiop │ │ │ └── hcert │ │ │ └── kotlin │ │ │ └── chain │ │ │ └── faults │ │ │ ├── BothProtectedWrongCoseService.kt │ │ │ ├── BothUnprotectedWrongCoseService.kt │ │ │ ├── BrokenCoseService.kt │ │ │ ├── DuplicateHeaderCoseService.kt │ │ │ ├── FaultyBase45Service.kt │ │ │ ├── FaultyCborService.kt │ │ │ ├── FaultyCompressorService.kt │ │ │ ├── FaultyCoseService.kt │ │ │ ├── FaultyCwtService.kt │ │ │ ├── NonVerifiableCoseService.kt │ │ │ ├── NoopCompressorService.kt │ │ │ ├── NoopContextIdentifierService.kt │ │ │ ├── UnprotectedCoseService.kt │ │ │ └── WrongUnprotectedCoseService.kt │ └── kotlin │ │ └── ehn │ │ └── techiop │ │ └── hcert │ │ └── kotlin │ │ ├── chain │ │ ├── Extensions.kt │ │ └── impl │ │ │ ├── CompressorAdapter.kt │ │ │ ├── DefaultTwoDimCodeService.kt │ │ │ └── SchemaValidationAdapter.kt │ │ ├── crypto │ │ ├── CertificateAdapter.kt │ │ ├── CryptoAdapter.kt │ │ ├── JvmPrivKey.kt │ │ ├── JvmPubKey.kt │ │ └── PkiUtils.kt │ │ ├── data │ │ └── ValueSetsInstanceHolder.kt │ │ ├── log │ │ └── BasicLogger.kt │ │ └── trust │ │ ├── CoseAdapter.kt │ │ ├── CoseCreationAdapter.kt │ │ ├── CoseWorkaround.kt │ │ ├── CwtAdapter.kt │ │ ├── CwtCreationAdapter.kt │ │ └── Hash.kt └── jvmTest │ └── kotlin │ └── ehn │ └── techiop │ └── hcert │ └── kotlin │ └── chain │ ├── FaultyImplementationsTest.kt │ ├── ext │ ├── ExtendedTestGenerator.kt │ └── loadResource.kt │ └── impl │ ├── DefaultTwoDimCodeServiceJvmTest.kt │ └── FileBasedCryptoServiceJvmTest.kt ├── webpack-templates ├── patch-browser.js └── patch-node.js └── yarn.lock.bak /.github/workflows/simple.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | submodules: true 17 | - name: Set up JDK 1.8 18 | uses: actions/setup-java@v1 19 | with: 20 | java-version: 1.8 21 | - name: Cache Gradle packages 22 | uses: actions/cache@v1 23 | with: 24 | path: ~/.gradle/caches 25 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 26 | restore-keys: ${{ runner.os }}-gradle 27 | - name: Build jsBrowserDevelopmentWebpack 28 | run: ./gradlew clean jvmTest jsBrowserTest jsBrowserDevelopmentWebpack 29 | - name: Upload hcert-kotlin.js 30 | uses: actions/upload-artifact@v2.2.3 31 | with: 32 | name: hcert-kotlin.js 33 | path: build/distributions/hcert-kotlin.js* 34 | - name: Build jsBrowserProductionWebpack 35 | run: ./gradlew clean jvmTest jsBrowserTest jsBrowserProductionWebpack 36 | - name: Upload hcert-kotlin-prod.js 37 | uses: actions/upload-artifact@v2.2.3 38 | with: 39 | name: hcert-kotlin-prod.js 40 | path: build/distributions/hcert-kotlin.js* 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### IntelliJ IDEA ### 2 | .idea 3 | *.iws 4 | *.iml 5 | *.ipr 6 | .DS_Store 7 | 8 | .gradle 9 | **/build/ 10 | !src/**/build/ 11 | src/jsTest/generated 12 | src/jsMain/generated 13 | webpack.config.d/ 14 | local.properties 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/commonTest/resources/dgc-testdata"] 2 | path = src/commonTest/resources/dgc-testdata 3 | url = https://github.com/eu-digital-green-certificates/dgc-testdata.git 4 | -------------------------------------------------------------------------------- /allowed-licenses.json: -------------------------------------------------------------------------------- 1 | { 2 | "allowedLicenses": [ 3 | { 4 | "moduleLicense": "Apache License, Version 2.0" 5 | }, 6 | { 7 | "moduleLicense": "The Apache License, Version 2.0" 8 | }, 9 | { 10 | "moduleLicense": "The Apache Software License, Version 2.0" 11 | }, 12 | { 13 | "moduleLicense": "Bouncy Castle Licence" 14 | }, 15 | { 16 | "moduleLicense": "BSD3" 17 | }, 18 | { 19 | "moduleLicense": "BSD 3-clause License w/nuclear disclaimer" 20 | }, 21 | { 22 | "moduleLicense": "CC0 Universal" 23 | }, 24 | { 25 | "moduleLicense": "CC0 1.0 Universal" 26 | }, 27 | { 28 | "moduleLicense": "MIT License" 29 | }, 30 | { 31 | "moduleLicense": "The MIT License (MIT)" 32 | }, 33 | { 34 | // is Apache 2 35 | "moduleName": "org.jetbrains.kotlinx:kotlinx-serialization-core" 36 | }, 37 | { 38 | // is Apache 2 39 | "moduleName": "org.jetbrains.kotlinx:kotlinx-serialization-cbor" 40 | }, 41 | { 42 | // is Apache 2 43 | "moduleName": "org.jetbrains.kotlinx:kotlinx-serialization-json" 44 | }, 45 | { 46 | // is Apache 2 47 | "moduleName": "org.jetbrains.kotlinx:kotlinx-datetime" 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | repositories { 5 | mavenCentral() 6 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/Versions.kt: -------------------------------------------------------------------------------- 1 | object Versions { 2 | const val kotlin = "1.7.10" 3 | const val serialization = "1.3.0" 4 | const val datetime = "0.4.0" 5 | const val kotest = "4.6.3" 6 | const val licenseReport = "1.16" 7 | const val napier = "2.6.1" 8 | 9 | val jvm = Jvm 10 | val js = Js 11 | 12 | object Jvm { 13 | const val cose = "1.1.0" 14 | const val zxing = "3.5.0" 15 | const val bcpkix = "1.70" 16 | const val jsonSchema = "0.36" 17 | } 18 | 19 | object Js { 20 | const val cose = "0.7.0" 21 | const val pako = "2.0.4" 22 | const val pkijs = "2.4.0" 23 | const val `crypto-browserify` = "3.12.0" 24 | const val `stream-browserify` = "3.0.0" 25 | const val `constants-browserify` = "1.0.0" 26 | const val util = "0.12.4" 27 | const val buffer = "6.0.3" 28 | const val process = "0.11.10" 29 | const val cbor = "8.1.0" 30 | const val `node-inspect-extracted` = "1.1.0" 31 | const val sha256 = "1.3.0" 32 | const val url = "0.11.0" 33 | const val elliptic = "6.5.4" 34 | const val rsa = "1.1.1" 35 | const val assert = "2.0.0" 36 | const val ajv = "8.11.0" 37 | const val `ajv-formats` = "2.1.1" 38 | const val qrcode = "3.3.0" 39 | const val bignumber = "9.1.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.mpp.stability.nowarn=true 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ehn-dcc-development/hcert-kotlin/ad74d757970595145afe7aaa827f02ab424cafbc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | install: 2 | - ./gradlew clean publishToMavenLocal 3 | -------------------------------------------------------------------------------- /karma.config.d/patch.js: -------------------------------------------------------------------------------- 1 | //Increase timeouts, since RSA key generation can take ages in JS 2 | //See https://discuss.kotlinlang.org/t/configuring-timeouts-when-running-mocha-based-tests-in-mpp-or-js-projects/16567/6 3 | config.set({ 4 | client: { 5 | mocha: { 6 | timeout: 281000 7 | } 8 | }, 9 | browserNoActivityTimeout: 191337, 10 | browserDisconnectTimeout: 192668, 11 | processKillTimeout: 193995 12 | }); 13 | -------------------------------------------------------------------------------- /node-demo/index.js: -------------------------------------------------------------------------------- 1 | const hcert = require("../build/distributions/hcert-node") 2 | 3 | const qr = "HC1:NCFTW2H:7*I06R3W/J:O6:P4QB3+7RKFVJWV66UBCE//UXDT:*ML-4D.NBXR+SRHMNIY6EB8I595+6UY9-+0DPIO6C5%0SBHN-OWKCJ6BLC2M.M/NPKZ4F3WNHEIE6IO26LB8:F4:JVUGVY8*EKCLQ..QCSTS+F$:0PON:.MND4Z0I9:GU.LBJQ7/2IJPR:PAJFO80NN0TRO1IB:44:N2336-:KC6M*2N*41C42CA5KCD555O/A46F6ST1JJ9D0:.MMLH2/G9A7ZX4DCL*010LGDFI$MUD82QXSVH6R.CLIL:T4Q3129HXB8WZI8RASDE1LL9:9NQDC/O3X3G+A:2U5VP:IE+EMG40R53CG9J3JE1KB KJA5*$4GW54%LJBIWKE*HBX+4MNEIAD$3NR E228Z9SS4E R3HUMH3J%-B6DRO3T7GJBU6O URY858P0TR8MDJ$6VL8+7B5$G CIKIPS2CPVDK%K6+N0GUG+TG+RB5JGOU55HXDR.TL-N75Y0NHQTZ3XNQMTF/ZHYBQ$8IR9MIQHOSV%9K5-7%ZQ/.15I0*-J8AVD0N0/0USH.3" 4 | 5 | const pemCert = 6 | "-----BEGIN CERTIFICATE-----\n" + 7 | "MIIBvTCCAWOgAwIBAgIKAXk8i88OleLsuTAKBggqhkjOPQQDAjA2MRYwFAYDVQQD\n" + 8 | "DA1BVCBER0MgQ1NDQSAxMQswCQYDVQQGEwJBVDEPMA0GA1UECgwGQk1TR1BLMB4X\n" + 9 | "DTIxMDUwNTEyNDEwNloXDTIzMDUwNTEyNDEwNlowPTERMA8GA1UEAwwIQVQgRFND\n" + 10 | "IDExCzAJBgNVBAYTAkFUMQ8wDQYDVQQKDAZCTVNHUEsxCjAIBgNVBAUTATEwWTAT\n" + 11 | "BgcqhkjOPQIBBggqhkjOPQMBBwNCAASt1Vz1rRuW1HqObUE9MDe7RzIk1gq4XW5G\n" + 12 | "TyHuHTj5cFEn2Rge37+hINfCZZcozpwQKdyaporPUP1TE7UWl0F3o1IwUDAOBgNV\n" + 13 | "HQ8BAf8EBAMCB4AwHQYDVR0OBBYEFO49y1ISb6cvXshLcp8UUp9VoGLQMB8GA1Ud\n" + 14 | "IwQYMBaAFP7JKEOflGEvef2iMdtopsetwGGeMAoGCCqGSM49BAMCA0gAMEUCIQDG\n" + 15 | "2opotWG8tJXN84ZZqT6wUBz9KF8D+z9NukYvnUEQ3QIgdBLFSTSiDt0UJaDF6St2\n" + 16 | "bkUQuVHW6fQbONd731/M4nc=\n" + 17 | "-----END CERTIFICATE-----" 18 | 19 | const verifier = new hcert.VerifierDirect([pemCert]); 20 | 21 | 22 | console.debug(verifier.verify(qr)) -------------------------------------------------------------------------------- /node-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodetest", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "hcert-kotlin" 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenCentral() 6 | gradlePluginPortal() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/Base45Service.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | /** 4 | * Encodes/decodes input in/from Base45 5 | */ 6 | interface Base45Service { 7 | 8 | fun encode(input: ByteArray): String 9 | 10 | fun decode(input: String, verificationResult: VerificationResult): ByteArray 11 | 12 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/CborService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import ehn.techiop.hcert.kotlin.data.GreenCertificate 4 | 5 | /** 6 | * Encodesinput as a CBOR structure 7 | */ 8 | interface CborService { 9 | 10 | fun encode(input: GreenCertificate): ByteArray 11 | } 12 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/CertificateRepository.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import ehn.techiop.hcert.kotlin.crypto.CertificateAdapter 4 | 5 | interface CertificateRepository { 6 | 7 | fun loadTrustedCertificates(kid: ByteArray, verificationResult: VerificationResult): List 8 | 9 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/ChainResult.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ChainResult( 7 | val step0Cbor: ByteArray, 8 | val step1Cwt: ByteArray, 9 | val step2Cose: ByteArray, 10 | val step3Compressed: ByteArray, 11 | val step4Encoded: String, 12 | val step5Prefixed: String, 13 | ) { 14 | override fun equals(other: Any?): Boolean { 15 | if (this === other) return true 16 | other?.let { if (this::class != other::class) return false } 17 | 18 | other as ChainResult 19 | 20 | if (!step0Cbor.contentEquals(other.step0Cbor)) return false 21 | if (!step1Cwt.contentEquals(other.step1Cwt)) return false 22 | if (!step2Cose.contentEquals(other.step2Cose)) return false 23 | if (!step3Compressed.contentEquals(other.step3Compressed)) return false 24 | if (step4Encoded != other.step4Encoded) return false 25 | if (step5Prefixed != other.step5Prefixed) return false 26 | 27 | return true 28 | } 29 | 30 | override fun hashCode(): Int { 31 | var result = step0Cbor.contentHashCode() 32 | result = 31 * result + step1Cwt.contentHashCode() 33 | result = 31 * result + step2Cose.contentHashCode() 34 | result = 31 * result + step3Compressed.contentHashCode() 35 | result = 31 * result + step4Encoded.hashCode() 36 | result = 31 * result + step5Prefixed.hashCode() 37 | return result 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/CompressorService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | /** 4 | * Compresses/decompresses input 5 | */ 6 | interface CompressorService { 7 | 8 | fun encode(input: ByteArray): ByteArray 9 | 10 | fun decode(input: ByteArray, verificationResult: VerificationResult): ByteArray 11 | 12 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/ContextIdentifierService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | /** 4 | * Appends/drops an Context identifier prefix from input 5 | */ 6 | interface ContextIdentifierService { 7 | 8 | fun encode(input: String): String 9 | 10 | fun decode(input: String, verificationResult: VerificationResult): String 11 | 12 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/CoseService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | /** 4 | * Encodes/decodes input as a Sign1Message according to COSE specification (RFC8152) 5 | */ 6 | interface CoseService { 7 | 8 | fun encode(input: ByteArray): ByteArray 9 | 10 | fun decode(input: ByteArray, verificationResult: VerificationResult): ByteArray 11 | 12 | fun getVerificationRepo(): CertificateRepository? 13 | 14 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/CryptoService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import ehn.techiop.hcert.kotlin.crypto.CertificateAdapter 4 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 5 | import ehn.techiop.hcert.kotlin.crypto.PrivKey 6 | import ehn.techiop.hcert.kotlin.crypto.PubKey 7 | 8 | 9 | /** 10 | * Uses a cryptographic key-pair to sign and verify COSE structures 11 | */ 12 | interface CryptoService { 13 | 14 | fun getCborHeaders(): List> 15 | 16 | fun getCborSigningKey(): PrivKey 17 | 18 | fun getCborVerificationKey(kid: ByteArray, verificationResult: VerificationResult = VerificationResult()): PubKey 19 | 20 | fun getCertificate(): CertificateAdapter 21 | 22 | fun exportPrivateKeyAsPem(): String 23 | 24 | fun exportCertificateAsPem(): String 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/CwtService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import ehn.techiop.hcert.kotlin.data.CborObject 4 | 5 | /** 6 | * Encodes/decodes input as a CWT structure, ready to sign with COSE 7 | */ 8 | interface CwtService { 9 | 10 | fun encode(input: ByteArray): ByteArray 11 | 12 | /** 13 | * Throws a Throwable if schema validation fails 14 | */ 15 | fun decode(input: ByteArray, verificationResult: VerificationResult): CborObject 16 | 17 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/DecodeResult.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.encodeToString 5 | import kotlinx.serialization.json.Json 6 | import kotlinx.serialization.json.encodeToJsonElement 7 | 8 | 9 | private val json = Json { prettyPrint = true } 10 | 11 | @Serializable 12 | data class DecodeResult( 13 | val verificationResult: VerificationResult, 14 | val chainDecodeResult: ChainDecodeResult 15 | ) { 16 | 17 | fun toJson(anonymized: Boolean = false) = json.encodeToJsonElement( 18 | if (anonymized) (DecodeResult( 19 | verificationResult, 20 | chainDecodeResult.anonymizedCopy 21 | )) else this 22 | ) 23 | 24 | fun toJsonString(anonymized: Boolean = false) = json.encodeToString(toJson(anonymized)) 25 | override fun toString() = toJsonString() 26 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/DefaultChain.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultBase45Service 4 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCborService 5 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCompressorService 6 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultContextIdentifierService 7 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCoseService 8 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCwtService 9 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultHigherOrderValidationService 10 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultSchemaValidationService 11 | import kotlinx.datetime.Clock 12 | import kotlin.js.JsName 13 | import kotlin.jvm.JvmOverloads 14 | import kotlin.jvm.JvmStatic 15 | 16 | 17 | object DefaultChain { 18 | @JvmStatic 19 | @JsName("buildCreationChain") 20 | fun buildCreationChain(cryptoService: CryptoService, context: String = "HC1:") = Chain( 21 | DefaultHigherOrderValidationService(), 22 | DefaultSchemaValidationService(), 23 | DefaultCborService(), 24 | DefaultCwtService(), 25 | DefaultCoseService(cryptoService), 26 | DefaultCompressorService(), 27 | DefaultBase45Service(), 28 | DefaultContextIdentifierService(context) 29 | ) 30 | 31 | /** 32 | * Builds a "default" chain for verifying, i.e. one with the implementation according to spec. 33 | */ 34 | @JvmStatic 35 | @JvmOverloads 36 | @JsName("buildVerificationChain") 37 | fun buildVerificationChain( 38 | repository: CertificateRepository, 39 | atRepository: CertificateRepository? = null, 40 | clock: Clock = Clock.System 41 | ): IChain { 42 | val euContextService = DefaultContextIdentifierService("HC1:") 43 | val euChain = Chain( 44 | DefaultHigherOrderValidationService(), 45 | DefaultSchemaValidationService(), 46 | DefaultCborService(), 47 | DefaultCwtService(clock = clock), 48 | DefaultCoseService(repository), 49 | DefaultCompressorService(), 50 | DefaultBase45Service(), 51 | euContextService 52 | ) 53 | if (atRepository == null) 54 | return euChain 55 | 56 | val atContextService = DefaultContextIdentifierService("AT1:") 57 | val atChain = Chain( 58 | DefaultHigherOrderValidationService(), 59 | DefaultSchemaValidationService(false, arrayOf("AT-1.0.0")), 60 | DefaultCborService(), 61 | DefaultCwtService(clock = clock), 62 | DefaultCoseService(atRepository), 63 | DefaultCompressorService(), 64 | DefaultBase45Service(), 65 | atContextService 66 | ) 67 | 68 | return DelegatingChain(euChain, euContextService, atChain, atContextService) 69 | } 70 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/DelegatingChain.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import ehn.techiop.hcert.kotlin.data.GreenCertificate 4 | 5 | /** 6 | * Can decode incoming HCERT data depending on Context, e.g. either "HC1:" or "AT1:" 7 | */ 8 | class DelegatingChain( 9 | private val euChain: Chain, 10 | private val euContextService: ContextIdentifierService, 11 | private val atChain: Chain, 12 | private val atContextService: ContextIdentifierService 13 | ) : IChain { 14 | 15 | override fun encode(input: GreenCertificate): ChainResult { 16 | return euChain.encode(input) 17 | } 18 | 19 | override fun decode(input: String): DecodeResult { 20 | val check = VerificationResult() 21 | return try { 22 | euContextService.decode(input, check) 23 | euChain.decode(input) 24 | } catch (_: VerificationException) { 25 | try { 26 | atContextService.decode(input, check) 27 | atChain.decode(input) 28 | } catch (e: VerificationException) { 29 | DecodeResult( 30 | VerificationResult().apply { error = e.error;e.details?.let { errorDetails.putAll(it) } }, 31 | ChainDecodeResult(listOf(e.error), null, null, null, null, null, null) 32 | ) 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/Error.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | /** 4 | * From Swift ValidationCore 5 | */ 6 | enum class Error { 7 | GENERAL_ERROR, 8 | INVALID_SCHEME_PREFIX, 9 | DECOMPRESSION_FAILED, 10 | BASE_45_DECODING_FAILED, 11 | COSE_DESERIALIZATION_FAILED, 12 | CBOR_DESERIALIZATION_FAILED, 13 | SCHEMA_VALIDATION_FAILED, // not on iOS 14 | CWT_EXPIRED, 15 | CWT_NOT_YET_VALID, 16 | QR_CODE_ERROR, 17 | CERTIFICATE_QUERY_FAILED, 18 | USER_CANCELLED, 19 | TRUST_SERVICE_ERROR, 20 | TRUST_LIST_EXPIRED, 21 | TRUST_LIST_NOT_YET_VALID, 22 | TRUST_LIST_SIGNATURE_INVALID, 23 | KEY_NOT_IN_TRUST_LIST, 24 | PUBLIC_KEY_EXPIRED, 25 | PUBLIC_KEY_NOT_YET_VALID, 26 | UNSUITABLE_PUBLIC_KEY_TYPE, 27 | KEY_CREATION_ERROR, 28 | KEYSTORE_ERROR, 29 | SIGNATURE_INVALID, 30 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/Extensions.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | 4 | expect fun ByteArray.asBase64(): String 5 | 6 | expect fun ByteArray.toHexString(): String 7 | 8 | expect fun String.fromBase64(): ByteArray 9 | 10 | expect fun String.fromHexString(): ByteArray 11 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/FixedClock.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import kotlinx.datetime.Clock 4 | import kotlinx.datetime.Instant 5 | 6 | class FixedClock(private val now: Instant) : Clock { 7 | override fun now() = now 8 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/HigherOrderValidationService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import ehn.techiop.hcert.kotlin.data.GreenCertificate 4 | 5 | /** 6 | * Performs higher-level checks on deserialised GreenCertificate 7 | */ 8 | interface HigherOrderValidationService { 9 | 10 | fun validate(input: GreenCertificate, verificationResult: VerificationResult): GreenCertificate 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/IChain.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import ehn.techiop.hcert.kotlin.data.GreenCertificate 4 | import kotlin.js.JsName 5 | 6 | /** 7 | * Interface for the encoding / decoding chain 8 | */ 9 | interface IChain { 10 | /** 11 | * Process the [input], apply encoding in this order: 12 | * - [CborService] 13 | * - [CwtService] 14 | * - [CoseService] 15 | * - [CompressorService] 16 | * - [Base45Service] 17 | * - [ContextIdentifierService] 18 | * 19 | * The result ([ChainResult]) will contain all intermediate steps, as well as the final result in [ChainResult.step5Prefixed]. 20 | */ 21 | @JsName("encode") 22 | fun encode(input: GreenCertificate): ChainResult 23 | 24 | /** 25 | * Process the [input], apply decoding in this order: 26 | * - [ContextIdentifierService] 27 | * - [Base45Service] 28 | * - [CompressorService] 29 | * - [CoseService] 30 | * - [CwtService] 31 | * - [CborService] 32 | * - [SchemaValidationService] 33 | * The result ([ChainDecodeResult]) will contain the parsed data, as well as intermediate results. 34 | */ 35 | @JsName("decode") 36 | fun decode(input: String): DecodeResult 37 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/SchemaValidationService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import ehn.techiop.hcert.kotlin.data.CborObject 4 | import ehn.techiop.hcert.kotlin.data.GreenCertificate 5 | 6 | interface SchemaValidationService { 7 | /** 8 | * Throws a Throwable if schema validation fails 9 | */ 10 | fun validate(cbor: CborObject, verificationResult: VerificationResult): GreenCertificate 11 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/TwoDimCodeService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | interface TwoDimCodeService { 4 | 5 | /** 6 | * Generates a 2D code, returns the image itself as an encoded png 7 | */ 8 | fun encode(data: String): ByteArray 9 | 10 | /** 11 | * Decodes the content of a png encoded image of a 2D code 12 | */ 13 | fun decode(input: ByteArray): String 14 | 15 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/VerificationException.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | open class VerificationException( 4 | val error: Error, 5 | message: String? = null, 6 | cause: Throwable? = null, 7 | val details: ErrorDetails? = null 8 | ) : Throwable(message, cause) 9 | 10 | class NonFatalVerificationException( 11 | val result: Any, 12 | error: Error, 13 | message: String? = null, 14 | cause: Throwable? = null, 15 | details: ErrorDetails? = null 16 | ) : VerificationException(error, message, cause, details) 17 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/debug/DebugChain.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.debug 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CertificateRepository 4 | import ehn.techiop.hcert.kotlin.chain.Chain 5 | import ehn.techiop.hcert.kotlin.chain.DelegatingChain 6 | import ehn.techiop.hcert.kotlin.chain.IChain 7 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultBase45Service 8 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCborService 9 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCompressorService 10 | import kotlinx.datetime.Clock 11 | import kotlin.js.JsName 12 | import kotlin.jvm.JvmOverloads 13 | import kotlin.jvm.JvmStatic 14 | 15 | 16 | object DebugChain { 17 | 18 | /** 19 | * Builds a "default" chain for verifying, i.e. one with the implementation according to spec. 20 | */ 21 | @JvmStatic 22 | @JvmOverloads 23 | @JsName("buildVerificationChain") 24 | fun buildVerificationChain( 25 | repository: CertificateRepository, 26 | clock: Clock = Clock.System, 27 | atRepository: CertificateRepository? = null 28 | ): IChain { 29 | val euContextService = DebugContextIdentifierService("HC1:") 30 | val euChain = Chain( 31 | DebugHigherOrderValidationService(), 32 | DebugSchemaValidationService(), 33 | DefaultCborService(), 34 | DebugCwtService(clock = clock), 35 | DebugCoseService(repository), 36 | DefaultCompressorService(), 37 | DefaultBase45Service(), 38 | euContextService 39 | ) 40 | if (atRepository == null) 41 | return euChain 42 | 43 | val atContextService = DebugContextIdentifierService("AT1:") 44 | val atChain = Chain( 45 | DebugHigherOrderValidationService(), 46 | DebugSchemaValidationService(false, arrayOf("AT-1.0.0")), 47 | DefaultCborService(), 48 | DebugCwtService(clock = clock), 49 | DebugCoseService(atRepository), 50 | DefaultCompressorService(), 51 | DefaultBase45Service(), 52 | atContextService 53 | ) 54 | 55 | return DelegatingChain(euChain, euContextService, atChain, atContextService) 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/debug/DebugContextIdentifierService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.debug 2 | 3 | import ehn.techiop.hcert.kotlin.chain.ContextIdentifierService 4 | import ehn.techiop.hcert.kotlin.chain.Error 5 | import ehn.techiop.hcert.kotlin.chain.NonFatalVerificationException 6 | import ehn.techiop.hcert.kotlin.chain.VerificationResult 7 | import kotlin.jvm.JvmOverloads 8 | 9 | /** 10 | * Appends/drops the Context identifier prefix from input, e.g. "HC1:" 11 | */ 12 | open class DebugContextIdentifierService @JvmOverloads constructor(private val prefix: String = "HC1:") : 13 | ContextIdentifierService { 14 | 15 | override fun encode(input: String): String { 16 | return "$prefix$input" 17 | } 18 | 19 | override fun decode(input: String, verificationResult: VerificationResult) = when { 20 | input.startsWith(prefix) -> input.drop(prefix.length) 21 | else -> throw NonFatalVerificationException( 22 | input, 23 | Error.INVALID_SCHEME_PREFIX, 24 | "No context prefix '$prefix'", 25 | details = mapOf("prefix" to prefix) 26 | ) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/debug/DebugSchemaValidationService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.debug 2 | 3 | import ehn.techiop.hcert.kotlin.chain.Error 4 | import ehn.techiop.hcert.kotlin.chain.NonFatalVerificationException 5 | import ehn.techiop.hcert.kotlin.chain.SchemaValidationService 6 | import ehn.techiop.hcert.kotlin.chain.VerificationResult 7 | import ehn.techiop.hcert.kotlin.chain.impl.SchemaValidationAdapter 8 | import ehn.techiop.hcert.kotlin.data.CborObject 9 | import ehn.techiop.hcert.kotlin.data.GreenCertificate 10 | import kotlin.jvm.JvmOverloads 11 | 12 | /** 13 | * Validates the HCERT data against the JSON schema. 14 | * Beware: By default [useFallback] is true, so we are trying to verify 15 | * the data against a very relaxed schema. 16 | */ 17 | class DebugSchemaValidationService @JvmOverloads constructor( 18 | private val useFallback: Boolean = true, 19 | private val knownVersions: Array? = null 20 | ) : 21 | SchemaValidationService { 22 | 23 | override fun validate(cbor: CborObject, verificationResult: VerificationResult): GreenCertificate { 24 | val adapter = knownVersions?.let { SchemaValidationAdapter(cbor, it) } ?: SchemaValidationAdapter(cbor) 25 | 26 | val versionString = cbor.getVersionString() ?: throw NonFatalVerificationException( 27 | adapter.toJson(), 28 | Error.CBOR_DESERIALIZATION_FAILED, 29 | "No schema version specified", 30 | details = mapOf("schemaVersion" to "null") 31 | ) 32 | if (!adapter.hasValidator(versionString)) throw NonFatalVerificationException( 33 | adapter.toJson(), 34 | Error.SCHEMA_VALIDATION_FAILED, 35 | "Schema version $versionString is not supported", 36 | details = mapOf("schemaVersion" to versionString) 37 | ) 38 | 39 | if (useFallback) { 40 | val fallbackErrors = adapter.validateWithFallback() 41 | if (fallbackErrors.isNotEmpty()) throw NonFatalVerificationException( 42 | adapter.toJson(), 43 | Error.SCHEMA_VALIDATION_FAILED, 44 | "Data does not follow fallback schema: $fallbackErrors}", 45 | details = mapOf("schemaErrors" to fallbackErrors.joinToString()) 46 | ) 47 | } else { 48 | val errors = adapter.validateBasic(versionString) 49 | if (errors.isNotEmpty()) throw NonFatalVerificationException( 50 | adapter.toJson(), 51 | Error.SCHEMA_VALIDATION_FAILED, 52 | "Data does not follow fallback schema: $errors}", 53 | details = mapOf("schemaErrors" to errors.joinToString()) 54 | ) 55 | } 56 | 57 | return adapter.toJson() 58 | } 59 | 60 | } 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/Base45Encoder.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import kotlin.math.pow 4 | import kotlin.math.roundToLong 5 | 6 | object Base45Encoder { 7 | 8 | // https://datatracker.ietf.org/doc/draft-faltstrom-base45/?include_text=1 9 | private const val ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:" 10 | 11 | fun encode(input: ByteArray) = 12 | input.asSequence().map { it.toUByte() } 13 | .chunked(2).map(this::encodeTwoCharsPadded) 14 | .flatten().joinToString(separator = "") 15 | 16 | private fun encodeTwoCharsPadded(input: List): List { 17 | val result = encodeTwoChars(input).toMutableList() 18 | when (input.size) { 19 | 1 -> if (result.size < 2) result += '0' 20 | 2 -> while (result.size < 3) result += '0' 21 | } 22 | return result 23 | } 24 | 25 | private fun encodeTwoChars(list: List) = 26 | generateSequenceByDivRem(toTwoCharValue(list), 45) 27 | .map { ALPHABET[it] }.toList() 28 | 29 | private fun toTwoCharValue(list: List) = 30 | list.reversed().foldIndexed(0L) { index, acc, element -> 31 | pow(256, index) * element.toShort() + acc 32 | } 33 | 34 | fun decode(input: String) = 35 | input.chunked(3).map(this::decodeThreeCharsPadded) 36 | .flatten().map { it.toByte() }.toByteArray() 37 | 38 | private fun decodeThreeCharsPadded(input: String): List { 39 | val result = decodeThreeChars(input).toMutableList() 40 | when (input.length) { 41 | 3 -> while (result.size < 2) result += 0U 42 | 1 -> throw IllegalArgumentException() // per spec 43 | } 44 | return result.reversed() 45 | } 46 | 47 | private fun decodeThreeChars(list: String) = 48 | generateSequenceByDivRem(fromThreeCharValue(list), 256) 49 | .map { it.toUByte() }.toList() 50 | 51 | private fun fromThreeCharValue(list: String): Long { 52 | return list.foldIndexed(0L) { index, acc: Long, element -> 53 | if (!ALPHABET.contains(element)) throw IllegalArgumentException() 54 | val result = pow(45, index) * ALPHABET.indexOf(element) + acc 55 | if (list.length == 2 && result > 255) throw IllegalArgumentException() // per spec 56 | if (result > 65535) throw IllegalArgumentException() // per spec 57 | result 58 | } 59 | } 60 | 61 | private fun generateSequenceByDivRem(seed: Long, divisor: Int) = 62 | generateSequence(seed) { if (it >= divisor) it.div(divisor) else null } 63 | .map { it.rem(divisor).toInt() } 64 | 65 | private fun pow(base: Int, exp: Int) = base.toDouble().pow(exp.toDouble()).roundToLong() 66 | 67 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/CompressorAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | /** 4 | * Can't put this into an expect class 5 | * Work around https://youtrack.jetbrains.com/issue/KT-21186 6 | */ 7 | object CompressionConstants { 8 | /** 9 | * Limit the byte array size after decompression to 5 MB. 10 | * 11 | * Reasoning: 12 | * 1. QR codes can hold at most < 4500 alphanumeric chars (https://www.qrcode.com/en/about/version.html) 13 | * Sidenote: The EHN spec recommends a compression level of Q, which limits it to at most < 2500 alphanumeric chars 14 | * (https://ec.europa.eu/health/sites/default/files/ehealth/docs/digital-green-certificates_v1_en.pdf#page=7) 15 | * This is a lower bound (since any DCC should be encodable in both Aztec and QR codes). 16 | * 2. As an additional upper bound: base45 encodes 2 bytes into 3 chars (https://datatracker.ietf.org/doc/html/draft-faltstrom-base45-04#section-4) 17 | * 3. zlib's maximum compression factor is roughly 1000:1 (http://www.zlib.net/zlib_tech.html) 18 | */ 19 | const val MAX_DECOMPRESSED_SIZE = 5 * 1024 * 1024 20 | } 21 | 22 | expect class CompressorAdapter() { 23 | 24 | fun encode(input: ByteArray, level: Int): ByteArray 25 | 26 | fun decode(input: ByteArray): ByteArray 27 | 28 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/DefaultBase45Service.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.Base45Service 4 | import ehn.techiop.hcert.kotlin.chain.Error 5 | import ehn.techiop.hcert.kotlin.chain.VerificationException 6 | import ehn.techiop.hcert.kotlin.chain.VerificationResult 7 | import io.github.aakira.napier.Napier 8 | 9 | /** 10 | * Encodes/decodes input in/from Base45 11 | */ 12 | open class DefaultBase45Service : Base45Service { 13 | 14 | private val encoder = Base45Encoder 15 | 16 | override fun encode(input: ByteArray) = 17 | encoder.encode(input) 18 | 19 | override fun decode(input: String, verificationResult: VerificationResult): ByteArray { 20 | 21 | try { 22 | return encoder.decode(input).also { Napier.d("Scanned QR code payload: $input") } 23 | } catch (e: Throwable) { 24 | throw VerificationException( 25 | Error.BASE_45_DECODING_FAILED, 26 | cause = e 27 | ).also { Napier.d("Error scanning QR code payload: $input\nCause: ${e.message}") } 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/DefaultCborService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CborService 4 | import ehn.techiop.hcert.kotlin.data.GreenCertificate 5 | import kotlinx.serialization.cbor.Cbor 6 | import kotlinx.serialization.encodeToByteArray 7 | 8 | open class DefaultCborService : CborService { 9 | 10 | override fun encode(input: GreenCertificate) = Cbor.encodeToByteArray(input) 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/DefaultCompressorService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.* 4 | import io.github.aakira.napier.Napier 5 | import kotlin.jvm.JvmOverloads 6 | 7 | /** 8 | * Compresses/decompresses input with ZLIB, [level] specifies the compression level (0-9) 9 | */ 10 | open class DefaultCompressorService @JvmOverloads constructor(private val level: Int = 9) : CompressorService { 11 | 12 | private val adapter = CompressorAdapter() 13 | 14 | /** 15 | * Compresses input with ZLIB = deflating 16 | */ 17 | override fun encode(input: ByteArray): ByteArray { 18 | return adapter.encode(input, level) 19 | } 20 | 21 | /** 22 | * Decompresses input with ZLIB = inflating. 23 | */ 24 | override fun decode(input: ByteArray, verificationResult: VerificationResult): ByteArray { 25 | try { 26 | return adapter.decode(input).also { 27 | Napier.d( 28 | """ 29 | Data is decompressable 30 | Base64: ${it.asBase64()} 31 | Hex: ${it.toHexString()} 32 | """.trimIndent() 33 | ) 34 | } 35 | } catch (e: Throwable) { 36 | throw VerificationException(Error.DECOMPRESSION_FAILED, cause = e) 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/DefaultContextIdentifierService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.ContextIdentifierService 4 | import ehn.techiop.hcert.kotlin.chain.Error 5 | import ehn.techiop.hcert.kotlin.chain.VerificationException 6 | import ehn.techiop.hcert.kotlin.chain.VerificationResult 7 | import kotlin.jvm.JvmOverloads 8 | 9 | /** 10 | * Appends/drops the Context identifier prefix from input, e.g. "HC1:" 11 | */ 12 | open class DefaultContextIdentifierService @JvmOverloads constructor(private val prefix: String = "HC1:") : 13 | ContextIdentifierService { 14 | 15 | override fun encode(input: String): String { 16 | return "$prefix$input" 17 | } 18 | 19 | override fun decode(input: String, verificationResult: VerificationResult) = when { 20 | input.startsWith(prefix) -> input.drop(prefix.length) 21 | else -> throw VerificationException( 22 | Error.INVALID_SCHEME_PREFIX, 23 | "No context prefix '$prefix'", 24 | details = mapOf("prefix" to prefix) 25 | ) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/DefaultCoseService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CertificateRepository 4 | import ehn.techiop.hcert.kotlin.chain.CoseService 5 | import ehn.techiop.hcert.kotlin.chain.CryptoService 6 | import ehn.techiop.hcert.kotlin.chain.Error 7 | import ehn.techiop.hcert.kotlin.chain.VerificationException 8 | import ehn.techiop.hcert.kotlin.chain.VerificationResult 9 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 10 | import ehn.techiop.hcert.kotlin.trust.CoseAdapter 11 | import ehn.techiop.hcert.kotlin.trust.CoseCreationAdapter 12 | 13 | 14 | /** 15 | * Encodes/decodes input as a Sign1Message according to COSE specification (RFC8152) 16 | */ 17 | open class DefaultCoseService private constructor( 18 | private val signingService: CryptoService?, 19 | private val verificationRepo: CertificateRepository? 20 | ) : CoseService { 21 | 22 | constructor(signingService: CryptoService) : this(signingService, null) 23 | 24 | constructor(verificationRepo: CertificateRepository) : this(null, verificationRepo) 25 | 26 | override fun encode(input: ByteArray): ByteArray { 27 | if (signingService == null) throw NotImplementedError() 28 | val coseAdapter = CoseCreationAdapter(input) 29 | signingService.getCborHeaders().forEach { 30 | coseAdapter.addProtectedAttribute(it.first, it.second) 31 | } 32 | coseAdapter.sign(signingService.getCborSigningKey()) 33 | return coseAdapter.encode() 34 | } 35 | 36 | override fun decode(input: ByteArray, verificationResult: VerificationResult): ByteArray { 37 | val coseAdapter = CoseAdapter(input) 38 | val kid = coseAdapter.getProtectedAttributeByteArray(CoseHeaderKeys.KID.intVal) 39 | ?: coseAdapter.getUnprotectedAttributeByteArray(CoseHeaderKeys.KID.intVal) 40 | ?: throw VerificationException(Error.KEY_NOT_IN_TRUST_LIST, "KID not found") 41 | // TODO is the algorithm relevant? 42 | //val algorithm = coseAdapter.getProtectedAttributeInt(CoseHeaderKeys.Algorithm.value) 43 | if (verificationRepo != null) { 44 | if (!coseAdapter.validate(kid, verificationRepo, verificationResult)) 45 | throw VerificationException(Error.SIGNATURE_INVALID, "Not validated") 46 | } else if (signingService != null) { 47 | if (!coseAdapter.validate(kid, signingService, verificationResult)) 48 | throw VerificationException(Error.SIGNATURE_INVALID, "Not validated") 49 | } else { 50 | // safe to throw this "ugly" error, as it should not happen 51 | throw NotImplementedError() 52 | } 53 | return coseAdapter.getContent() 54 | } 55 | 56 | override fun getVerificationRepo() = verificationRepo 57 | } 58 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/DefaultCryptoService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.* 4 | import ehn.techiop.hcert.kotlin.crypto.CertificateAdapter 5 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 6 | import ehn.techiop.hcert.kotlin.crypto.CryptoAdapter 7 | import ehn.techiop.hcert.kotlin.crypto.PubKey 8 | 9 | open class DefaultCryptoService internal constructor( 10 | private val cryptoAdapter: CryptoAdapter 11 | ) : CryptoService { 12 | 13 | override fun getCborHeaders() = listOf( 14 | Pair(CoseHeaderKeys.ALGORITHM, cryptoAdapter.algorithm), 15 | Pair(CoseHeaderKeys.KID, cryptoAdapter.certificate.kid) 16 | ) 17 | 18 | override fun getCborSigningKey() = cryptoAdapter.privateKey 19 | 20 | override fun getCborVerificationKey(kid: ByteArray, verificationResult: VerificationResult): PubKey { 21 | if (!(cryptoAdapter.certificate.kid contentEquals kid)) 22 | throw VerificationException( 23 | Error.KEY_NOT_IN_TRUST_LIST, 24 | "kid not known: $kid", 25 | details = mapOf("hexEncodedKid" to kid.toHexString()) 26 | ) 27 | 28 | verificationResult.setCertificateData(cryptoAdapter.certificate) 29 | return cryptoAdapter.certificate.publicKey 30 | } 31 | 32 | override fun getCertificate(): CertificateAdapter = cryptoAdapter.certificate 33 | 34 | override fun exportPrivateKeyAsPem() = "-----BEGIN PRIVATE KEY-----\n" + 35 | base64forPem(cryptoAdapter.privateKeyEncoded) + 36 | "\n-----END PRIVATE KEY-----\n" 37 | 38 | override fun exportCertificateAsPem() = "-----BEGIN CERTIFICATE-----\n" + 39 | base64forPem(cryptoAdapter.certificate.encoded) + 40 | "\n-----END CERTIFICATE-----\n" 41 | 42 | private fun base64forPem(encoded: ByteArray) = 43 | encoded.asBase64().chunked(64).joinToString(separator = "\n") 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/DefaultSchemaValidationService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.Error 4 | import ehn.techiop.hcert.kotlin.chain.SchemaValidationService 5 | import ehn.techiop.hcert.kotlin.chain.VerificationException 6 | import ehn.techiop.hcert.kotlin.chain.VerificationResult 7 | import ehn.techiop.hcert.kotlin.data.CborObject 8 | import ehn.techiop.hcert.kotlin.data.GreenCertificate 9 | import kotlin.jvm.JvmOverloads 10 | 11 | /** 12 | * Validates the HCERT data against the JSON schema. 13 | * Beware: By default [useFallback] is true, so we are trying to verify 14 | * the data against a very relaxed schema. 15 | */ 16 | class DefaultSchemaValidationService @JvmOverloads constructor( 17 | private val useFallback: Boolean = true, 18 | private val knownVersions: Array? = null 19 | ) : 20 | SchemaValidationService { 21 | 22 | override fun validate(cbor: CborObject, verificationResult: VerificationResult): GreenCertificate { 23 | val adapter = knownVersions?.let { SchemaValidationAdapter(cbor, it) } ?: SchemaValidationAdapter(cbor) 24 | 25 | val versionString = cbor.getVersionString() ?: throw VerificationException( 26 | Error.CBOR_DESERIALIZATION_FAILED, 27 | "No schema version specified", 28 | details = mapOf("schemaVersion" to "null") 29 | ) 30 | if (!adapter.hasValidator(versionString)) throw VerificationException( 31 | Error.SCHEMA_VALIDATION_FAILED, 32 | "Schema version $versionString is not supported", 33 | details = mapOf("schemaVersion" to versionString) 34 | ) 35 | 36 | if (useFallback) { 37 | val fallbackErrors = adapter.validateWithFallback() 38 | if (fallbackErrors.isNotEmpty()) throw VerificationException( 39 | Error.SCHEMA_VALIDATION_FAILED, 40 | "Data does not follow fallback schema: $fallbackErrors}", 41 | details = mapOf("schemaErrors" to fallbackErrors.joinToString()) 42 | ) 43 | } else { 44 | val errors = adapter.validateBasic(versionString) 45 | if (errors.isNotEmpty()) throw VerificationException( 46 | Error.SCHEMA_VALIDATION_FAILED, 47 | "Data does not follow fallback schema: $errors}", 48 | details = mapOf("schemaErrors" to errors.joinToString()) 49 | ) 50 | } 51 | 52 | return adapter.toJson() 53 | } 54 | 55 | } 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/FileBasedCryptoService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.crypto.CryptoAdapter 4 | 5 | class FileBasedCryptoService( 6 | pemEncodedPrivateKey: String, 7 | pemEncodedCertificate: String 8 | ) : DefaultCryptoService(CryptoAdapter(pemEncodedPrivateKey, pemEncodedCertificate)) -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/PrefilledCertificateRepository.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.* 4 | import ehn.techiop.hcert.kotlin.crypto.CertificateAdapter 5 | 6 | class PrefilledCertificateRepository : CertificateRepository { 7 | 8 | private val list = mutableListOf() 9 | 10 | constructor(vararg certificates: CertificateAdapter) { 11 | certificates.forEach { list += it } 12 | } 13 | 14 | constructor(vararg pemEncodedCertificates: String) { 15 | pemEncodedCertificates.forEach { list += CertificateAdapter(it) } 16 | } 17 | 18 | constructor(pemEncoded: String) { 19 | list += CertificateAdapter(pemEncoded) 20 | } 21 | 22 | constructor() 23 | 24 | override fun loadTrustedCertificates( 25 | kid: ByteArray, 26 | verificationResult: VerificationResult 27 | ): List { 28 | val certList = list.filter { it.kid contentEquals kid } 29 | if (certList.isEmpty()) 30 | throw VerificationException( 31 | Error.KEY_NOT_IN_TRUST_LIST, 32 | "kid not found", 33 | details = mapOf("hexEncodedKid" to kid.toHexString()) 34 | ) 35 | 36 | return certList 37 | } 38 | 39 | override fun toString() = "PrefilledCertificateRepository(" + list.joinToString { it.prettyPrint() } + ")" 40 | 41 | 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/RandomEcKeyCryptoService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.crypto.CryptoAdapter 4 | import ehn.techiop.hcert.kotlin.crypto.KeyType 5 | import ehn.techiop.hcert.kotlin.trust.ContentType 6 | import kotlinx.datetime.Clock 7 | import kotlin.jvm.JvmOverloads 8 | 9 | class RandomEcKeyCryptoService @JvmOverloads constructor( 10 | keySize: Int = 256, 11 | contentType: List = ContentType.values().toList(), 12 | clock: Clock = Clock.System 13 | ) : DefaultCryptoService(CryptoAdapter(KeyType.EC, keySize, contentType, clock)) -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/RandomRsaKeyCryptoService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.crypto.CryptoAdapter 4 | import ehn.techiop.hcert.kotlin.crypto.KeyType 5 | import ehn.techiop.hcert.kotlin.trust.ContentType 6 | import kotlinx.datetime.Clock 7 | import kotlin.jvm.JvmOverloads 8 | 9 | class RandomRsaKeyCryptoService @JvmOverloads constructor( 10 | keySize: Int = 2048, 11 | contentType: List = ContentType.values().toList(), 12 | clock: Clock = Clock.System 13 | ) : DefaultCryptoService(CryptoAdapter(KeyType.RSA, keySize, contentType, clock)) -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/SchemaValidationAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.data.CborObject 4 | import ehn.techiop.hcert.kotlin.data.GreenCertificate 5 | 6 | //As of 1.3.0 our codebase handles all version equally well 7 | 8 | 9 | //we need to work around Duplicate JVM class name bug → we can skip expect definitions altogether 10 | abstract class SchemaLoader(vararg validVersions: String = KNOWN_VERSIONS) { 11 | 12 | companion object { 13 | internal val KNOWN_VERSIONS = arrayOf( 14 | "1.0.0", 15 | "1.0.1", 16 | "1.1.0", 17 | "1.2.0", 18 | "1.2.1", 19 | "1.3.0", 20 | "1.3.1", 21 | "1.3.2" 22 | ) 23 | } 24 | 25 | internal val validators = validVersions.mapIndexed { i, version -> 26 | validVersions[i] to loadSchema(version) 27 | }.toMap() 28 | 29 | internal abstract fun loadSchema(version: String): T 30 | 31 | internal abstract fun loadFallbackSchema(): T 32 | 33 | } 34 | 35 | expect class SchemaValidationAdapter(cbor: CborObject, validVersions: Array = SchemaLoader.KNOWN_VERSIONS) { 36 | 37 | fun hasValidator(versionString: String): Boolean 38 | fun validateBasic(versionString: String): Collection 39 | fun toJson(): GreenCertificate 40 | fun validateWithFallback(): Collection 41 | 42 | } 43 | 44 | data class SchemaError(val error: String) 45 | 46 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/TrustListCertificateRepository.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.* 4 | import ehn.techiop.hcert.kotlin.crypto.CertificateAdapter 5 | import ehn.techiop.hcert.kotlin.trust.SignedData 6 | import ehn.techiop.hcert.kotlin.trust.TrustListDecodeService 7 | import kotlinx.datetime.Clock 8 | import kotlin.jvm.JvmOverloads 9 | 10 | 11 | class TrustListCertificateRepository @JvmOverloads constructor( 12 | trustList: SignedData, 13 | certificateRepository: CertificateRepository, 14 | clock: Clock = Clock.System, 15 | ) : CertificateRepository { 16 | 17 | private val list = TrustListDecodeService(certificateRepository, clock).decode(trustList).second.certificates 18 | 19 | override fun loadTrustedCertificates( 20 | kid: ByteArray, 21 | verificationResult: VerificationResult 22 | ): List { 23 | val certList = list.filter { it.kid contentEquals kid } 24 | if (certList.isEmpty()) 25 | throw VerificationException( 26 | Error.KEY_NOT_IN_TRUST_LIST, "kid not found", 27 | details = mapOf("hexEncodedKid" to kid.toHexString()) 28 | ) 29 | 30 | return certList.map { it.toCertificateAdapter() } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/crypto/CertificateAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | import ehn.techiop.hcert.kotlin.trust.ContentType 4 | import ehn.techiop.hcert.kotlin.trust.TrustedCertificateV2 5 | import kotlinx.datetime.Instant 6 | 7 | expect class CertificateAdapter(pemEncoded: String) { 8 | 9 | constructor(_encoded: ByteArray) 10 | 11 | val validContentTypes: List 12 | val validFrom: Instant 13 | val validUntil: Instant 14 | val subjectCountry: String? 15 | val publicKey: PubKey 16 | fun toTrustedCertificate(): TrustedCertificateV2 17 | val kid: ByteArray 18 | val encoded: ByteArray 19 | 20 | fun prettyPrint(): String 21 | } 22 | 23 | interface PrivKey 24 | 25 | interface PubKey 26 | 27 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/crypto/CryptoAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | import ehn.techiop.hcert.kotlin.trust.ContentType 4 | import kotlinx.datetime.Clock 5 | 6 | expect class CryptoAdapter { 7 | 8 | constructor(keyType: KeyType = KeyType.EC, keySize: Int = 256, contentType: List, clock: Clock) 9 | 10 | constructor(pemEncodedPrivateKey: String, pemEncodedCertificate: String) 11 | 12 | val privateKey: PrivKey 13 | val publicKey: PubKey 14 | val algorithm: CwtAlgorithm 15 | val certificate: CertificateAdapter 16 | val privateKeyEncoded: ByteArray 17 | 18 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/crypto/Datatypes.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | 4 | enum class CoseHeaderKeys(val intVal: Int, val stringVal: String) { 5 | ALGORITHM(1, "alg"), 6 | CRITICAL_HEADERS(2, "crit"), 7 | CONTENT_TYPE(3, "content type"), 8 | KID(4, "kid"), 9 | IV(5, "IV"), 10 | PARTIAL_IV(6, "Partial IV"), 11 | TRUSTLIST_VERSION(42, "tlv"), 12 | BUSINESS_RULES_VERSION(-65537, "brv"), 13 | VALUE_SET_VERSION(-65538, "vsv"); 14 | 15 | companion object { 16 | fun fromIntVal(intVal: Int) = values().firstOrNull { it.intVal == intVal } 17 | } 18 | } 19 | 20 | enum class CwtHeaderKeys(val intVal: Int, val stringVal: String) { 21 | ISSUER(1, "iss"), 22 | SUBJECT(2, "sub"), 23 | AUDIENCE(3, "aud"), 24 | EXPIRATION(4, "exp"), 25 | NOT_BEFORE(5, "nbf"), 26 | ISSUED_AT(6, "iat"), 27 | CWT_ID(7, "cti"), 28 | HCERT(-260, "hcert"), 29 | EUDGC_IN_HCERT(1, "eu_dgc_v1"); 30 | 31 | 32 | companion object { 33 | fun fromIntVal(intVal: Int) = values().firstOrNull { it.intVal == intVal } 34 | } 35 | } 36 | 37 | 38 | enum class CwtAlgorithm(val intVal: Int, val stringVal: String) { 39 | 40 | 41 | ECDSA_256(-7, "ES256"), 42 | ECDSA_384(-35, "ES384"), 43 | RSA_PSS_256(-37, "PS256"); 44 | 45 | 46 | companion object { 47 | fun fromIntVal(intVal: Int) = values().firstOrNull { it.intVal == intVal } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/crypto/KeyType.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | enum class KeyType { 4 | EC, 5 | RSA 6 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/crypto/PkiUtils.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | import ehn.techiop.hcert.kotlin.trust.ContentType 4 | import kotlinx.datetime.Clock 5 | 6 | expect object PkiUtils { 7 | 8 | fun selfSignCertificate( 9 | privateKey: PrivKey, 10 | publicKey: PubKey, 11 | keySize: Int, 12 | contentType: List = ContentType.values().toList(), 13 | clock: Clock = Clock.System 14 | ): CertificateAdapter 15 | 16 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/data/CborObject.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | //This is actually a platform type 4 | interface CborObject { 5 | fun toJsonString(): String 6 | fun getVersionString(): String? 7 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/data/LenientInstantParser.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import kotlinx.datetime.Instant 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.descriptors.PrimitiveKind 6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 7 | import kotlinx.serialization.descriptors.SerialDescriptor 8 | import kotlinx.serialization.encoding.Decoder 9 | import kotlinx.serialization.encoding.Encoder 10 | 11 | 12 | /** 13 | * Tries to parse an [Instant], see [InstantParser.parseInstant] 14 | */ 15 | object LenientInstantParser : KSerializer { 16 | 17 | override val descriptor: SerialDescriptor = 18 | PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING) 19 | 20 | override fun deserialize(decoder: Decoder): Instant { 21 | return InstantParser.parseInstant(decoder.decodeString()) 22 | } 23 | 24 | override fun serialize(encoder: Encoder, value: Instant) { 25 | encoder.encodeString(value.toString()) 26 | } 27 | } 28 | 29 | 30 | /** 31 | * Tries to parse an [Instant] that may be null, see [InstantParser.parseInstant] 32 | */ 33 | object LenientNullableInstantParser : KSerializer { 34 | 35 | override val descriptor: SerialDescriptor = 36 | PrimitiveSerialDescriptor("Instant?", PrimitiveKind.STRING) 37 | 38 | override fun deserialize(decoder: Decoder): Instant? { 39 | return try { 40 | InstantParser.parseInstant(decoder.decodeString()) 41 | } catch (e: Throwable) { 42 | null 43 | } 44 | } 45 | 46 | override fun serialize(encoder: Encoder, value: Instant?) { 47 | if (value != null) { 48 | encoder.encodeString(value.toString()) 49 | } else { 50 | encoder.encodeNull() 51 | } 52 | } 53 | 54 | } 55 | 56 | 57 | internal object InstantParser { 58 | 59 | /** 60 | * Some countries encode Instants in a wrong format, 61 | * e.g. missing "Z" or the offset "+0200" instead of "+02:00", 62 | * so we'll try to work around those issues 63 | */ 64 | fun parseInstant(value: String): Instant { 65 | val fixOffset = value.replace(Regex("\\+(\\d{2})(\\d{2})")) { "+${it.groupValues[1]}:${it.groupValues[2]}" } 66 | val fixZulu = if (fixOffset.contains('Z') || fixOffset.contains("+")) fixOffset else fixOffset + 'Z' 67 | return Instant.parse(fixZulu) 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/data/LenientLocalDateParser.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import kotlinx.datetime.LocalDate 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.descriptors.PrimitiveKind 6 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 7 | import kotlinx.serialization.descriptors.SerialDescriptor 8 | import kotlinx.serialization.encoding.Decoder 9 | import kotlinx.serialization.encoding.Encoder 10 | 11 | 12 | /** 13 | * Some countries include a timestamp, e.g. "T00:00:00", 14 | * where everybody else expects YYYY-MM-DD only, 15 | * so we'll strip it, to be able to parse it. 16 | */ 17 | object LenientLocalDateParser : KSerializer { 18 | 19 | override val descriptor: SerialDescriptor = 20 | PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING) 21 | 22 | override fun deserialize(decoder: Decoder): LocalDate { 23 | val value = decoder.decodeString() 24 | return LocalDate.parse(value.substringBefore("T")) 25 | } 26 | 27 | override fun serialize(encoder: Encoder, value: LocalDate) { 28 | encoder.encodeString(value.toString()) 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/data/Person.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Person( 8 | @SerialName("fn") 9 | val familyName: String? = null, 10 | 11 | @SerialName("fnt") 12 | val familyNameTransliterated: String, 13 | 14 | @SerialName("gn") 15 | val givenName: String? = null, 16 | 17 | @SerialName("gnt") 18 | val givenNameTransliterated: String? = null, 19 | ) 20 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/data/RecoveryStatement.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import kotlinx.datetime.LocalDate 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class RecoveryStatement( 9 | @SerialName("tg") 10 | val target: ValueSetEntryAdapter, 11 | 12 | @SerialName("fr") 13 | @Serializable(with = LenientLocalDateParser::class) 14 | val dateOfFirstPositiveTestResult: LocalDate, 15 | 16 | @SerialName("co") 17 | val country: String, 18 | 19 | @SerialName("is") 20 | val certificateIssuer: String, 21 | 22 | @SerialName("df") 23 | @Serializable(with = LenientLocalDateParser::class) 24 | val certificateValidFrom: LocalDate, 25 | 26 | @SerialName("du") 27 | @Serializable(with = LenientLocalDateParser::class) 28 | val certificateValidUntil: LocalDate, 29 | 30 | @SerialName("ci") 31 | val certificateIdentifier: String, 32 | ) { 33 | } 34 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/data/Serializers.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.Serializer 5 | import kotlinx.serialization.encoding.Decoder 6 | import kotlinx.serialization.encoding.Encoder 7 | 8 | 9 | @Serializer(forClass = ValueSetEntryAdapter::class) 10 | object ValueSetEntryAdapterSerializer : KSerializer { 11 | 12 | override fun deserialize(decoder: Decoder): ValueSetEntryAdapter { 13 | val key = decoder.decodeString() 14 | return ValueSetsInstanceHolder.INSTANCE.find(key) 15 | } 16 | 17 | override fun serialize(encoder: Encoder, value: ValueSetEntryAdapter) { 18 | encoder.encodeString(value.key.trim()) 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/data/Test.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import kotlinx.datetime.Instant 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Test constructor( 9 | @SerialName("tg") 10 | val target: ValueSetEntryAdapter, 11 | 12 | @SerialName("tt") 13 | val type: ValueSetEntryAdapter, 14 | 15 | @SerialName("nm") 16 | val nameNaa: String? = null, 17 | 18 | @SerialName("ma") 19 | val nameRat: ValueSetEntryAdapter? = null, 20 | 21 | @SerialName("sc") 22 | @Serializable(with = LenientInstantParser::class) 23 | val dateTimeSample: Instant, 24 | 25 | @SerialName("dr") 26 | @Serializable(with = LenientNullableInstantParser::class) 27 | val dateTimeResult: Instant? = null, 28 | 29 | @SerialName("tr") 30 | val resultPositive: ValueSetEntryAdapter, 31 | 32 | @SerialName("tc") 33 | val testFacility: String? = null, 34 | 35 | @SerialName("co") 36 | val country: String, 37 | 38 | @SerialName("is") 39 | val certificateIssuer: String, 40 | 41 | @SerialName("ci") 42 | val certificateIdentifier: String, 43 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/data/Vaccination.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import kotlinx.datetime.LocalDate 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Vaccination( 9 | @SerialName("tg") 10 | val target: ValueSetEntryAdapter, 11 | 12 | @SerialName("vp") 13 | val vaccine: ValueSetEntryAdapter, 14 | 15 | @SerialName("mp") 16 | val medicinalProduct: ValueSetEntryAdapter, 17 | 18 | @SerialName("ma") 19 | val authorizationHolder: ValueSetEntryAdapter, 20 | 21 | @SerialName("dn") 22 | val doseNumber: Int, 23 | 24 | @SerialName("sd") 25 | val doseTotalNumber: Int, 26 | 27 | @SerialName("dt") 28 | @Serializable(with = LenientLocalDateParser::class) 29 | val date: LocalDate, 30 | 31 | @SerialName("co") 32 | val country: String, 33 | 34 | @SerialName("is") 35 | val certificateIssuer: String, 36 | 37 | @SerialName("ci") 38 | val certificateIdentifier: String, 39 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/data/VaccinationExemption.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import kotlinx.datetime.LocalDate 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class VaccinationExemption( 9 | @SerialName("tg") 10 | val target: ValueSetEntryAdapter, 11 | 12 | @SerialName("du") 13 | @Serializable(with = LenientLocalDateParser::class) 14 | val date: LocalDate, 15 | 16 | @SerialName("co") 17 | val country: String, 18 | 19 | @SerialName("is") 20 | val certificateIssuer: String, 21 | 22 | @SerialName("ci") 23 | val certificateIdentifier: String, 24 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/data/ValueSets.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import kotlinx.datetime.LocalDate 4 | import kotlinx.serialization.Serializable 5 | 6 | val inputPaths = listOf( 7 | "/value-sets/disease-agent-targeted.json", 8 | "/value-sets/test-manf.json", 9 | "/value-sets/test-type.json", 10 | "/value-sets/test-result.json", 11 | "/value-sets/vaccine-mah-manf.json", 12 | "/value-sets/vaccine-medicinal-product.json", 13 | "/value-sets/vaccine-prophylaxis.json", 14 | //"/value-sets/country-2-codes.json", 15 | ) 16 | 17 | /** 18 | * Holds a list of value sets, that get lazily loaded from the JSON files in "src/main/resources/value-sets". 19 | */ 20 | data class ValueSetHolder( 21 | val valueSets: List 22 | ) { 23 | fun find(valueSetId: String, key: String): ValueSetEntryAdapter { 24 | // we'll trim it, to work around entries containing whitespace 25 | val trimmed = key.trim() 26 | return valueSets.firstOrNull { it.valueSetId == valueSetId } 27 | ?.valueSetValues?.get(trimmed)?.let { ValueSetEntryAdapter(trimmed, it) } 28 | ?: return buildMissingValueSetEntry(trimmed, valueSetId) 29 | } 30 | 31 | fun find(key: String): ValueSetEntryAdapter { 32 | val trimmed = key.trim() 33 | valueSets.forEach { 34 | if (it.valueSetValues.containsKey(trimmed)) 35 | return ValueSetEntryAdapter(trimmed, it.valueSetValues[trimmed]!!) 36 | } 37 | return buildMissingValueSetEntry(trimmed, null) 38 | } 39 | 40 | private fun buildMissingValueSetEntry(key: String, valueSetId: String?): ValueSetEntryAdapter { 41 | val entry = ValueSetEntry(key, "en", false, "http://example.com/missing", "0", valueSetId) 42 | return ValueSetEntryAdapter(key, entry) 43 | } 44 | 45 | } 46 | 47 | expect object ValueSetsInstanceHolder { 48 | val INSTANCE: ValueSetHolder 49 | } 50 | 51 | @Serializable 52 | data class ValueSet constructor( 53 | val valueSetId: String, 54 | val valueSetDate: LocalDate, 55 | val valueSetValues: Map 56 | ) 57 | 58 | @Serializable 59 | data class ValueSetEntry( 60 | val display: String, 61 | val lang: String, 62 | val active: Boolean, 63 | val system: String, 64 | val version: String, 65 | val valueSetId: String? = null 66 | ) 67 | 68 | @Serializable(with = ValueSetEntryAdapterSerializer::class) 69 | data class ValueSetEntryAdapter( 70 | val key: String, 71 | val valueSetEntry: ValueSetEntry 72 | ) 73 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/log/LoggingUtils.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.log 2 | 3 | import io.github.aakira.napier.Antilog 4 | import io.github.aakira.napier.LogLevel 5 | import kotlin.math.floor 6 | import kotlin.math.roundToLong 7 | 8 | internal var globalLogLevel: LogLevel? = null 9 | 10 | fun setLogLevel(level:LogLevel?) { 11 | globalLogLevel = level 12 | } 13 | 14 | expect open class BasicLogger(defaultTag: String? = null) : Antilog 15 | 16 | 17 | fun Int.formatMag() = toLong().formatMag() 18 | fun Long.formatMag(): String { 19 | val toString = toString() 20 | return when { 21 | this < 1024 -> toString 22 | this < 1024 * 1024 -> format() + "Ki" 23 | this < 1024 * 1024 * 1024 -> { 24 | val m = (toDouble() / 1024.toDouble()).roundToLong() 25 | m.format() + "Mi" 26 | } 27 | else -> { 28 | val g = (toDouble() / (1024 * 1024).toDouble()).roundToLong() 29 | g.format() + "Gi" 30 | } 31 | } 32 | } 33 | 34 | private fun Long.format(): String { 35 | val mag = floor(this.toDouble() / 1024.0).toInt() 36 | val toString = (1000 + (this % 1024)).toString().substring(1) 37 | return "$mag.${toString}" 38 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/rules/BusinessRule.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.rules 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * One specific business rule, as raw JSON 8 | */ 9 | @Serializable 10 | data class BusinessRule( 11 | 12 | @SerialName("i") 13 | val identifier: String, 14 | 15 | @SerialName("r") 16 | val rule: String 17 | 18 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/rules/BusinessRulesContainer.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.rules 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | 7 | /** 8 | * Contains a list of business rules, see [BusinessRule] 9 | */ 10 | @Serializable 11 | data class BusinessRulesContainer( 12 | 13 | @SerialName("r") 14 | val rules: Array 15 | 16 | ) { 17 | override fun equals(other: Any?): Boolean { 18 | if (this === other) return true 19 | if (other == null || this::class != other::class) return false 20 | 21 | other as BusinessRulesContainer 22 | 23 | if (!rules.contentEquals(other.rules)) return false 24 | 25 | return true 26 | } 27 | 28 | override fun hashCode(): Int { 29 | return rules.contentHashCode() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/rules/BusinessRulesDecodeService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.rules 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CertificateRepository 4 | import ehn.techiop.hcert.kotlin.chain.Error.TRUST_SERVICE_ERROR 5 | import ehn.techiop.hcert.kotlin.chain.VerificationException 6 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 7 | import ehn.techiop.hcert.kotlin.trust.SignedData 8 | import ehn.techiop.hcert.kotlin.trust.SignedDataDecodeService 9 | import ehn.techiop.hcert.kotlin.trust.SignedDataParsed 10 | import kotlinx.datetime.Clock 11 | import kotlinx.serialization.cbor.Cbor 12 | import kotlinx.serialization.decodeFromByteArray 13 | import kotlin.jvm.JvmOverloads 14 | 15 | /** 16 | * Decodes a [SignedData] blob, expected to contain the content and signature of a [BusinessRulesContainer] 17 | * 18 | * - [repository] contains the trust anchor for the parsed file 19 | * - [clock] defines the current time to use for validity checks 20 | * - [clockSkewSeconds] defines the error margin when comparing time validity of the parsed file in seconds 21 | */ 22 | class BusinessRulesDecodeService @JvmOverloads constructor( 23 | repository: CertificateRepository, 24 | clock: Clock = Clock.System, 25 | clockSkewSeconds: Int = 300 26 | ) { 27 | 28 | private val decodeService = SignedDataDecodeService(repository, clock, clockSkewSeconds) 29 | 30 | /** 31 | * See [SignedData] for details about the content 32 | * If all checks succeed, [input.content] is parsed as a [BusinessRulesContainer] 33 | */ 34 | @Throws(VerificationException::class) 35 | fun decode(input: SignedData): Pair { 36 | val parsed = decodeService.decode(input, listOf(CoseHeaderKeys.BUSINESS_RULES_VERSION)) 37 | when (val version = parsed.headers[CoseHeaderKeys.BUSINESS_RULES_VERSION]) { 38 | 1 -> return Pair(parsed, Cbor.decodeFromByteArray(parsed.content)) 39 | else -> throw VerificationException( 40 | TRUST_SERVICE_ERROR, 41 | "Version unknown", 42 | details = mapOf("businessRulesVersion" to (version ?: "null").toString()) 43 | ) 44 | } 45 | } 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/rules/BusinessRulesV1EncodeService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.rules 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoService 4 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 5 | import ehn.techiop.hcert.kotlin.trust.SignedData 6 | import ehn.techiop.hcert.kotlin.trust.SignedDataEncodeService 7 | import kotlinx.datetime.Clock 8 | import kotlinx.serialization.cbor.Cbor 9 | import kotlinx.serialization.encodeToByteArray 10 | import kotlin.jvm.JvmOverloads 11 | import kotlin.time.Duration 12 | import kotlin.time.Duration.Companion.hours 13 | 14 | 15 | /** 16 | * Encodes a list of business rules as [SignedData] object 17 | */ 18 | class BusinessRulesV1EncodeService constructor( 19 | signingService: CryptoService, 20 | validity: Duration = 48.hours, 21 | clock: Clock = Clock.System, 22 | ) { 23 | 24 | @JvmOverloads 25 | constructor(signingService: CryptoService, validityHours: Int = 48) 26 | : this(signingService,validityHours.hours, Clock.System) 27 | 28 | private val signedDataService = SignedDataEncodeService(signingService, validity, clock) 29 | 30 | /** 31 | * Content is a CBOR encoded [BusinessRulesContainer] object, i.e. a list of business rules 32 | */ 33 | private fun encodeContent(input: List): ByteArray { 34 | val content = BusinessRulesContainer(input.toTypedArray()) 35 | return Cbor.encodeToByteArray(content) 36 | } 37 | 38 | /** 39 | * See [SignedData] for details about returned structure 40 | */ 41 | fun encode(input: List): SignedData { 42 | val content = encodeContent(input) 43 | val headers = mapOf(CoseHeaderKeys.BUSINESS_RULES_VERSION to 1) 44 | return signedDataService.wrapWithSignature(content, headers) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/ContentType.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | /** 4 | * Document signing certificates may include an extended key usage extension, 5 | * that contains a list of OID values, that map to valid HCERT contents, 6 | * that this certificate is allowed to sign. 7 | */ 8 | enum class ContentType(val oid: String, val alternativeOid: Collection) { 9 | 10 | TEST("1.3.6.1.4.1.1847.2021.1.1", listOf("1.3.6.1.4.1.0.1847.2021.1.1")), 11 | 12 | VACCINATION("1.3.6.1.4.1.1847.2021.1.2", listOf("1.3.6.1.4.1.0.1847.2021.1.2")), 13 | 14 | RECOVERY("1.3.6.1.4.1.1847.2021.1.3", listOf("1.3.6.1.4.1.0.1847.2021.1.3")); 15 | 16 | companion object { 17 | fun findByOid(oid: String): ContentType? { 18 | return values().firstOrNull { it.oid == oid || it.alternativeOid.contains(oid) } 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/CoseAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CertificateRepository 4 | import ehn.techiop.hcert.kotlin.chain.CryptoService 5 | import ehn.techiop.hcert.kotlin.chain.VerificationResult 6 | 7 | /** 8 | * Adapter to deserialize COSE structures on all targets 9 | */ 10 | expect class CoseAdapter constructor(input: ByteArray) { 11 | 12 | fun getProtectedAttributeByteArray(key: Int): ByteArray? 13 | 14 | fun getUnprotectedAttributeByteArray(key: Int): ByteArray? 15 | 16 | fun getProtectedAttributeInt(key: Int): Int? 17 | 18 | fun validate(kid: ByteArray, repository: CertificateRepository): Boolean 19 | 20 | fun validate(kid: ByteArray, repository: CertificateRepository, verificationResult: VerificationResult): Boolean 21 | 22 | fun validate(kid: ByteArray, cryptoService: CryptoService, verificationResult: VerificationResult): Boolean 23 | 24 | fun getContent(): ByteArray 25 | 26 | fun getContentMap(): CwtAdapter 27 | 28 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/CoseCreationAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 4 | import ehn.techiop.hcert.kotlin.crypto.PrivKey 5 | 6 | /** 7 | * Adapter to create and serialize COSE structures on all targets 8 | */ 9 | expect class CoseCreationAdapter constructor(content: ByteArray) { 10 | 11 | fun addProtectedAttribute(key: CoseHeaderKeys, value: Any) 12 | 13 | fun addUnprotectedAttribute(key: CoseHeaderKeys, value: Any) 14 | 15 | fun sign(key: PrivKey) 16 | 17 | fun encode(): ByteArray 18 | 19 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/CwtAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import ehn.techiop.hcert.kotlin.data.CborObject 4 | 5 | /** 6 | * Adapter to deserialize CWT structures (CBOR maps) on all targets 7 | */ 8 | interface CwtAdapter { 9 | 10 | fun getByteArray(key: Int): ByteArray? 11 | 12 | fun getString(key: Int): String? 13 | 14 | fun getNumber(key: Int): Number? 15 | 16 | fun getMap(key: Int): CwtAdapter? 17 | 18 | fun toCborObject(): CborObject 19 | } 20 | 21 | expect object CwtHelper { 22 | fun fromCbor(input: ByteArray): CwtAdapter 23 | } 24 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/CwtCreationAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | /** 4 | * Adapter to create and serialize CWT structures (CBOR maps) on all targets 5 | */ 6 | expect class CwtCreationAdapter constructor() { 7 | 8 | fun add(key: Int, value: Any) 9 | 10 | fun addDgc(key: Int, innerKey: Int, input: ByteArray) 11 | 12 | fun encode(): ByteArray 13 | 14 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/Hash.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | /** 4 | * Adapter to access SHA-256 hashing on all targets 5 | */ 6 | expect class Hash constructor(input: ByteArray) { 7 | fun calc(): ByteArray 8 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/SignedData.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 4 | import ehn.techiop.hcert.kotlin.crypto.CwtHeaderKeys 5 | 6 | /** 7 | * Holds a [content] and detached [signature] in a certain format: 8 | * [signature] is an encoded COSE structure with protected header keys 9 | * - [CoseHeaderKeys.TRUSTLIST_VERSION] 10 | * - [CoseHeaderKeys.KID]: KID of the signer certificate 11 | * The content of that COSE structure is a CWT map containing 12 | * - [CwtHeaderKeys.NOT_BEFORE]: seconds since UNIX epoch 13 | * - [CwtHeaderKeys.EXPIRATION]: seconds since UNIX epoch 14 | * - [CwtHeaderKeys.SUBJECT]: the SHA-256 hash of the content file 15 | */ 16 | data class SignedData( 17 | val content: ByteArray, 18 | val signature: ByteArray 19 | ) { 20 | override fun equals(other: Any?): Boolean { 21 | if (this === other) return true 22 | if (other == null || this::class != other::class) return false 23 | 24 | other as SignedData 25 | 26 | if (!content.contentEquals(other.content)) return false 27 | if (!signature.contentEquals(other.signature)) return false 28 | 29 | return true 30 | } 31 | 32 | override fun hashCode(): Int { 33 | var result = content.contentHashCode() 34 | result = 31 * result + signature.contentHashCode() 35 | return result 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/SignedDataEncodeService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoService 4 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 5 | import ehn.techiop.hcert.kotlin.crypto.CwtHeaderKeys 6 | import kotlinx.datetime.Clock 7 | import kotlin.time.Duration 8 | import kotlin.time.Duration.Companion.hours 9 | 10 | 11 | /** 12 | * Encodes arbitrary content as a [SignedData] object 13 | */ 14 | class SignedDataEncodeService constructor( 15 | private val signingService: CryptoService, 16 | private val validity: Duration = 48.hours, 17 | private val clock: Clock = Clock.System, 18 | ) { 19 | 20 | /** 21 | * Signature is a COSE structure with protected header keys 22 | * - [CoseHeaderKeys.TRUSTLIST_VERSION] 23 | * - [CoseHeaderKeys.KID]: KID of the [signingService]'s certificate 24 | * The content of that COSE structure is a CWT map containing 25 | * - [CwtHeaderKeys.NOT_BEFORE]: seconds since UNIX epoch 26 | * - [CwtHeaderKeys.EXPIRATION]: seconds since UNIX epoch 27 | * - [CwtHeaderKeys.SUBJECT]: the SHA-256 hash of the content file 28 | */ 29 | private fun encodeSignature(content: ByteArray, coseHeaderKeys: Map = mapOf()): ByteArray { 30 | val validFrom = clock.now() 31 | val validUntil = validFrom + validity 32 | val hash = Hash(content).calc() 33 | val cwt = CwtCreationAdapter() 34 | cwt.add(CwtHeaderKeys.NOT_BEFORE.intVal, validFrom.epochSeconds) 35 | cwt.add(CwtHeaderKeys.EXPIRATION.intVal, validUntil.epochSeconds) 36 | cwt.add(CwtHeaderKeys.SUBJECT.intVal, hash) 37 | val cose = CoseCreationAdapter(cwt.encode()) 38 | signingService.getCborHeaders().forEach { 39 | cose.addProtectedAttribute(it.first, it.second) 40 | } 41 | coseHeaderKeys.forEach { 42 | cose.addProtectedAttribute(it.key, it.value) 43 | } 44 | cose.sign(signingService.getCborSigningKey()) 45 | return cose.encode() 46 | } 47 | 48 | fun wrapWithSignature(content: ByteArray, coseHeaderKeys: Map = mapOf()): SignedData { 49 | val signature = encodeSignature(content, coseHeaderKeys) 50 | return SignedData(content, signature) 51 | } 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/SignedDataParsed.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 4 | import kotlinx.datetime.Instant 5 | 6 | /** 7 | * Information parsed from [SignedData] 8 | */ 9 | data class SignedDataParsed( 10 | val validFrom: Instant, 11 | val validUntil: Instant, 12 | val content: ByteArray, 13 | val headers: Map 14 | ) { 15 | override fun equals(other: Any?): Boolean { 16 | if (this === other) return true 17 | if (other == null || this::class != other::class) return false 18 | 19 | other as SignedDataParsed 20 | 21 | if (validFrom != other.validFrom) return false 22 | if (validUntil != other.validUntil) return false 23 | if (!content.contentEquals(other.content)) return false 24 | if (headers != other.headers) return false 25 | 26 | return true 27 | } 28 | 29 | override fun hashCode(): Int { 30 | var result = validFrom.hashCode() 31 | result = 31 * result + validUntil.hashCode() 32 | result = 31 * result + content.contentHashCode() 33 | result = 31 * result + headers.hashCode() 34 | return result 35 | } 36 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/TrustListDecodeService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CertificateRepository 4 | import ehn.techiop.hcert.kotlin.chain.Error.TRUST_SERVICE_ERROR 5 | import ehn.techiop.hcert.kotlin.chain.VerificationException 6 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 7 | import kotlinx.datetime.Clock 8 | import kotlinx.serialization.cbor.Cbor 9 | import kotlinx.serialization.decodeFromByteArray 10 | import kotlin.jvm.JvmOverloads 11 | 12 | /** 13 | * Decodes a [SignedData] blob, expected to contain the content and signature of a [TrustListV2] 14 | * 15 | * - [repository] contains the trust anchor for the parsed file 16 | * - [clock] defines the current time to use for validity checks 17 | * - [clockSkewSeconds] defines the error margin when comparing time validity of the parsed file in seconds 18 | */ 19 | class TrustListDecodeService @JvmOverloads constructor( 20 | repository: CertificateRepository, 21 | clock: Clock = Clock.System, 22 | clockSkewSeconds: Int = 300 23 | ) { 24 | 25 | private val decodeService = SignedDataDecodeService(repository, clock, clockSkewSeconds) 26 | 27 | /** 28 | * See [SignedData] for details about the content 29 | * If all checks succeed, [input.content] is parsed as a [TrustListV2], and the certificates are and returned 30 | */ 31 | @Throws(VerificationException::class) 32 | fun decode(input: SignedData): Pair { 33 | val parsed = decodeService.decode(input, listOf(CoseHeaderKeys.TRUSTLIST_VERSION)) 34 | when (val version = parsed.headers[CoseHeaderKeys.TRUSTLIST_VERSION]) { 35 | 1 -> throw VerificationException( 36 | TRUST_SERVICE_ERROR, "Version 1", 37 | details = mapOf("trustListVersion" to version.toString()) 38 | ) 39 | 2 -> return Pair(parsed, Cbor.decodeFromByteArray(parsed.content)) 40 | else -> throw VerificationException( 41 | TRUST_SERVICE_ERROR, "Version unknown", 42 | details = mapOf("trustListVersion" to (version ?: "null").toString()) 43 | ) 44 | } 45 | } 46 | 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/TrustListV2.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | 7 | /** 8 | * Contains a list of certificates, that will be loaded by clients to get a list of trusted certificates. 9 | * This structure does not contain a signature value, it will be provided in a separate file by [TrustListV2EncodeService]. 10 | */ 11 | @Serializable 12 | data class TrustListV2( 13 | 14 | @SerialName("c") 15 | val certificates: Array 16 | 17 | ) { 18 | override fun equals(other: Any?): Boolean { 19 | if (this === other) return true 20 | if (other == null || this::class != other::class) return false 21 | 22 | other as TrustListV2 23 | 24 | if (!certificates.contentEquals(other.certificates)) return false 25 | 26 | return true 27 | } 28 | 29 | override fun hashCode(): Int { 30 | return certificates.contentHashCode() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/TrustListV2EncodeService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoService 4 | import ehn.techiop.hcert.kotlin.crypto.CertificateAdapter 5 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 6 | import kotlinx.datetime.Clock 7 | import kotlinx.serialization.cbor.Cbor 8 | import kotlinx.serialization.encodeToByteArray 9 | import kotlin.jvm.JvmOverloads 10 | import kotlin.time.Duration 11 | import kotlin.time.Duration.Companion.hours 12 | 13 | 14 | /** 15 | * Encodes a list of certificates as a [SignedData] object 16 | */ 17 | class TrustListV2EncodeService constructor( 18 | signingService: CryptoService, 19 | validity: Duration = 48.hours, 20 | clock: Clock = Clock.System, 21 | ) { 22 | 23 | @JvmOverloads 24 | constructor(signingService: CryptoService, validityHours: Int = 48) 25 | : this(signingService,validityHours.hours, Clock.System) 26 | 27 | private val signedDataService = SignedDataEncodeService(signingService, validity, clock) 28 | 29 | /** 30 | * Content is a CBOR encoded [TrustListV2] object, i.e. a list of entries that contain 31 | * a KID and a X.509 encoded certificate as bytes 32 | */ 33 | private fun encodeContent(input: Set): ByteArray { 34 | val content = TrustListV2( 35 | certificates = input.map { it.toTrustedCertificate() }.toTypedArray() 36 | ) 37 | return Cbor.encodeToByteArray(content) 38 | } 39 | 40 | /** 41 | * See [SignedData] for details about returned structure 42 | */ 43 | fun encode(input: Set): SignedData { 44 | val content = encodeContent(input) 45 | val headers = mapOf(CoseHeaderKeys.TRUSTLIST_VERSION to 2) 46 | return signedDataService.wrapWithSignature(content, headers) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/TrustedCertificate.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import ehn.techiop.hcert.kotlin.crypto.CertificateAdapter 4 | 5 | /** 6 | * Interface for a "trusted" certificate to allow more versions 7 | * of a [TrustedCertificateV2] and [TrustListV2] 8 | */ 9 | interface TrustedCertificate { 10 | 11 | val kid: ByteArray 12 | 13 | fun toCertificateAdapter(): CertificateAdapter 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/trust/TrustedCertificateV2.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import ehn.techiop.hcert.kotlin.crypto.CertificateAdapter 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.cbor.ByteString 7 | 8 | 9 | /** 10 | * Current implementation of a "trusted" certificate, encoded inside a [TrustListV2] 11 | */ 12 | @Serializable 13 | data class TrustedCertificateV2( 14 | @SerialName("i") 15 | @ByteString 16 | override val kid: ByteArray, 17 | 18 | @SerialName("c") 19 | @ByteString 20 | val certificate: ByteArray 21 | ) : TrustedCertificate { 22 | 23 | //**WARNING*** do not use lazy delegates! it *will* break!!! 24 | 25 | override fun toCertificateAdapter() = CertificateAdapter(certificate) 26 | 27 | override fun equals(other: Any?): Boolean { 28 | if (this === other) return true 29 | other ?: let { if (this::class != it::class) return false } 30 | 31 | other as TrustedCertificateV2 32 | 33 | if (!kid.contentEquals(other.kid)) return false 34 | if (!certificate.contentEquals(other.certificate)) return false 35 | 36 | return true 37 | } 38 | 39 | override fun hashCode(): Int { 40 | var result = kid.contentHashCode() 41 | result = 31 * result + certificate.contentHashCode() 42 | return result 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/valueset/ValueSet.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.valueset 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | /** 7 | * One specific value set, as raw JSON 8 | */ 9 | @Serializable 10 | data class ValueSet( 11 | 12 | @SerialName("n") 13 | val name: String, 14 | 15 | @SerialName("v") 16 | val valueSet: String 17 | 18 | ) -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/valueset/ValueSetContainer.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.valueset 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | 7 | /** 8 | * Contains a list of value sets, ee [ValueSet] 9 | */ 10 | @Serializable 11 | data class ValueSetContainer( 12 | 13 | @SerialName("v") 14 | val valueSets: Array 15 | 16 | ) { 17 | override fun equals(other: Any?): Boolean { 18 | if (this === other) return true 19 | if (other == null || this::class != other::class) return false 20 | 21 | other as ValueSetContainer 22 | 23 | if (!valueSets.contentEquals(other.valueSets)) return false 24 | 25 | return true 26 | } 27 | 28 | override fun hashCode(): Int { 29 | return valueSets.contentHashCode() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/valueset/ValueSetDecodeService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.valueset 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CertificateRepository 4 | import ehn.techiop.hcert.kotlin.chain.Error.TRUST_SERVICE_ERROR 5 | import ehn.techiop.hcert.kotlin.chain.VerificationException 6 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 7 | import ehn.techiop.hcert.kotlin.trust.SignedData 8 | import ehn.techiop.hcert.kotlin.trust.SignedDataDecodeService 9 | import ehn.techiop.hcert.kotlin.trust.SignedDataParsed 10 | import kotlinx.datetime.Clock 11 | import kotlinx.serialization.cbor.Cbor 12 | import kotlinx.serialization.decodeFromByteArray 13 | import kotlin.jvm.JvmOverloads 14 | 15 | /** 16 | * Decodes a [SignedData] blob, expected to contain the content and signature of a [ValueSetContainer] 17 | * 18 | * - [repository] contains the trust anchor for the parsed file 19 | * - [clock] defines the current time to use for validity checks 20 | * - [clockSkewSeconds] defines the error margin when comparing time validity of the parsed file in seconds 21 | */ 22 | class ValueSetDecodeService @JvmOverloads constructor( 23 | repository: CertificateRepository, 24 | clock: Clock = Clock.System, 25 | clockSkewSeconds: Int = 300 26 | ) { 27 | 28 | private val decodeService = SignedDataDecodeService(repository, clock, clockSkewSeconds) 29 | 30 | /** 31 | * See [SignedData] for details about the content 32 | * If all checks succeed, [input.content] is parsed as a [VauleSetContainer] 33 | */ 34 | @Throws(VerificationException::class) 35 | fun decode(input: SignedData): Pair { 36 | val parsed = decodeService.decode(input, listOf(CoseHeaderKeys.VALUE_SET_VERSION)) 37 | when (val version = parsed.headers[CoseHeaderKeys.VALUE_SET_VERSION]) { 38 | 1 -> return Pair(parsed, Cbor.decodeFromByteArray(parsed.content)) 39 | else -> throw VerificationException( 40 | TRUST_SERVICE_ERROR, "Version unknown", 41 | details = mapOf("valueSetVersion" to (version ?: "null").toString()) 42 | ) 43 | } 44 | } 45 | 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/ehn/techiop/hcert/kotlin/valueset/ValueSetV1EncodeService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.valueset 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoService 4 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 5 | import ehn.techiop.hcert.kotlin.trust.SignedData 6 | import ehn.techiop.hcert.kotlin.trust.SignedDataEncodeService 7 | import kotlinx.datetime.Clock 8 | import kotlinx.serialization.cbor.Cbor 9 | import kotlinx.serialization.encodeToByteArray 10 | import kotlin.jvm.JvmOverloads 11 | import kotlin.time.Duration 12 | import kotlin.time.Duration.Companion.hours 13 | 14 | 15 | /** 16 | * Encodes a list of value sets as [SignedData] object 17 | */ 18 | class ValueSetV1EncodeService constructor( 19 | signingService: CryptoService, 20 | validity: Duration = 48.hours, 21 | clock: Clock = Clock.System, 22 | ) { 23 | 24 | @JvmOverloads 25 | constructor(signingService: CryptoService, validityHours: Int = 48) 26 | : this(signingService, validityHours.hours, Clock.System) 27 | 28 | private val signedDataService = SignedDataEncodeService(signingService, validity, clock) 29 | 30 | /** 31 | * Content is a CBOR encoded [ValueSetContainer] object, i.e. a list of value sets 32 | */ 33 | private fun encodeContent(input: List): ByteArray { 34 | val content = ValueSetContainer(input.toTypedArray()) 35 | return Cbor.encodeToByteArray(content) 36 | } 37 | 38 | /** 39 | * See [SignedData] for details about returned structure 40 | */ 41 | fun encode(input: List): SignedData { 42 | val content = encodeContent(input) 43 | val headers = mapOf(CoseHeaderKeys.VALUE_SET_VERSION to 1) 44 | return signedDataService.wrapWithSignature(content, headers) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/commonMain/resources/value-sets/disease-agent-targeted.json: -------------------------------------------------------------------------------- 1 | { 2 | "valueSetId": "disease-agent-targeted", 3 | "valueSetDate": "2021-04-27", 4 | "valueSetValues": { 5 | "840539006": { 6 | "display": "COVID-19", 7 | "lang": "en", 8 | "active": true, 9 | "version": "http://snomed.info/sct/900000000000207008/version/20210131", 10 | "system": "http://snomed.info/sct" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commonMain/resources/value-sets/test-result.json: -------------------------------------------------------------------------------- 1 | { 2 | "valueSetId": "covid-19-lab-result", 3 | "valueSetDate": "2021-04-27", 4 | "valueSetValues": { 5 | "260415000": { 6 | "display": "Not detected", 7 | "lang": "en", 8 | "active": true, 9 | "version": "http://snomed.info/sct/900000000000207008/version/20210131", 10 | "system": "http://snomed.info/sct" 11 | }, 12 | "260373001": { 13 | "display": "Detected", 14 | "lang": "en", 15 | "active": true, 16 | "version": "http://snomed.info/sct/900000000000207008/version/20210131", 17 | "system": "http://snomed.info/sct" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commonMain/resources/value-sets/test-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "valueSetId": "covid-19-lab-test-type", 3 | "valueSetDate": "2021-04-27", 4 | "valueSetValues": { 5 | "LP6464-4": { 6 | "display": "Nucleic acid amplification with probe detection", 7 | "lang": "en", 8 | "active": true, 9 | "version": "2.69", 10 | "system": "http://loinc.org" 11 | }, 12 | "LP217198-3": { 13 | "display": "Rapid immunoassay", 14 | "lang": "en", 15 | "active": true, 16 | "version": "2.69", 17 | "system": "http://loinc.org" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commonMain/resources/value-sets/vaccine-prophylaxis.json: -------------------------------------------------------------------------------- 1 | { 2 | "valueSetId": "sct-vaccines-covid-19", 3 | "valueSetDate": "2021-04-27", 4 | "valueSetValues": { 5 | "1119349007": { 6 | "display": "SARS-CoV-2 mRNA vaccine", 7 | "lang": "en", 8 | "active": true, 9 | "version": "http://snomed.info/sct/900000000000207008/version/20210131", 10 | "system": "http://snomed.info/sct" 11 | }, 12 | "1119305005": { 13 | "display": "SARS-CoV-2 antigen vaccine", 14 | "lang": "en", 15 | "active": true, 16 | "version": "http://snomed.info/sct/900000000000207008/version/20210131", 17 | "system": "http://snomed.info/sct" 18 | }, 19 | "J07BX03": { 20 | "display": "covid-19 vaccines", 21 | "lang": "en", 22 | "active": true, 23 | "version": "http://snomed.info/sct/900000000000207008/version/20210131", 24 | "system": "http://snomed.info/sct" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/ehn/techiop/hcert/kotlin/000InitTestContext.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoServiceHolder 4 | import ehn.techiop.hcert.kotlin.crypto.KeyType 5 | import ehn.techiop.hcert.kotlin.log.BasicLogger 6 | import ehn.techiop.hcert.kotlin.log.setLogLevel 7 | import ehn.techiop.hcert.kotlin.trust.ContentType 8 | import io.github.aakira.napier.LogLevel 9 | import io.github.aakira.napier.Napier 10 | import io.kotest.core.spec.style.FunSpec 11 | import io.kotest.datatest.withData 12 | 13 | //RSA key generation can take ages in JS 14 | //This location and this naming ensures that this executes before any other tests (although the Kotest docs say otherwise) 15 | class A000InitTestContext : FunSpec({ 16 | 17 | test("Enabling logging") { 18 | setLogLevel(LogLevel.ERROR) 19 | 20 | //This is pretty awesome, as it supports an arbitrary number of arbitrary loggers 21 | //So we could create a custom test logger, which collects all log messages their parameters 22 | //This would allows us to write tests, which make sure that both JS and JVM target provide the same level of details (i.e. meaningful stack traces for exceptions, which are currently heavily platform-specific) 23 | Napier.base(BasicLogger()) 24 | Napier.i(message = "Logging enabled, you should see me!", tag = "Test Context") 25 | } 26 | 27 | withData(nameFn = { "prefill RandomCryptoService map for $it" }, ContentType.values().toList()) { ct -> 28 | Napier.i(tag = "start", message = "Generating EC 256 key for $ct") 29 | CryptoServiceHolder.getRandomCryptoService(KeyType.EC, 256, ct) 30 | Napier.i(tag = "start", message = "Generating EC 384 key for $ct") 31 | CryptoServiceHolder.getRandomCryptoService(KeyType.EC, 384, ct) 32 | 33 | Napier.i(tag = "start", message = "Generating RSA 2048 key for $ct") 34 | CryptoServiceHolder.getRandomCryptoService(KeyType.RSA, 2048, ct) 35 | Napier.i(tag = "start", message = "Generating RSA 3072 key for $ct") 36 | CryptoServiceHolder.getRandomCryptoService(KeyType.RSA, 3072, ct) 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/ehn/techiop/hcert/kotlin/chain/RsaCryptoServiceHolder.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import ehn.techiop.hcert.kotlin.chain.impl.RandomEcKeyCryptoService 4 | import ehn.techiop.hcert.kotlin.chain.impl.RandomRsaKeyCryptoService 5 | import ehn.techiop.hcert.kotlin.crypto.KeyType 6 | import ehn.techiop.hcert.kotlin.trust.ContentType 7 | 8 | internal object CryptoServiceHolder { 9 | 10 | data class MapKey(val keyType: KeyType, val keySize: Int, val contentType: ContentType?) 11 | 12 | val map = mutableMapOf() 13 | 14 | fun getRandomCryptoService(type: KeyType, keySize: Int, contentType: ContentType?): CryptoService { 15 | val mapKey = MapKey(type, keySize, contentType) 16 | val cryptoService = map[mapKey] 17 | if (cryptoService != null) { 18 | return cryptoService 19 | } 20 | val safeContentTypes = if (contentType != null) listOf(contentType) else ContentType.values().toList() 21 | val newService = when (type) { 22 | KeyType.EC -> RandomEcKeyCryptoService(keySize, safeContentTypes) 23 | else -> RandomRsaKeyCryptoService(keySize, safeContentTypes) 24 | } 25 | map[mapKey] = newService 26 | return newService 27 | } 28 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/ehn/techiop/hcert/kotlin/chain/ext/TestCase.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.ext 2 | 3 | import ehn.techiop.hcert.kotlin.data.GreenCertificate 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class TestCase( 9 | @SerialName("JSON") 10 | val eudgc: GreenCertificate? = null, 11 | 12 | @SerialName("CBOR") 13 | val cborHex: String? = null, 14 | 15 | @SerialName("COSE") 16 | val coseHex: String? = null, 17 | 18 | @SerialName("COMPRESSED") 19 | val compressedHex: String? = null, 20 | 21 | @SerialName("BASE45") 22 | val base45: String? = null, 23 | 24 | @SerialName("PREFIX") 25 | val base45WithPrefix: String? = null, 26 | 27 | @SerialName("2DCODE") 28 | val qrCodePng: String? = null, 29 | 30 | @SerialName("TESTCTX") 31 | val context: TestContext, 32 | 33 | @SerialName("EXPECTEDRESULTS") 34 | val expectedResult: TestExpectedResults, 35 | ) 36 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/ehn/techiop/hcert/kotlin/chain/ext/TestContext.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.ext 2 | 3 | 4 | import ehn.techiop.hcert.kotlin.data.LenientInstantParser 5 | import kotlinx.datetime.Instant 6 | import kotlinx.serialization.SerialName 7 | import kotlinx.serialization.Serializable 8 | 9 | @Serializable 10 | data class TestContext( 11 | @SerialName("VERSION") 12 | val version: Int, 13 | 14 | @SerialName("SCHEMA") 15 | val schema: String, 16 | 17 | @SerialName("CERTIFICATE") 18 | val certificate: String? = null, 19 | 20 | @SerialName("AT_CERTIFICATE") 21 | val atCertificate: String? = null, 22 | 23 | @SerialName("VALIDATIONCLOCK") 24 | @Serializable(with = LenientInstantParser::class) 25 | val validationClock: Instant, 26 | 27 | @SerialName("DESCRIPTION") 28 | val description: String? = null, 29 | ) 30 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/ehn/techiop/hcert/kotlin/chain/ext/TestExpectedResults.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.ext 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class TestExpectedResults( 8 | @SerialName("EXPECTEDVALIDOBJECT") 9 | val schemaGeneration: Boolean? = null, 10 | 11 | @SerialName("EXPECTEDSCHEMAVALIDATION") 12 | val schemaValidation: Boolean? = null, 13 | 14 | @SerialName("EXPECTEDENCODE") 15 | val encodeGeneration: Boolean? = null, 16 | 17 | @SerialName("EXPECTEDDECODE") 18 | val cborDecode: Boolean? = null, 19 | 20 | @SerialName("EXPECTEDVERIFY") 21 | val coseSignature: Boolean? = null, 22 | 23 | @SerialName("EXPECTEDUNPREFIX") 24 | val prefix: Boolean? = null, 25 | 26 | @SerialName("EXPECTEDVALIDJSON") 27 | val json: Boolean? = null, 28 | 29 | @SerialName("EXPECTEDCOMPRESSION") 30 | val compression: Boolean? = null, 31 | 32 | @SerialName("EXPECTEDB45DECODE") 33 | val base45Decode: Boolean? = null, 34 | 35 | @SerialName("EXPECTEDPICTUREDECODE") 36 | val qrDecode: Boolean? = null, 37 | 38 | @SerialName("EXPECTEDEXPIRATIONCHECK") 39 | val expirationCheck: Boolean? = null, 40 | 41 | @SerialName("EXPECTEDKEYUSAGE") 42 | val keyUsage: Boolean? = null, 43 | ) -------------------------------------------------------------------------------- /src/commonTest/kotlin/ehn/techiop/hcert/kotlin/chain/impl/Base45EncoderTest.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.fromHexString 4 | import ehn.techiop.hcert.kotlin.chain.toHexString 5 | import io.kotest.assertions.throwables.shouldThrow 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.datatest.withData 8 | import io.kotest.matchers.shouldBe 9 | 10 | 11 | class Base45EncoderTest : FunSpec({ 12 | 13 | withData( 14 | nameFn = { "Encode String \"${it.first}\" to \"${it.second}\"" }, 15 | Pair("AB", "BB8"), 16 | Pair("Hello!!", "%69 VD92EX0"), 17 | Pair("base-45", "UJCLQE7W581"), 18 | Pair("ietf!", "QED8WEX0") 19 | ) { (plain, encoded) -> 20 | val input = plain.encodeToByteArray() 21 | val implEncoded = Base45Encoder.encode(input) 22 | implEncoded shouldBe encoded 23 | val implDecoded = Base45Encoder.decode(implEncoded) 24 | implDecoded.asList() shouldBe input.asList() 25 | } 26 | 27 | withData( 28 | nameFn = { "Encode Bytes (hex) \"${it.first.toHexString()}\" to \"${it.second}\"" }, 29 | Pair("00".fromHexString(), "00"), 30 | Pair("07".fromHexString(), "70"), 31 | Pair("ff".fromHexString(), "U5"), 32 | Pair("000f".fromHexString(), "F00"), 33 | Pair("0000".fromHexString(), "000"), 34 | Pair("ffff".fromHexString(), "FGW"), 35 | Pair("000000".fromHexString(), "00000"), 36 | ) { (input, encoded) -> 37 | val implEncoded = Base45Encoder.encode(input) 38 | implEncoded shouldBe encoded 39 | val implDecoded = Base45Encoder.decode(implEncoded) 40 | implDecoded.asList() shouldBe input.asList() 41 | } 42 | 43 | withData( 44 | nameFn = { "Decode \"${it}\" is not valid" }, 45 | "V5", // first value with 2 chars too large, after "U5" 46 | "AA", // too large 47 | "GGW", // first value with 3 chars too large, after "FGW" 48 | "WWW", // too large 49 | "7", // one char strings are not allowed 50 | "0" // one char strings are not allowed 51 | ) { encoded -> 52 | shouldThrow { 53 | println(Base45Encoder.decode(encoded).toHexString()) 54 | } 55 | } 56 | 57 | }) -------------------------------------------------------------------------------- /src/commonTest/kotlin/ehn/techiop/hcert/kotlin/chain/impl/CompressorServiceTest.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.VerificationResult 4 | import ehn.techiop.hcert.kotlin.chain.impl.CompressionConstants.MAX_DECOMPRESSED_SIZE 5 | import ehn.techiop.hcert.kotlin.chain.toHexString 6 | import io.kotest.assertions.throwables.shouldThrowExactly 7 | import io.kotest.assertions.withClue 8 | import io.kotest.core.spec.style.FunSpec 9 | import io.kotest.core.spec.style.StringSpec 10 | import io.kotest.datatest.withData 11 | import io.kotest.matchers.shouldBe 12 | import kotlin.random.Random 13 | 14 | //Need to split this into two tests because "Spec styles that support nested tests are disabled in kotest-js because 15 | //the underlying JS frameworks do not support promises for outer root scopes". 16 | 17 | class CompressorServiceTest : StringSpec({ 18 | "ZLib random compress+deflate" { 19 | val compressorService = DefaultCompressorService() 20 | val input = Random.nextBytes(32).toHexString() 21 | val deflated = compressorService.encode(input.encodeToByteArray()) 22 | val encoded = Base45Encoder.encode(deflated) 23 | // transfer 24 | val decoded = Base45Encoder.decode(encoded) 25 | decoded.asList() shouldBe deflated.asList() 26 | val inflated = compressorService.decode(decoded, VerificationResult()) 27 | inflated.decodeToString() shouldBe input 28 | } 29 | }) 30 | 31 | class ChunkedCompressionTest : FunSpec({ 32 | val rnd = Random(1337) 33 | val compressor = CompressorAdapter() 34 | withData( 35 | mapOf( 36 | "small" to rnd.nextBytes(674), 37 | //these next three make little sense, but hey… 38 | "larger" to rnd.nextBytes(3072), 39 | "~double chunk size" to rnd.nextBytes(2 * 65536), 40 | "MAX_DECOMPRESSED_SIZE" to rnd.nextBytes(MAX_DECOMPRESSED_SIZE), 41 | "too large" to rnd.nextBytes(1 + MAX_DECOMPRESSED_SIZE), 42 | "100MiB zLib bomb" to ByteArray(20 * MAX_DECOMPRESSED_SIZE) 43 | ) 44 | ) { input -> 45 | val deflated = compressor.encode(input, 9) 46 | if (input.size > MAX_DECOMPRESSED_SIZE) { 47 | withClue("Exceeding MAX_DECOMPRESSED_SIZE") { 48 | shouldThrowExactly { 49 | compressor.decode(deflated) 50 | } 51 | } 52 | } else { 53 | val inflated = compressor.decode(deflated) 54 | inflated shouldBe input 55 | } 56 | } 57 | }) -------------------------------------------------------------------------------- /src/commonTest/kotlin/ehn/techiop/hcert/kotlin/chain/impl/RandomCryptoServiceTest.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoService 4 | import ehn.techiop.hcert.kotlin.chain.VerificationResult 5 | import io.kotest.core.spec.style.DescribeSpec 6 | import io.kotest.datatest.withData 7 | import io.kotest.matchers.shouldBe 8 | import io.kotest.matchers.shouldNotBe 9 | import kotlin.random.Random 10 | import kotlin.time.Duration.Companion.seconds 11 | 12 | class RandomCryptoServiceTest : DescribeSpec({ 13 | 14 | //RSA key generation can take ages in JS 15 | timeout = 50.seconds.inWholeMilliseconds 16 | 17 | withData(nameFn = { "EC$it" }, 256, 384) { keySize -> 18 | val service = RandomEcKeyCryptoService(keySize) 19 | 20 | assertEncodeDecode(service) 21 | } 22 | 23 | withData(nameFn = { "RSA$it" }, 2048, 3072) { keySize -> 24 | val service = RandomRsaKeyCryptoService(keySize) 25 | 26 | assertEncodeDecode(service) 27 | } 28 | 29 | }) 30 | 31 | 32 | private fun assertEncodeDecode(service: CryptoService) { 33 | service.exportPrivateKeyAsPem() shouldNotBe null 34 | service.exportCertificateAsPem() shouldNotBe null 35 | 36 | val plaintext = Random.nextBytes(32) 37 | val encoded = DefaultCoseService(service).encode(plaintext) 38 | encoded shouldNotBe null 39 | 40 | val verificationResult = VerificationResult() 41 | val repo = PrefilledCertificateRepository(service.exportCertificateAsPem()) 42 | val decoded = DefaultCoseService(repo).decode(encoded, verificationResult) 43 | decoded shouldBe plaintext 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/ehn/techiop/hcert/kotlin/data/ValueSetHolderTest.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class ValueSetHolderTest : StringSpec({ 7 | 8 | "Access by Key" { 9 | val found = ValueSetsInstanceHolder.INSTANCE.find("840539006") 10 | found.key shouldBe "840539006" 11 | found.valueSetEntry.display shouldBe "COVID-19" 12 | found.valueSetEntry.lang shouldBe "en" 13 | found.valueSetEntry.active shouldBe true 14 | found.valueSetEntry.version shouldBe "http://snomed.info/sct/900000000000207008/version/20210131" 15 | found.valueSetEntry.system shouldBe "http://snomed.info/sct" 16 | } 17 | 18 | "Access by Key with Whitespace" { 19 | val found = ValueSetsInstanceHolder.INSTANCE.find(" 840539006 ") 20 | found.key shouldBe "840539006" 21 | found.valueSetEntry.display shouldBe "COVID-19" 22 | found.valueSetEntry.lang shouldBe "en" 23 | found.valueSetEntry.active shouldBe true 24 | found.valueSetEntry.version shouldBe "http://snomed.info/sct/900000000000207008/version/20210131" 25 | found.valueSetEntry.system shouldBe "http://snomed.info/sct" 26 | } 27 | 28 | "Access by Category Key" { 29 | val found = ValueSetsInstanceHolder.INSTANCE.find("vaccines-covid-19-names", "EU/1/20/1528") 30 | found.key shouldBe "EU/1/20/1528" 31 | found.valueSetEntry.display shouldBe "Comirnaty" 32 | found.valueSetEntry.lang shouldBe "en" 33 | found.valueSetEntry.active shouldBe true 34 | found.valueSetEntry.version shouldBe "" 35 | found.valueSetEntry.system shouldBe "https://ec.europa.eu/health/documents/community-register/html/" 36 | } 37 | }) -------------------------------------------------------------------------------- /src/commonTest/kotlin/ehn/techiop/hcert/kotlin/valueset/VauleSetTest.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.valueset 2 | 3 | import ehn.techiop.hcert.kotlin.chain.asBase64 4 | import ehn.techiop.hcert.kotlin.chain.impl.PrefilledCertificateRepository 5 | import ehn.techiop.hcert.kotlin.chain.impl.RandomEcKeyCryptoService 6 | import ehn.techiop.hcert.kotlin.trust.SignedData 7 | import io.kotest.core.spec.style.StringSpec 8 | import io.kotest.matchers.longs.shouldBeGreaterThanOrEqual 9 | import io.kotest.matchers.longs.shouldBeLessThanOrEqual 10 | import io.kotest.matchers.shouldBe 11 | import kotlinx.datetime.Clock 12 | import kotlinx.serialization.json.Json 13 | 14 | class VauleSetTest : StringSpec({ 15 | 16 | "V1 Client-Server Exchange" { 17 | val cryptoService = RandomEcKeyCryptoService() 18 | val certificate = cryptoService.getCertificate().encoded.asBase64() 19 | val encodeService = ValueSetV1EncodeService(cryptoService) 20 | val valueSetJson = """ 21 | { 22 | "valueSetId": "disease-agent-targeted", 23 | "valueSetDate": "2021-04-27", 24 | "valueSetValues": { 25 | "840539006": { 26 | "display": "COVID-19", 27 | "lang": "en", 28 | "active": true, 29 | "version": "http://snomed.info/sct/900000000000207008/version/20210131", 30 | "system": "http://snomed.info/sct" 31 | } 32 | } 33 | } 34 | """.trimIndent() 35 | val signedData = encodeService.encode(listOf(ValueSet("disease-agent-targeted", valueSetJson))) 36 | 37 | verifyClientOperations(certificate, signedData, valueSetJson) 38 | } 39 | 40 | 41 | }) 42 | 43 | private fun verifyClientOperations( 44 | certificate: String, 45 | signedData: SignedData, 46 | valueSetJson: String 47 | ) { 48 | val clientTrustRoot = PrefilledCertificateRepository(certificate) 49 | val decodeService = ValueSetDecodeService(clientTrustRoot) 50 | val (clientSignedDataParsed, valueSets) = decodeService.decode(signedData) 51 | 52 | valueSets.valueSets.size shouldBe 1 53 | for (vs in valueSets.valueSets) { 54 | Json.parseToJsonElement(vs.valueSet) shouldBe Json.parseToJsonElement(valueSetJson) 55 | } 56 | clientSignedDataParsed.validFrom.epochSeconds shouldBeLessThanOrEqual Clock.System.now().epochSeconds 57 | clientSignedDataParsed.validUntil.epochSeconds shouldBeGreaterThanOrEqual Clock.System.now().epochSeconds 58 | clientSignedDataParsed.content shouldBe signedData.content 59 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/chain/Extensions.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import Buffer 4 | import org.khronos.webgl.ArrayBuffer 5 | import org.khronos.webgl.Uint8Array 6 | import org.khronos.webgl.Uint8ClampedArray 7 | import org.khronos.webgl.get 8 | 9 | actual fun ByteArray.asBase64() = Buffer.from(this.toUint8Array()).toString("base64") 10 | 11 | actual fun ByteArray.toHexString() = joinToString("") { ('0' + (it.toUByte()).toString(16)).takeLast(2) } 12 | 13 | actual fun String.fromBase64() = Buffer.from(this, "base64").toByteArray() 14 | 15 | fun ArrayBuffer.toByteArray() = org.khronos.webgl.Int8Array(this).unsafeCast() 16 | 17 | fun ArrayBuffer.Companion.from(array: ByteArray) = Buffer.from(array).buffer 18 | 19 | fun Buffer.toBase64UrlString() = this.toString("base64") 20 | .replace("+", "-") 21 | .replace("/", "_") 22 | .replace("=", "") 23 | 24 | actual fun String.fromHexString() = Buffer.from(this, "hex").toByteArray() 25 | 26 | fun ByteArray.toUint8Array(): Uint8Array { 27 | return Uint8Array(toTypedArray()) 28 | } 29 | 30 | fun ByteArray.toUint8ClampedArray(): Uint8ClampedArray { 31 | return Uint8ClampedArray(toTypedArray()) 32 | } 33 | 34 | fun ByteArray.toBuffer(): Buffer = Buffer.from(toUint8Array()) 35 | 36 | fun Uint8Array.toByteArray(): ByteArray { 37 | return ByteArray(this.length) { this[it] } 38 | } 39 | 40 | fun Buffer.Companion.from(arr: ByteArray): Buffer { 41 | return from(arr.toUint8Array()) 42 | } 43 | 44 | internal object NullableTryCatch { 45 | 46 | internal sealed class Holder { 47 | class Result(internal val result: T) : Holder() 48 | class Error(internal val throwable: Throwable) : Holder() 49 | } 50 | 51 | internal inline fun jsTry(tryBlock: () -> T) = try { 52 | Holder.Result(tryBlock()) 53 | } catch (e: dynamic) { 54 | val throwable = (if (e is Throwable) e else Throwable(JSON.stringify(e))) as Throwable 55 | Holder.Error(throwable) 56 | } 57 | 58 | internal inline fun Holder.catch(catchBlock: (t: Throwable) -> U) = when (this) { 59 | is Holder.Result -> this.result 60 | is Holder.Error -> catchBlock(this.throwable) 61 | } 62 | 63 | internal inline fun Holder.catch(block: (t: Throwable) -> Unit) { 64 | if (this is Holder.Error) block(this.throwable) 65 | } 66 | 67 | } 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/chain/VerificationResultJs.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import ehn.techiop.hcert.kotlin.data.JsDateSerializer 4 | import kotlinx.serialization.Serializable 5 | import kotlin.js.Date 6 | 7 | /** 8 | * This class contains "safe" objects for Javascript, 9 | * i.e. ones that can be serialized without name mangling. 10 | * One solution would be to annotate all exported classes with 11 | * [JsExport], but that won't work together with [Serializable]... 12 | */ 13 | @Serializable 14 | data class VerificationResultJs( 15 | @Serializable(with = JsDateSerializer::class) 16 | val expirationTime: Date? = null, 17 | @Serializable(with = JsDateSerializer::class) 18 | val issuedAt: Date? = null, 19 | val issuer: String? = null, 20 | @Serializable(with = JsDateSerializer::class) 21 | val certificateValidFrom: Date? = null, 22 | @Serializable(with = JsDateSerializer::class) 23 | val certificateValidUntil: Date? = null, 24 | val certificateValidContent: Array? = null, 25 | val certificateSubjectCountry: String? = null, 26 | val content: Array? = null, 27 | val error: String? = null 28 | ) { 29 | constructor(result: VerificationResult) : this( 30 | result.expirationTime?.let { Date(it.toString()) }, 31 | result.issuedAt?.let { Date(it.toString()) }, 32 | result.issuer, 33 | result.certificateValidFrom?.let { Date(it.toString()) }, 34 | result.certificateValidUntil?.let { Date(it.toString()) }, 35 | result.certificateValidContent.map { it.name }.toTypedArray(), 36 | result.certificateSubjectCountry, 37 | result.content.map { it.name }.toTypedArray(), 38 | result.error?.name 39 | ) 40 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/CompressorAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import Pako.DeflateFunctionOptions 4 | import Pako.Inflate 5 | import Pako.deflate 6 | import ehn.techiop.hcert.kotlin.chain.NullableTryCatch.catch 7 | import ehn.techiop.hcert.kotlin.chain.NullableTryCatch.jsTry 8 | import ehn.techiop.hcert.kotlin.chain.impl.CompressionConstants.MAX_DECOMPRESSED_SIZE 9 | import ehn.techiop.hcert.kotlin.chain.toByteArray 10 | import ehn.techiop.hcert.kotlin.chain.toUint8Array 11 | import ehn.techiop.hcert.kotlin.log.formatMag 12 | import io.github.aakira.napier.Napier 13 | import org.khronos.webgl.Uint8Array 14 | 15 | actual class CompressorAdapter { 16 | 17 | val tag = "ZLib${hashCode()}" 18 | 19 | actual fun encode(input: ByteArray, level: Int): ByteArray { 20 | Napier.v("Deflating ${input.size.formatMag()}B input using compression level $level", tag = tag) 21 | val compressed = (deflate(input.toUint8Array(), 22 | object : DeflateFunctionOptions { 23 | override var level = level 24 | }) as Uint8Array).toByteArray() 25 | Napier.v("... was deflated to ${compressed.size.formatMag()}B", tag = tag) 26 | return compressed 27 | } 28 | 29 | actual fun decode(input: ByteArray): ByteArray { 30 | Napier.v("Inflating ${input.size.formatMag()}B input", tag = tag) 31 | return jsTry { 32 | // It's fine to push the whole data in at once, since Pako will split it into 65536 byte chunks internally 33 | val inflator = SafeInflate(input, tag).also { it.push(input.toUint8Array()) } 34 | (inflator.result as Uint8Array).toByteArray() 35 | }.catch { throw it } 36 | } 37 | 38 | } 39 | 40 | private class SafeInflate(private val input: ByteArray, private val tag: String) : Inflate() { 41 | var numDecompressedBytes = 0 42 | override fun onData(chunk: Uint8Array) { 43 | //one too many is not an issue 44 | super.onData(chunk) 45 | numDecompressedBytes += chunk.length 46 | if (numDecompressedBytes > MAX_DECOMPRESSED_SIZE) { 47 | Napier.v("Refusing to decompress and additional ${chunk.length.formatMag()}B from input", tag = tag) 48 | throw IllegalArgumentException("Decompression exceeded $MAX_DECOMPRESSED_SIZE bytes, is: $numDecompressedBytes! Input must be invalid.") 49 | } 50 | Napier.v("Decompressed ${numDecompressedBytes.formatMag()}B from ${input.size.formatMag()}B input", tag = tag) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/DefaultTwoDimCodeService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.TwoDimCodeService 4 | import ehn.techiop.hcert.kotlin.chain.fromBase64 5 | import ehn.techiop.hcert.kotlin.chain.toUint8ClampedArray 6 | import qrcode.Decoder 7 | import qrcode.Encoder 8 | import qrcode.ErrorCorrectionLevel 9 | import kotlin.math.roundToInt 10 | import kotlin.math.sqrt 11 | 12 | /** 13 | * Encodes the input as an 2D QR code. 14 | */ 15 | class DefaultTwoDimCodeService( 16 | private val moduleSize: Int = 3, 17 | private val marginSize: Int = 2 18 | ) : TwoDimCodeService { 19 | 20 | /** 21 | * Generates a 2D code, returns the image itself as an encoded *gif* 22 | */ 23 | override fun encode(data: String): ByteArray { 24 | val encoder = Encoder() 25 | encoder.setErrorCorrectionLevel(ErrorCorrectionLevel.Q) 26 | encoder.write(data) 27 | encoder.make() 28 | val dataUrl = encoder.toDataURL(moduleSize = moduleSize, margin = marginSize) 29 | return dataUrl.replace("data:image/gif;base64,", "").fromBase64() 30 | } 31 | 32 | /** 33 | * Decodes the content of an image of a 2D code. 34 | * 35 | * Note: This method can not decode the pictures generated with [encode]. 36 | * The JS library seems to expect the rgb-encoded data of an image ... 37 | */ 38 | override fun decode(input: ByteArray): String { 39 | val decoder = Decoder() 40 | // calculate width and height as expected by JS library 41 | val data = input.toUint8ClampedArray() 42 | val pixelCount = data.length / 4 43 | val width = sqrt(pixelCount.toFloat()).roundToInt() 44 | val result = decoder.decode(data, width, width) 45 | return result.data 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/crypto/Cose.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | import Buffer 4 | import cose.Signer 5 | import cose.Verifier 6 | import cose.common 7 | import cose.sign 8 | import ehn.techiop.hcert.kotlin.chain.toByteArray 9 | import ehn.techiop.hcert.kotlin.chain.toUint8Array 10 | 11 | 12 | internal object Cose { 13 | 14 | init { 15 | common.HeaderParameters["tlv"]=42 16 | common.HeaderParameters["brv"]=-65537 17 | common.HeaderParameters["vsv"]=-65538 18 | } 19 | 20 | fun verifySync(signedBitString: ByteArray, pubKey: PubKey): ByteArray { 21 | val key = (pubKey as JsPubKey).toCoseRepresentation() 22 | val verifier = object : Verifier { 23 | override val key = key 24 | } 25 | return sign.verifySync(Buffer.from(signedBitString.toUint8Array()), verifier).toByteArray() 26 | } 27 | 28 | fun sign(header: dynamic, input: ByteArray, privKey: PrivKey): Buffer { 29 | val key = (privKey as JsPrivKey).toCoseRepresentation() 30 | val signer = object : Signer { 31 | override val key = key 32 | } 33 | return sign.createSync(header, Buffer(input.toUint8Array()), signer) 34 | } 35 | 36 | } 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/crypto/JsEcPrivKey.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | import Buffer 4 | import cose.EcCosePrivateKey 5 | import ehn.techiop.hcert.kotlin.chain.toBase64UrlString 6 | import elliptic.EC 7 | import elliptic.EcKeyPair 8 | import tsstdlib.JsonWebKey 9 | 10 | class JsEcPrivKey(val dValue: Buffer, val ec: EC, private val keySizeBits: Int) : JsPrivKey { 11 | 12 | constructor(keyPair: EcKeyPair, keySizeBits: Int) : this( 13 | keyPair.getPrivate().toArrayLike(Buffer), 14 | keyPair.ec, 15 | keySizeBits 16 | ) 17 | 18 | override fun toCoseRepresentation() = object : EcCosePrivateKey { 19 | override val d: Buffer = dValue 20 | } 21 | 22 | fun toPlatformPrivateKey() = object : JsonWebKey { 23 | override var kty: String? = "EC" 24 | override var crv: String? = if (keySizeBits == 384) "P-384" else "P-256" 25 | override var d: String? = dValue.toBase64UrlString() 26 | } 27 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/crypto/JsEcPubKey.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | import Buffer 4 | import cose.EcCosePublicKey 5 | import ehn.techiop.hcert.kotlin.chain.toBase64UrlString 6 | import elliptic.EcKeyPair 7 | import elliptic.EcPublicKey 8 | import org.khronos.webgl.Uint8Array 9 | import tsstdlib.JsonWebKey 10 | 11 | class JsEcPubKey(private val xCoord: Buffer, private val yCoord: Buffer, private val keySizeBits: Int) : 12 | JsPubKey { 13 | 14 | constructor(ecPublicKey: EcPublicKey, keySize: Int) : this( 15 | ecPublicKey.getX().toArrayLike(Buffer), 16 | ecPublicKey.getY().toArrayLike(Buffer), 17 | keySize 18 | ) 19 | 20 | constructor(ecKeyPair: EcKeyPair, keySize: Int) : this(ecKeyPair.getPublic(), keySize) 21 | 22 | constructor(x: Uint8Array, y: Uint8Array) : this( 23 | xCoord = Buffer.from(x), 24 | yCoord = Buffer.from(y), 25 | if (x.length == 48) 384 else 256 26 | ) 27 | 28 | override fun toCoseRepresentation(): EcCosePublicKey { 29 | return object : EcCosePublicKey { 30 | override val x = xCoord 31 | override val y = yCoord 32 | } 33 | } 34 | 35 | override fun toPlatformPublicKey() = object : JsonWebKey { 36 | override var alg: String? = "EC" 37 | override var crv: String? = if (keySizeBits == 384) "P-384" else "P-256" 38 | override var kty: String? = "EC" 39 | override var x: String? = xCoord.toBase64UrlString() 40 | override var y: String? = yCoord.toBase64UrlString() 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/crypto/JsRsaPrivKey.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | import Buffer 4 | import cose.RsaCosePrivateKey 5 | import ehn.techiop.hcert.kotlin.chain.toBase64UrlString 6 | import ehn.techiop.hcert.kotlin.chain.toBuffer 7 | import ehn.techiop.hcert.kotlin.chain.toByteArray 8 | import ehn.techiop.hcert.kotlin.chain.toHexString 9 | import org.khronos.webgl.Int32Array 10 | import pkijs.src.RSAPrivateKey.RSAPrivateKey 11 | import tsstdlib.JsonWebKey 12 | import kotlin.js.Json 13 | import kotlin.js.json 14 | 15 | class JsRsaPrivKey(val raw: Json) : JsPrivKey { 16 | 17 | constructor(privateKey: RSAPrivateKey) : this( 18 | json( 19 | "n" to privateKey.modulus.valueBlock.valueHex.toByteArray().toBuffer(), 20 | "e" to privateKey.publicExponent.valueBlock.valueHex.toByteArray().toHexString().toInt(16) as Number, 21 | "d" to privateKey.privateExponent.valueBlock.valueHex.toByteArray().toBuffer(), 22 | "p" to privateKey.prime1.valueBlock.valueHex.toByteArray().toBuffer(), 23 | "q" to privateKey.prime2.valueBlock.valueHex.toByteArray().toBuffer(), 24 | "dmp1" to privateKey.exponent1.valueBlock.valueHex.toByteArray().toBuffer(), 25 | "dmq1" to privateKey.exponent2.valueBlock.valueHex.toByteArray().toBuffer(), 26 | "coeff" to privateKey.coefficient.valueBlock.valueHex.toByteArray().toBuffer(), 27 | ) 28 | ) 29 | 30 | override fun toCoseRepresentation() = object : RsaCosePrivateKey { 31 | override val p: Buffer = raw["p"] as Buffer 32 | override val q: Buffer = raw["q"] as Buffer 33 | override val dp: Buffer = raw["dmp1"] as Buffer 34 | override val dq: Buffer = raw["dmq1"] as Buffer 35 | override val qi: Buffer = raw["coeff"] as Buffer 36 | override val d: Buffer = raw["d"] as Buffer 37 | override val n: Buffer = raw["n"] as Buffer 38 | override val e: Number = raw["e"] as Number 39 | } 40 | 41 | fun toPlatformPrivateKey() = object : JsonWebKey { 42 | override var alg: String? = "PS256" 43 | override var kty: String? = "RSA" 44 | override var p: String? = (raw["p"] as Buffer).toBase64UrlString() 45 | override var q: String? = (raw["q"] as Buffer).toBase64UrlString() 46 | override var dp: String? = (raw["dmp1"] as Buffer).toBase64UrlString() 47 | override var dq: String? = (raw["dmq1"] as Buffer).toBase64UrlString() 48 | override var qi: String? = (raw["coeff"] as Buffer).toBase64UrlString() 49 | override var d: String? = (raw["d"] as Buffer).toBase64UrlString() 50 | override var n: String? = (raw["n"] as Buffer).toBase64UrlString() 51 | override var e: String? = Buffer(Int32Array(arrayOf((raw["e"] as Number).toInt())).buffer).toBase64UrlString() 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/crypto/JsRsaPubKey.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | import Buffer 4 | import cose.RsaCosePublicKey 5 | import ehn.techiop.hcert.kotlin.chain.from 6 | import ehn.techiop.hcert.kotlin.chain.toBase64UrlString 7 | import ehn.techiop.hcert.kotlin.chain.toByteArray 8 | import org.khronos.webgl.ArrayBuffer 9 | import org.khronos.webgl.Int32Array 10 | import org.khronos.webgl.Uint8Array 11 | import tsstdlib.JsonWebKey 12 | import kotlin.js.Json 13 | 14 | class JsRsaPubKey(val modulus: ArrayBuffer, val publicExponent: Number) : 15 | JsPubKey { 16 | 17 | constructor(publicKey: Json) : this( 18 | ArrayBuffer.from((publicKey["n"] as Buffer).toByteArray()), 19 | publicKey["e"] as Number 20 | ) 21 | 22 | override fun toCoseRepresentation(): RsaCosePublicKey = object : RsaCosePublicKey { 23 | override val n = Buffer.from(Uint8Array(modulus)) 24 | override val e = publicExponent 25 | } 26 | 27 | override fun toPlatformPublicKey() = object : JsonWebKey { 28 | override var alg: String? = "RS256" 29 | override var kty: String? = "RSA" 30 | override var n: String? = stripLeadingZero(Buffer.from(Uint8Array(modulus))).toBase64UrlString() 31 | override var e: String? = Buffer(Int32Array(arrayOf(publicExponent.toInt())).buffer).toBase64UrlString() 32 | } 33 | 34 | // We'll need to strip the leading zero from the Buffer 35 | // because ASN.1 will add it's own leading zero, if needed 36 | private fun stripLeadingZero(n: Buffer): Buffer { 37 | return if (n.readUInt8(0) == 0) n.slice(1) else n 38 | } 39 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/data/JsDateSerializer.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.PrimitiveKind 5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 6 | import kotlinx.serialization.descriptors.SerialDescriptor 7 | import kotlinx.serialization.encoding.Decoder 8 | import kotlinx.serialization.encoding.Encoder 9 | import kotlin.js.Date 10 | 11 | 12 | object JsDateSerializer : KSerializer { 13 | 14 | override val descriptor: SerialDescriptor = 15 | PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING) 16 | 17 | override fun deserialize(decoder: Decoder): Date { 18 | val value = decoder.decodeString() 19 | return Date(dateString = value) 20 | } 21 | 22 | override fun serialize(encoder: Encoder, value: Date) { 23 | encoder.encodeString(value.toISOString()) 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/data/ValueSetsInstanceHolder.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import MainResourceHolder 4 | import R 5 | import ehn.techiop.hcert.kotlin.chain.fromBase64 6 | import kotlinx.serialization.decodeFromString 7 | import kotlinx.serialization.json.Json 8 | 9 | fun R.load(input: String): ByteArray? { 10 | val value = get(if (input.startsWith("/")) input.substring(1); else input) 11 | return value?.fromBase64() 12 | } 13 | 14 | fun R.loadAsString(input: String) = load(input)?.decodeToString() 15 | 16 | actual object ValueSetsInstanceHolder { 17 | actual val INSTANCE: ValueSetHolder = run { 18 | val values = mutableListOf() 19 | inputPaths.forEach { input -> 20 | MainResourceHolder.loadAsString(input)?.let { values.add(Json.decodeFromString(it)) } 21 | } 22 | ValueSetHolder(values) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/log/BasicLogger.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.log 2 | 3 | import io.github.aakira.napier.Antilog 4 | import io.github.aakira.napier.LogLevel 5 | import kotlinx.datetime.Clock 6 | 7 | //was once based on default JS debug Antilog 8 | 9 | actual open class BasicLogger actual constructor(protected val defaultTag: String?) : Antilog() { 10 | 11 | override fun performLog(priority: LogLevel, tag: String?, throwable: Throwable?, message: String?) { 12 | if (tag != null && defaultTag != null && tag != defaultTag) 13 | return 14 | 15 | val fullMessage = if (message != null) { 16 | if (throwable != null) "$message\n${throwable.stackTraceToString()}" else message 17 | } else throwable?.message ?: return 18 | 19 | globalLogLevel?.let { setLevel -> 20 | if (setLevel.ordinal <= priority.ordinal) 21 | log(priority, "${setupTag(priority, tag)} $fullMessage\n") 22 | } 23 | } 24 | 25 | private fun setupTag(priority: LogLevel, tag: String?): String { 26 | val logTag = tag ?: defaultTag ?: "" 27 | return Clock.System.now().toString() + if (logTag.isEmpty()) " $priority:" else " $priority $logTag:" 28 | } 29 | 30 | private fun log(priority: LogLevel, msg: String) { 31 | when (priority) { 32 | LogLevel.VERBOSE, LogLevel.DEBUG -> console.log(msg) 33 | LogLevel.INFO -> console.info(msg) 34 | LogLevel.WARNING -> console.warn(msg) 35 | LogLevel.ERROR, LogLevel.ASSERT -> console.error(msg) 36 | } 37 | } 38 | } 39 | 40 | @JsExport 41 | @Suppress("NON_EXPORTABLE_TYPE") 42 | class JsLogger(private val loggingFunction: (level: String, tag: String?, stackTrace: String?, message: String?) -> Unit) : 43 | Antilog() { 44 | override fun performLog(priority: LogLevel, tag: String?, throwable: Throwable?, message: String?) { 45 | if (globalLogLevel != null) 46 | loggingFunction(priority.name, tag, throwable?.stackTraceToString(), message) 47 | } 48 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/trust/CoseCreationAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import ehn.techiop.hcert.kotlin.chain.toByteArray 4 | import ehn.techiop.hcert.kotlin.crypto.Cose 5 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 6 | import ehn.techiop.hcert.kotlin.crypto.CwtAlgorithm 7 | import ehn.techiop.hcert.kotlin.crypto.PrivKey 8 | import kotlin.js.json 9 | 10 | actual class CoseCreationAdapter actual constructor(private val content: ByteArray) { 11 | 12 | private val protectedHeader = json() 13 | private val unprotectedHeader = json() 14 | private var encoded: ByteArray = byteArrayOf() 15 | 16 | actual fun addProtectedAttribute(key: CoseHeaderKeys, value: Any) { 17 | val content = if (value is CwtAlgorithm) value.stringVal else value 18 | protectedHeader.set(key.stringVal, content) 19 | } 20 | 21 | actual fun addUnprotectedAttribute(key: CoseHeaderKeys, value: Any) { 22 | val content = if (value is CwtAlgorithm) value.stringVal else value 23 | unprotectedHeader.set(key.stringVal, content) 24 | } 25 | 26 | actual fun sign(key: PrivKey) { 27 | val header = json("p" to protectedHeader, "u" to unprotectedHeader) 28 | encoded = Cose.sign(header, content, key).toByteArray() 29 | } 30 | 31 | actual fun encode(): ByteArray { 32 | return encoded 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/trust/CwtAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import Cbor.Wrappers.decodeFirst 4 | import ehn.techiop.hcert.kotlin.chain.NullableTryCatch.catch 5 | import ehn.techiop.hcert.kotlin.chain.NullableTryCatch.jsTry 6 | import ehn.techiop.hcert.kotlin.chain.toByteArray 7 | import ehn.techiop.hcert.kotlin.data.CborObject 8 | import org.khronos.webgl.Uint8Array 9 | 10 | actual object CwtHelper { 11 | actual fun fromCbor(input: ByteArray): CwtAdapter = 12 | JsCwtAdapter(decodeFirst(input)) 13 | } 14 | 15 | @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") 16 | class JsCwtAdapter(private val map: dynamic) : CwtAdapter { 17 | 18 | override fun getByteArray(key: Int): ByteArray? = 19 | jsTry { (map.get(key) as Uint8Array?)?.toByteArray() }.catch { null } 20 | 21 | override fun getString(key: Int): String? = 22 | jsTry { map.get(key) as String? }.catch { null } 23 | 24 | override fun getNumber(key: Int): Number? = 25 | jsTry { map.get(key) as Number? }.catch { null } 26 | 27 | override fun getMap(key: Int): CwtAdapter? = 28 | jsTry { 29 | val value = map?.get(key) 30 | if (value == null || value == undefined) return null 31 | JsCwtAdapter(value) 32 | }.catch { 33 | null 34 | } 35 | 36 | 37 | //This seems gruesome, but works on JS since the Interface does not declare any members 38 | override fun toCborObject(): CborObject = JsCborObject(map) 39 | class JsCborObject(internal val internalRepresentation: dynamic) : CborObject { 40 | override fun toJsonString() = JSON.stringify(internalRepresentation) 41 | 42 | //if not present in object structure, this is technically a schema issue and we therefore do not handle it here 43 | override fun getVersionString(): String? = jsTry { internalRepresentation["ver"] as String? }.catch { null } 44 | } 45 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/trust/CwtCreationAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import Cbor.Encoder 4 | import Cbor.Wrappers 5 | import Cbor.Wrappers.decodeFirst 6 | import ehn.techiop.hcert.kotlin.chain.toByteArray 7 | import ehn.techiop.hcert.kotlin.chain.toUint8Array 8 | 9 | actual class CwtCreationAdapter actual constructor() { 10 | 11 | private val map = Wrappers.map() 12 | 13 | actual fun add(key: Int, value: Any) { 14 | when (value) { 15 | is ByteArray -> map.set(key, value.toUint8Array()) 16 | is Long -> map.set(key, value.toInt()) 17 | else -> map.set(key, value) 18 | } 19 | } 20 | 21 | actual fun addDgc(key: Int, innerKey: Int, input: ByteArray) { 22 | val innerMap = Wrappers.map() 23 | val value = decodeFirst(input) 24 | innerMap.set(innerKey, value) 25 | map.set(key, innerMap) 26 | } 27 | 28 | actual fun encode(): ByteArray { 29 | return Encoder.encode(map).toByteArray() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/ehn/techiop/hcert/kotlin/trust/Hash.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import ehn.techiop.hcert.kotlin.chain.toByteArray 4 | import ehn.techiop.hcert.kotlin.chain.toUint8Array 5 | import Hash as JsHash 6 | 7 | actual class Hash actual constructor(private val input: ByteArray) { 8 | actual fun calc(): ByteArray { 9 | val hash = JsHash() 10 | hash.update(input.toUint8Array()) 11 | return hash.digest().toByteArray() 12 | } 13 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/Wrappers.kt: -------------------------------------------------------------------------------- 1 | package Cbor 2 | 3 | import Buffer 4 | import ehn.techiop.hcert.kotlin.chain.toBuffer 5 | 6 | 7 | object Wrappers { 8 | @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") 9 | fun map() = Map(js("([])") as tsstdlib.Iterable) 10 | 11 | //it would be nice to do away with this, but the generated externals are somehow messed up 12 | @Suppress("UnsafeCastFromDynamic", "UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") 13 | fun dateDecoderOptions(): DecoderOptions = js("({tags:{0:function(x){return x}}})") 14 | 15 | //decodeFirstSync will cause issues in some edge cases, so we simply delegate to decodeAllSyncq 16 | fun decodeFirst(input: ByteArray): dynamic = decodeFirst(input.toBuffer()) 17 | fun decodeFirst(input: Buffer): dynamic = decodeAll(input)[0] 18 | 19 | fun decodeAll(input: ByteArray) = decodeAll(input.toBuffer()) 20 | fun decodeAll(input: Buffer) = Decoder.decodeAllSync(input, options = dateDecoderOptions()) 21 | } 22 | -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/ajv.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | 8 | @JsNonModule 9 | @JsModule("ajv") 10 | open external class AJV { 11 | fun validate(schema: dynamic, data: dynamic): Boolean 12 | fun validateSchema(schema: dynamic): Boolean 13 | fun addKeyword(keyword: String) 14 | 15 | val errors: dynamic 16 | } 17 | 18 | @JsNonModule 19 | @JsModule("ajv/dist/2019") 20 | external class AJV2019 : AJV 21 | 22 | @JsNonModule 23 | @JsModule("ajv/dist/2020") 24 | external class AJV2020 : AJV 25 | 26 | @JsNonModule 27 | @JsModule("ajv-formats") 28 | external fun addFormats(ajv: AJV) -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/cose-js.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("cose-js") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package cose 11 | 12 | import Buffer 13 | import kotlin.js.Json 14 | 15 | external interface CosePublicKey 16 | 17 | external interface EcCosePublicKey : CosePublicKey { 18 | val x: Buffer 19 | val y: Buffer 20 | } 21 | 22 | external interface RsaCosePublicKey : CosePublicKey { 23 | val n: Buffer 24 | val e: Number 25 | } 26 | 27 | external interface RsaCosePrivateKey : CosePrivateKey, RsaCosePublicKey { 28 | val p: Buffer 29 | val q: Buffer 30 | val dp: Buffer 31 | val dq: Buffer 32 | val qi: Buffer 33 | } 34 | 35 | external interface CosePrivateKey { 36 | val d: Buffer 37 | } 38 | 39 | external interface EcCosePrivateKey : CosePrivateKey 40 | 41 | external interface Verifier { 42 | val key: CosePublicKey 43 | } 44 | 45 | external interface AlgorithmHeader { 46 | val alg: String 47 | } 48 | 49 | external interface KidHeader { 50 | val kid: String 51 | } 52 | 53 | external interface Headers { 54 | val p: AlgorithmHeader 55 | val u: KidHeader 56 | } 57 | 58 | external interface Signer { 59 | val key: CosePrivateKey 60 | } 61 | 62 | open external class common { 63 | companion object { 64 | val HeaderParameters: Json 65 | } 66 | } 67 | 68 | open external class sign { 69 | companion object { 70 | fun verifySync(message: Buffer, verifier: Verifier): Buffer 71 | fun createSync(headers: dynamic, data: Buffer, signer: Signer): Buffer 72 | } 73 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/crypto/elliptic.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("elliptic") 2 | @file:JsNonModule 3 | 4 | package elliptic 5 | 6 | import BN 7 | import Buffer 8 | import org.khronos.webgl.Uint8Array 9 | 10 | external class Signature { 11 | fun toDER(): Array 12 | } 13 | 14 | external class EcPublicKey { 15 | fun getX(): BN 16 | fun getY(): BN 17 | } 18 | 19 | external class EcKeyPair { 20 | fun getPublic(): EcPublicKey 21 | fun getPrivate(): BN 22 | val ec: EC 23 | } 24 | 25 | @JsName("ec") 26 | external class EC(curve: String) { 27 | fun genKeyPair(): EcKeyPair 28 | 29 | fun keyFromPrivate(buffer: Buffer): EcKeyPair 30 | 31 | fun sign(msg: Uint8Array, key: BN): Signature 32 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/crypto/index.curves.module_elliptic.kt: -------------------------------------------------------------------------------- 1 | @file:JsQualifier("curves") 2 | @file:Suppress( 3 | "INTERFACE_WITH_SUPERCLASS", 4 | "OVERRIDING_FINAL_MEMBER", 5 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 6 | "CONFLICTING_OVERLOADS" 7 | ) 8 | 9 | package curves 10 | 11 | open external class PresetCurve(options: Options) { 12 | open var type: String 13 | open var g: Any 14 | open var n: Any? 15 | open var hash: Any 16 | 17 | interface Options { 18 | var type: String 19 | var prime: String? 20 | var p: String 21 | var a: String 22 | var b: String 23 | var n: String 24 | var hash: Any 25 | var gRed: Boolean 26 | var g: Any 27 | var beta: String? 28 | get() = definedExternally 29 | set(value) = definedExternally 30 | var lambda: String? 31 | get() = definedExternally 32 | set(value) = definedExternally 33 | var basis: Any? 34 | get() = definedExternally 35 | set(value) = definedExternally 36 | } 37 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.PvUtils.module_pvutils.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pvutils") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package PvUtils 11 | 12 | import org.khronos.webgl.ArrayBuffer 13 | import org.khronos.webgl.ArrayBufferView 14 | import org.khronos.webgl.Uint8Array 15 | import kotlin.js.Date 16 | 17 | external fun getUTCDate(date: Date): Date 18 | 19 | external fun getParametersValue(parameters: Any, name: String, defaultValue: T = definedExternally): T 20 | 21 | external fun bufferToHexCodes( 22 | inputBuffer: ArrayBuffer, 23 | inputOffset: Number = definedExternally, 24 | inputLength: Number = definedExternally 25 | ): String 26 | 27 | external fun checkBufferParams( 28 | baseBlock: Any, 29 | inputBuffer: ArrayBuffer, 30 | inputOffset: Number, 31 | inputLength: Number 32 | ): Boolean 33 | 34 | external fun utilFromBase(inputBuffer: Uint8Array, inputBase: Number): Number 35 | 36 | external fun utilToBase(value: Number, base: Number, reserved: Number = definedExternally): ArrayBuffer 37 | 38 | external fun utilConcatBuf(vararg buf: ArrayBuffer): ArrayBuffer 39 | 40 | external fun utilDecodeTC(): Number 41 | 42 | external fun utilEncodeTC(value: Number): ArrayBuffer 43 | 44 | external fun isEqualBuffer(inputBuffer1: ArrayBuffer, inputBuffer2: ArrayBuffer): Boolean 45 | 46 | external fun padNumber(inputNumber: Number, fullLength: Number): String 47 | 48 | external fun toBase64( 49 | input: String, 50 | useUrlTemplate: Boolean = definedExternally, 51 | skipPadding: Boolean = definedExternally 52 | ): String 53 | 54 | external fun fromBase64( 55 | input: String, 56 | useUrlTemplate: Boolean = definedExternally, 57 | cutTailZeros: Boolean = definedExternally 58 | ): String 59 | 60 | external fun arrayBufferToString(buffer: ArrayBuffer): String 61 | 62 | external fun arrayBufferToString(buffer: ArrayBufferView): String 63 | 64 | external fun stringToArrayBuffer(str: String): ArrayBuffer 65 | 66 | external fun nearestPowerOf2(length: Number): Number -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "INTERFACE_WITH_SUPERCLASS", 3 | "OVERRIDING_FINAL_MEMBER", 4 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 5 | "CONFLICTING_OVERLOADS" 6 | ) 7 | 8 | external interface JsonOtherPrimeInfo { 9 | var r: String 10 | var d: String 11 | var t: String 12 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.AlgorithmIdentifier.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/AlgorithmIdentifier") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.AlgorithmIdentifier 11 | 12 | @JsName("default") 13 | open external class AlgorithmIdentifier(params: Any = definedExternally) { 14 | open var algorithmId: String 15 | open var algorithmParams: Any 16 | open fun isEqual(algorithmIdentifier: AlgorithmIdentifier): Boolean 17 | open fun fromSchema(schema: Any) 18 | open fun toSchema(): Any 19 | open fun toJSON(): Any 20 | 21 | companion object { 22 | fun compareWithDefault(memberName: String, memberValue: Any): Boolean 23 | fun defaultValues(memberName: String): Any 24 | fun schema(parameters: Any = definedExternally): Any 25 | } 26 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.AltName.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/AltName") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.AltName 11 | 12 | import pkijs.src.GeneralName.GeneralName 13 | 14 | @JsName("default") 15 | open external class AltName(params: Any = definedExternally) { 16 | open var altNames: Array 17 | open fun fromSchema(schema: Any) 18 | open fun toSchema(): Any 19 | open fun toJSON(): Any 20 | 21 | companion object { 22 | fun defaultValues(memberName: String): Any 23 | fun schema(parameters: Any = definedExternally): Any 24 | } 25 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.Attribute.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/Attribute") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.Attribute 11 | 12 | @JsName("default") 13 | open external class Attribute(params: Any = definedExternally) { 14 | open var type: String 15 | open var value: Array 16 | open fun fromSchema(schema: Any) 17 | open fun toSchema(): Any 18 | open fun toJSON(): Any 19 | 20 | companion object { 21 | fun compareWithDefault(memberName: String, memberValue: Any): Boolean 22 | fun defaultValues(memberName: String): Any 23 | fun schema(parameters: Any = definedExternally): Any 24 | } 25 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.AttributeTypeAndValue.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/AttributeTypeAndValue") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.AttributeTypeAndValue 11 | 12 | import Asn1js.ObjectIdentifier 13 | import org.khronos.webgl.ArrayBuffer 14 | 15 | @JsName("default") 16 | open external class AttributeTypeAndValue(params: Any = definedExternally) { 17 | open var type: ObjectIdentifier 18 | open var value: Any 19 | open fun isEqual(compareTo: AttributeTypeAndValue): Boolean 20 | open fun isEqual(compareTo: ArrayBuffer): Boolean 21 | open fun fromSchema(schema: Any) 22 | open fun toSchema(): Any 23 | open fun toJSON(): Any 24 | 25 | companion object { 26 | fun defaultValues(memberName: String): Any 27 | fun schema(parameters: Any = definedExternally): Any 28 | } 29 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.AuthorityKeyIdentifier.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/AuthorityKeyIdentifier") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.AuthorityKeyIdentifier 11 | 12 | import Asn1js.Integer 13 | import Asn1js.OctetString 14 | import pkijs.src.GeneralName.GeneralName 15 | 16 | @JsName("default") 17 | open external class AuthorityKeyIdentifier(params: Any = definedExternally) { 18 | open var keyIdentifier: OctetString 19 | open var authorityCertIssuer: Array 20 | open var authorityCertSerialNumber: Integer 21 | open fun fromSchema(schema: Any) 22 | open fun toSchema(): Any 23 | open fun toJSON(): Any 24 | 25 | companion object { 26 | fun defaultValues(memberName: String): Any 27 | fun schema(parameters: Any = definedExternally): Any 28 | } 29 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.BasicConstraints.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/BasicConstraints") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.BasicConstraints 11 | 12 | @JsName("default") 13 | open external class BasicConstraints(params: Any = definedExternally) { 14 | open var cA: Boolean 15 | open var pathLenConstraint: dynamic /* Number | Integer */ 16 | open fun fromSchema(schema: Any) 17 | open fun toSchema(): Any 18 | open fun toJSON(): Any 19 | 20 | companion object { 21 | fun defaultValues(memberName: String): Any 22 | fun schema(parameters: Any = definedExternally): Any 23 | } 24 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.CRLDistributionPoints.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/CRLDistributionPoints") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.CRLDistributionPoints 11 | 12 | import pkijs.src.DistributionPoint.DistributionPoint 13 | 14 | @JsName("default") 15 | open external class CRLDistributionPoints(params: Any = definedExternally) { 16 | open var distributionPoints: Array 17 | open fun fromSchema(schema: Any) 18 | open fun toSchema(): Any 19 | open fun toJSON(): Any 20 | 21 | companion object { 22 | fun defaultValues(memberName: String): Any 23 | fun schema(parameters: Any = definedExternally): Any 24 | } 25 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.Certificate.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/Certificate") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.Certificate 11 | 12 | //import tsstdlib.PromiseLike 13 | import Asn1js.BitString 14 | import Asn1js.Integer 15 | import Asn1js.Sequence 16 | import org.khronos.webgl.ArrayBuffer 17 | import pkijs.src.Extension.Extension 18 | import tsstdlib.CryptoKey 19 | 20 | @JsName("default") 21 | open external class Certificate(params: Any = definedExternally) { 22 | open var tbs: ArrayBuffer 23 | open var version: Number 24 | open var serialNumber: Integer 25 | open var signature: Any 26 | open var issuer: Any 27 | open var notBefore: Any 28 | open var notAfter: Any 29 | open var subject: Any 30 | open var subjectPublicKeyInfo: Any 31 | open var issuerUniqueID: ArrayBuffer 32 | open var subjectUniqueID: ArrayBuffer 33 | open var extensions: Array 34 | open var signatureAlgorithm: Any 35 | open var signatureValue: BitString 36 | open fun toSchema(encodeFlag: Boolean = definedExternally): Any 37 | open fun encodeTBS(): Sequence 38 | open fun getPublicKey(parameters: Any = definedExternally): dynamic // PromiseLike 39 | open fun getKeyHash(): dynamic // PromiseLike 40 | open fun sign(privateKey: CryptoKey, hashAlgorithm: String = definedExternally): dynamic // PromiseLike 41 | open fun verify(issuerCertificate: Certificate = definedExternally): dynamic //PromiseLike 42 | open fun fromSchema(schema: Any) 43 | open fun toJSON(): Any 44 | 45 | companion object { 46 | fun defaultValues(memberName: String): Any 47 | fun schema(parameters: Any = definedExternally): Any 48 | } 49 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.CertificateRevocationList.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/CertificateRevocationList") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.CertificateRevocationList 11 | 12 | import Asn1js.BitString 13 | import Asn1js.Sequence 14 | import org.khronos.webgl.ArrayBuffer 15 | import pkijs.src.Certificate.Certificate 16 | import pkijs.src.RevokedCertificate.RevokedCertificate 17 | import tsstdlib.CryptoKey 18 | 19 | external interface `T$2` { 20 | var issuerCertificate: Any? 21 | get() = definedExternally 22 | set(value) = definedExternally 23 | var publicKeyInfo: Any? 24 | get() = definedExternally 25 | set(value) = definedExternally 26 | } 27 | 28 | @JsName("default") 29 | open external class CertificateRevocationList(params: Any = definedExternally) { 30 | open var tbs: ArrayBuffer 31 | open var version: Number 32 | open var signature: dynamic /* String | Algorithm */ 33 | open var issuer: Any 34 | open var thisUpdate: Any 35 | open var nextUpdate: Any 36 | open var revokedCertificates: Array 37 | open var crlExtensions: Any 38 | open var signatureAlgorithm: dynamic /* String | Algorithm */ 39 | open var signatureValue: BitString 40 | open fun toSchema(encodeFlag: Boolean = definedExternally): Any 41 | open fun encodeTBS(): Sequence 42 | open fun isCertificateRevoked(certificate: Certificate): Boolean 43 | open fun sign(privateKey: CryptoKey, hashAlgorithm: String = definedExternally): dynamic // PromiseLike 44 | open fun verify(parameters: `T$2`): dynamic// PromiseLike 45 | open fun fromSchema(schema: Any) 46 | open fun toJSON(): Any 47 | 48 | companion object { 49 | fun defaultValues(memberName: String): Any 50 | fun schema(parameters: Any = definedExternally): Any 51 | } 52 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.DistributionPoint.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/DistributionPoint") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.DistributionPoint 11 | 12 | import Asn1js.BitString 13 | import pkijs.src.GeneralName.GeneralName 14 | 15 | @JsName("default") 16 | open external class DistributionPoint(params: Any = definedExternally) { 17 | open var distributionPoint: Array 18 | open var reasons: BitString 19 | open var cRLIssuer: Array 20 | open fun fromSchema(schema: Any) 21 | open fun toSchema(): Any 22 | open fun toJSON(): Any 23 | 24 | companion object { 25 | fun defaultValues(memberName: String): Any 26 | fun schema(parameters: Any = definedExternally): Any 27 | } 28 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.ECPrivateKey.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/ECPrivateKey") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.ECPrivateKey 11 | 12 | import Asn1js.OctetString 13 | import tsstdlib.JsonWebKey 14 | 15 | @JsName("default") 16 | open external class ECPrivateKey(params: Any = definedExternally) { 17 | open var version: Number 18 | open var privateKey: OctetString 19 | open var namedCurve: String 20 | open var publicKey: Any 21 | open fun fromJSON(json: JsonWebKey) 22 | open fun fromSchema(schema: Any) 23 | open fun toSchema(): Any 24 | open fun toJSON(): Any 25 | 26 | companion object { 27 | fun compareWithDefault(memberName: String, memberValue: Any): Boolean 28 | fun defaultValues(memberName: String): Any 29 | fun schema(parameters: Any = definedExternally): Any 30 | } 31 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.ECPublicKey.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/ECPublicKey") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.ECPublicKey 11 | 12 | import org.khronos.webgl.ArrayBuffer 13 | import tsstdlib.JsonWebKey 14 | 15 | @JsName("default") 16 | open external class ECPublicKey(params: Any = definedExternally) { 17 | open var x: ArrayBuffer 18 | open var y: ArrayBuffer 19 | open var namedCurve: String 20 | open fun fromJSON(json: JsonWebKey) 21 | open fun fromSchema(schema: Any) 22 | open fun toSchema(): Any 23 | open fun toJSON(): Any 24 | 25 | companion object { 26 | fun compareWithDefault(memberName: String, memberValue: Any): Boolean 27 | fun defaultValues(memberName: String): Any 28 | fun schema(parameters: Any = definedExternally): Any 29 | } 30 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.ExtKeyUsage.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/ExtKeyUsage") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.ExtKeyUsage 11 | 12 | @JsName("default") 13 | open external class ExtKeyUsage(params: Any = definedExternally) { 14 | open var keyPurposes: Array 15 | open fun fromSchema(schema: Any) 16 | open fun toSchema(): Any 17 | open fun toJSON(): Any 18 | 19 | companion object { 20 | fun defaultValues(memberName: String): Any 21 | fun schema(parameters: Any = definedExternally): Any 22 | } 23 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.Extension.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/Extension") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.Extension 11 | 12 | import Asn1js.OctetString 13 | 14 | @JsName("default") 15 | open external class Extension(params: Any = definedExternally) { 16 | open var extnID: String 17 | open var critical: Boolean 18 | open var extnValue: OctetString 19 | open var parsedValue: Any 20 | open fun fromSchema(schema: Any) 21 | open fun toSchema(): Any 22 | open fun toJSON(): Any 23 | 24 | companion object { 25 | fun defaultValues(memberName: String): Any 26 | fun schema(parameters: Any = definedExternally): Any 27 | } 28 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.Extensions.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/Extensions") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.Extensions 11 | 12 | import pkijs.src.Extension.Extension 13 | 14 | @JsName("default") 15 | open external class Extensions(params: Any = definedExternally) { 16 | open var extensions: Array 17 | open fun fromSchema(schema: Any) 18 | open fun toSchema(): Any 19 | open fun toJSON(): Any 20 | 21 | companion object { 22 | fun defaultValues(memberName: String): Any 23 | fun schema(parameters: Any = definedExternally): Any 24 | } 25 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.GeneralName.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/GeneralName") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.GeneralName 11 | 12 | @JsName("default") 13 | open external class GeneralName(params: Any = definedExternally) { 14 | open var type: Number 15 | open var value: Any 16 | open fun fromSchema(schema: Any) 17 | open fun toSchema(): Any 18 | open fun toJSON(): Any 19 | 20 | companion object { 21 | fun compareWithDefault(memberName: String, memberValue: Any): Boolean 22 | fun defaultValues(memberName: String): Any 23 | fun schema(parameters: Any = definedExternally): Any 24 | } 25 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.GeneralNames.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/GeneralNames") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.GeneralNames 11 | 12 | import pkijs.src.GeneralName.GeneralName 13 | 14 | @JsName("default") 15 | open external class GeneralNames(params: Any = definedExternally) { 16 | open var names: Array 17 | open fun fromSchema(schema: Any) 18 | open fun toSchema(): Any 19 | open fun toJSON(): Any 20 | 21 | companion object { 22 | fun defaultValues(memberName: String): Any 23 | fun schema(parameters: Any = definedExternally): Any 24 | } 25 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.IssuerAndSerialNumber.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/IssuerAndSerialNumber") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.IssuerAndSerialNumber 11 | 12 | import Asn1js.Integer 13 | 14 | @JsName("default") 15 | open external class IssuerAndSerialNumber(params: Any = definedExternally) { 16 | open var issuer: Any 17 | open var serialNumber: Integer 18 | open fun fromSchema(schema: Any) 19 | open fun toSchema(): Any 20 | open fun toJSON(): Any 21 | 22 | companion object { 23 | fun defaultValues(memberName: String): Any 24 | fun schema(parameters: Any = definedExternally): Any 25 | } 26 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.IssuingDistributionPoint.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/IssuingDistributionPoint") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.IssuingDistributionPoint 11 | 12 | @JsName("default") 13 | open external class IssuingDistributionPoint(params: Any = definedExternally) { 14 | open var distributionPoint: dynamic /* Array | RelativeDistinguishedNames */ 15 | open var onlyContainsUserCerts: Boolean 16 | open var onlySomeReasons: Number 17 | open var indirectCRL: Boolean 18 | open var onlyContainsAttributeCerts: Boolean 19 | open fun fromSchema(schema: Any) 20 | open fun toSchema(): Any 21 | open fun toJSON(): Any 22 | 23 | companion object { 24 | fun defaultValues(memberName: String): Any 25 | fun schema(parameters: Any = definedExternally): Any 26 | } 27 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.OtherPrimeInfo.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/OtherPrimeInfo") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.OtherPrimeInfo 11 | 12 | import Asn1js.Integer 13 | import JsonOtherPrimeInfo 14 | 15 | @JsName("default") 16 | open external class OtherPrimeInfo(params: Any = definedExternally) { 17 | open var prime: Integer 18 | open var exponent: Integer 19 | open var coefficient: Integer 20 | open fun fromJSON(json: JsonOtherPrimeInfo) 21 | open fun fromSchema(schema: Any) 22 | open fun toSchema(): Any 23 | open fun toJSON(): Any 24 | 25 | companion object { 26 | fun defaultValues(memberName: String): Any 27 | fun schema(parameters: Any = definedExternally): Any 28 | } 29 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.PrivateKeyInfo.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/PrivateKeyInfo") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.PrivateKeyInfo 11 | 12 | import Asn1js.OctetString 13 | import pkijs.src.Attribute.Attribute 14 | import tsstdlib.JsonWebKey 15 | 16 | @JsName("default") 17 | open external class PrivateKeyInfo(params: Any = definedExternally) { 18 | open var version: Number 19 | open var privateKeyAlgorithm: Any 20 | open var privateKey: OctetString 21 | open var attributes: Array 22 | open var parsedKey: dynamic /* ECPrivateKey | RSAPrivateKey */ 23 | open fun fromJSON(json: JsonWebKey) 24 | open fun fromSchema(schema: Any) 25 | open fun toSchema(): Any 26 | open fun toJSON(): Any 27 | 28 | companion object { 29 | fun defaultValues(memberName: String): Any 30 | fun schema(parameters: Any = definedExternally): Any 31 | } 32 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.PrivateKeyUsagePeriod.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/PrivateKeyUsagePeriod") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.PrivateKeyUsagePeriod 11 | 12 | import kotlin.js.Date 13 | 14 | @JsName("default") 15 | open external class PrivateKeyUsagePeriod(params: Any = definedExternally) { 16 | open var notBefore: Date 17 | open var notAfter: Date 18 | open fun fromSchema(schema: Any) 19 | open fun toSchema(): Any 20 | open fun toJSON(): Any 21 | 22 | companion object { 23 | fun defaultValues(memberName: String): Any 24 | fun schema(parameters: Any = definedExternally): Any 25 | } 26 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.PublicKeyInfo.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/PublicKeyInfo") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.PublicKeyInfo 11 | 12 | import Asn1js.BitString 13 | import tsstdlib.CryptoKey 14 | import tsstdlib.JsonWebKey 15 | 16 | @JsName("default") 17 | open external class PublicKeyInfo(params: Any = definedExternally) { 18 | open var algorithm: Any 19 | open var subjectPublicKey: BitString 20 | open var parsedKey: dynamic /* ECPublicKey | RSAPublicKey */ 21 | open fun fromJSON(json: JsonWebKey) 22 | 23 | open fun importKey(publicKey: CryptoKey): dynamic // PromiseLike 24 | open fun fromSchema(schema: Any) 25 | open fun toSchema(): Any 26 | open fun toJSON(): Any 27 | 28 | companion object { 29 | fun defaultValues(memberName: String): Any 30 | fun schema(parameters: Any = definedExternally): Any 31 | } 32 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.RSAPrivateKey.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/RSAPrivateKey") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.RSAPrivateKey 11 | 12 | import Asn1js.Integer 13 | import pkijs.src.OtherPrimeInfo.OtherPrimeInfo 14 | import tsstdlib.JsonWebKey 15 | 16 | @JsName("default") 17 | open external class RSAPrivateKey(params: Any = definedExternally) { 18 | open var version: Number 19 | open var modulus: Integer 20 | open var publicExponent: Integer 21 | open var privateExponent: Integer 22 | open var prime1: Integer 23 | open var prime2: Integer 24 | open var exponent1: Integer 25 | open var exponent2: Integer 26 | open var coefficient: Integer 27 | open var otherPrimeInfos: Array 28 | open fun fromJSON(json: JsonWebKey) 29 | open fun fromSchema(schema: Any) 30 | open fun toSchema(): Any 31 | open fun toJSON(): Any 32 | 33 | companion object { 34 | fun defaultValues(memberName: String): Any 35 | fun schema(parameters: Any = definedExternally): Any 36 | } 37 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.RSAPublicKey.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/RSAPublicKey") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.RSAPublicKey 11 | 12 | import Asn1js.Integer 13 | import tsstdlib.JsonWebKey 14 | 15 | @JsName("default") 16 | open external class RSAPublicKey(params: Any = definedExternally) { 17 | open var modulus: Integer 18 | open var publicExponent: Integer 19 | open fun fromJSON(json: JsonWebKey) 20 | open fun fromSchema(schema: Any) 21 | open fun toSchema(): Any 22 | open fun toJSON(): Any 23 | 24 | companion object { 25 | fun defaultValues(memberName: String): Any 26 | fun schema(parameters: Any = definedExternally): Any 27 | } 28 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.RSASSAPSSParams.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/RSASSAPSSParams") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.RSASSAPSSParams 11 | 12 | @JsName("default") 13 | open external class RSASSAPSSParams(params: Any = definedExternally) { 14 | open var hashAlgorithm: Any 15 | open var maskGenAlgorithm: Any 16 | open var saltLength: Number 17 | open var trailerField: Number 18 | open fun fromSchema(schema: Any) 19 | open fun toSchema(): Any 20 | open fun toJSON(): Any 21 | 22 | companion object { 23 | fun defaultValues(memberName: String): Any 24 | fun schema(parameters: Any = definedExternally): Any 25 | } 26 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.RelativeDistinguishedNames.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/RelativeDistinguishedNames") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.RelativeDistinguishedNames 11 | 12 | import org.khronos.webgl.ArrayBuffer 13 | import pkijs.src.AttributeTypeAndValue.AttributeTypeAndValue 14 | 15 | @JsName("default") 16 | open external class RelativeDistinguishedNames(params: Any = definedExternally) { 17 | open var typesAndValues: Array 18 | open var valueBeforeDecode: ArrayBuffer 19 | open fun fromSchema(schema: Any) 20 | open fun toSchema(): Any 21 | open fun toJSON(): Any 22 | open fun isEqual(compareTo: RelativeDistinguishedNames): Boolean 23 | open fun isEqual(compareTo: ArrayBuffer): Boolean 24 | 25 | companion object { 26 | fun compareWithDefault(memberName: String, memberValue: Any): Boolean 27 | fun defaultValues(memberName: String): Any 28 | fun schema(parameters: Any = definedExternally): Any 29 | } 30 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.RevokedCertificate.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/RevokedCertificate") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.RevokedCertificate 11 | 12 | import Asn1js.Integer 13 | 14 | @JsName("default") 15 | open external class RevokedCertificate(params: Any = definedExternally) { 16 | open var userCertificate: Integer 17 | open var revocationDate: Any 18 | open var crlEntryExtensions: Any 19 | open fun fromSchema(schema: Any) 20 | open fun toSchema(): Any 21 | open fun toJSON(): Any 22 | 23 | companion object { 24 | fun defaultValues(memberName: String): Any 25 | fun schema(parameters: Any = definedExternally): Any 26 | } 27 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.Signature.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/Signature") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.Signature 11 | 12 | import Asn1js.BitString 13 | import pkijs.src.Certificate.Certificate 14 | 15 | @JsName("default") 16 | open external class Signature(params: Any = definedExternally) { 17 | open var signatureAlgorithm: Any 18 | open var signature: BitString 19 | open var certs: Array 20 | open fun fromSchema(schema: Any) 21 | open fun toSchema(): Any 22 | open fun toJSON(): Any 23 | 24 | companion object { 25 | fun compareWithDefault(memberName: String, memberValue: Any): Boolean 26 | fun defaultValues(memberName: String): Any 27 | fun schema(parameters: Any = definedExternally): Any 28 | } 29 | } -------------------------------------------------------------------------------- /src/jsMain/kotlin/external/pkijs/index.pkijs.src.Time.module_pkijs.kt: -------------------------------------------------------------------------------- 1 | @file:JsModule("pkijs/src/Time") 2 | @file:JsNonModule 3 | @file:Suppress( 4 | "INTERFACE_WITH_SUPERCLASS", 5 | "OVERRIDING_FINAL_MEMBER", 6 | "RETURN_TYPE_MISMATCH_ON_OVERRIDE", 7 | "CONFLICTING_OVERLOADS" 8 | ) 9 | 10 | package pkijs.src.Time 11 | 12 | import kotlin.js.Date 13 | 14 | @JsName("default") 15 | open external class Time(params: Any = definedExternally) { 16 | open var type: Number 17 | open var value: Date 18 | open fun fromSchema(schema: Any) 19 | open fun toSchema(): Any 20 | open fun toJSON(): Any 21 | 22 | companion object { 23 | fun defaultValues(memberName: String): Any 24 | fun schema(parameters: Any = definedExternally): Any 25 | } 26 | } -------------------------------------------------------------------------------- /src/jsTest/kotlin/ehn/techiop/hcert/kotlin/chain/ext/loadResource.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.ext 2 | 3 | import TestResourceHolder 4 | import ehn.techiop.hcert.kotlin.data.loadAsString 5 | 6 | 7 | actual fun allOfficialTestCases(): Map { 8 | val map = mutableMapOf() 9 | TestResourceHolder.allResourceNames() 10 | .filter { it.startsWith("dgc-testdata/") } 11 | .filter { it.endsWith(".json") } 12 | .forEach { map[it] = TestResourceHolder.loadAsString(it)!! } 13 | return map 14 | } 15 | -------------------------------------------------------------------------------- /src/jsTest/kotlin/ehn/techiop/hcert/kotlin/chain/impl/DefaultTwoDimCodeServiceJsTest.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.asBase64 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.datatest.withData 6 | import io.kotest.matchers.ints.shouldBeGreaterThan 7 | import io.kotest.matchers.shouldNotBe 8 | import kotlin.random.Random 9 | 10 | class DefaultTwoDimCodeServiceJsTest : FunSpec({ 11 | withData( 12 | TestInput(Random.nextBytes(32).asBase64(), 1, 0), 13 | TestInput(Random.nextBytes(32).asBase64(), 2, 2), 14 | TestInput(Random.nextBytes(32).asBase64(), 3, 4), 15 | TestInput(Random.nextBytes(32).asBase64(), 4, 8) 16 | ) { input -> 17 | val service = DefaultTwoDimCodeService(input.moduleSize) 18 | 19 | val encoded = service.encode(input.input) 20 | encoded shouldNotBe null 21 | encoded.asBase64().length shouldBeGreaterThan (input.moduleSize * input.moduleSize) 22 | // decoding the image from encode isn't supported by the JS library 23 | //val decoded = service.decode(encoded) 24 | //decoded shouldBe input.input 25 | } 26 | }) 27 | 28 | data class TestInput(val input: String, val moduleSize: Int, val marginSize: Int) 29 | -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/BothProtectedWrongCoseService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoService 4 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCoseService 5 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 6 | import ehn.techiop.hcert.kotlin.trust.CoseCreationAdapter 7 | 8 | /** 9 | * Puts wrong header entries into the protected COSE header, plus an additional unprotected KID with correct value. 10 | */ 11 | class BothProtectedWrongCoseService(private val cryptoService: CryptoService) : DefaultCoseService(cryptoService) { 12 | 13 | override fun encode(input: ByteArray): ByteArray { 14 | val coseAdapter = CoseCreationAdapter(input) 15 | cryptoService.getCborHeaders().forEach { 16 | if (it.first == CoseHeaderKeys.KID) { 17 | coseAdapter.addUnprotectedAttribute(it.first, it.second) 18 | coseAdapter.addProtectedAttribute(it.first, "foo".encodeToByteArray()) 19 | } else 20 | coseAdapter.addProtectedAttribute(it.first, it.second) 21 | } 22 | coseAdapter.sign(cryptoService.getCborSigningKey()) 23 | return coseAdapter.encode() 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/BothUnprotectedWrongCoseService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoService 4 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCoseService 5 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 6 | import ehn.techiop.hcert.kotlin.trust.CoseCreationAdapter 7 | 8 | /** 9 | * Puts header entries into the protected COSE header, plus an additional unprotected KID with wrong value. 10 | * 11 | * Actually, this conforms to the specification, but we'll prefer to put the entries into the protected COSE header. 12 | */ 13 | class BothUnprotectedWrongCoseService(private val cryptoService: CryptoService) : DefaultCoseService(cryptoService) { 14 | 15 | override fun encode(input: ByteArray): ByteArray { 16 | val coseAdapter = CoseCreationAdapter(input) 17 | cryptoService.getCborHeaders().forEach { 18 | coseAdapter.addProtectedAttribute(it.first, it.second) 19 | } 20 | coseAdapter.addUnprotectedAttribute(CoseHeaderKeys.KID, "foo".encodeToByteArray()) 21 | coseAdapter.sign(cryptoService.getCborSigningKey()) 22 | return coseAdapter.encode() 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/BrokenCoseService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import COSE.Sign1Message 4 | import com.upokecenter.cbor.CBORObject 5 | import ehn.techiop.hcert.kotlin.chain.CryptoService 6 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCoseService 7 | 8 | /** 9 | * Encodes the input into a COSE structure with broken signature values 10 | * 11 | * **Should not be used in production.** 12 | */ 13 | class BrokenCoseService(private val cryptoService: CryptoService) : DefaultCoseService(cryptoService) { 14 | 15 | override fun encode(input: ByteArray): ByteArray { 16 | val signed = Sign1Message.DecodeFromBytes(super.encode(input)) as Sign1Message 17 | val cbor = signed.EncodeToCBORObject() 18 | cbor[3] = CBORObject.FromObject("foo".toByteArray()) 19 | return cbor.EncodeToBytes() 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/DuplicateHeaderCoseService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoService 4 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCoseService 5 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 6 | import ehn.techiop.hcert.kotlin.trust.CoseCreationAdapter 7 | 8 | /** 9 | * Puts the KID header entry into the unprotected *and* protected COSE header. 10 | * 11 | * Actually, this conforms to the specification, but we'll prefer to put the entries into the protected COSE header. 12 | */ 13 | class DuplicateHeaderCoseService(private val cryptoService: CryptoService) : DefaultCoseService(cryptoService) { 14 | 15 | override fun encode(input: ByteArray): ByteArray { 16 | val coseAdapter = CoseCreationAdapter(input) 17 | cryptoService.getCborHeaders().forEach { 18 | coseAdapter.addProtectedAttribute(it.first, it.second) 19 | if (it.first == CoseHeaderKeys.KID) 20 | coseAdapter.addUnprotectedAttribute(it.first, it.second) 21 | } 22 | coseAdapter.sign(cryptoService.getCborSigningKey()) 23 | return coseAdapter.encode() 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/FaultyBase45Service.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import ehn.techiop.hcert.kotlin.chain.impl.Base45Encoder 4 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultBase45Service 5 | 6 | /** 7 | * Produces a wrong Base45 encoding. 8 | * 9 | * **Should not be used in production.** 10 | */ 11 | class FaultyBase45Service : DefaultBase45Service() { 12 | 13 | private val encoder = Base45Encoder 14 | 15 | override fun encode(input: ByteArray) = 16 | encoder.encode(input).dropLast(5) + "=====" 17 | 18 | } -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/FaultyCborService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCborService 4 | import ehn.techiop.hcert.kotlin.data.GreenCertificate 5 | 6 | /** 7 | * Encodes the input in a faulty CBOR encoding, i.e. reversed byte array 8 | * 9 | * **Should not be used in production.** 10 | */ 11 | class FaultyCborService : DefaultCborService() { 12 | 13 | override fun encode(input: GreenCertificate) = super.encode(input).reversed().toByteArray() 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/FaultyCompressorService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCompressorService 4 | import java.util.zip.Deflater 5 | import java.util.zip.DeflaterInputStream 6 | 7 | /** 8 | * Reverses the ZLIB encoding, resulting in a non-decodable output. 9 | * 10 | * **Should not be used in production.** 11 | */ 12 | class FaultyCompressorService : DefaultCompressorService() { 13 | 14 | override fun encode(input: ByteArray): ByteArray { 15 | return DeflaterInputStream(input.inputStream(), Deflater(9)).readBytes().reversedArray() 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/FaultyCoseService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoService 4 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCoseService 5 | 6 | /** 7 | * Encodes the input into an non-parsable COSE structure (i.e. reversed). 8 | * 9 | * **Should not be used in production.** 10 | */ 11 | class FaultyCoseService(private val cryptoService: CryptoService) : DefaultCoseService(cryptoService) { 12 | 13 | override fun encode(input: ByteArray): ByteArray { 14 | return super.encode(input).reversedArray() 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/FaultyCwtService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import com.upokecenter.cbor.CBORObject 4 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCwtService 5 | 6 | /** 7 | * Encodes the input without the required CWT structure around it. 8 | * 9 | * **Should not be used in production.** 10 | */ 11 | class FaultyCwtService : DefaultCwtService() { 12 | 13 | override fun encode(input: ByteArray) = CBORObject.NewMap().also { 14 | it[CBORObject.FromObject(1)] = CBORObject.DecodeFromBytes(input) 15 | }.EncodeToBytes() 16 | 17 | } -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/NonVerifiableCoseService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoService 4 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCoseService 5 | import ehn.techiop.hcert.kotlin.chain.impl.RandomEcKeyCryptoService 6 | import ehn.techiop.hcert.kotlin.trust.CoseCreationAdapter 7 | 8 | /** 9 | * Signs the input with a random key, i.e. it is never verifiable. 10 | * 11 | * **Should not be used in production.** 12 | */ 13 | class NonVerifiableCoseService(private val cryptoService: CryptoService) : DefaultCoseService(cryptoService) { 14 | 15 | override fun encode(input: ByteArray): ByteArray { 16 | val coseAdapter = CoseCreationAdapter(input) 17 | cryptoService.getCborHeaders().forEach { 18 | coseAdapter.addProtectedAttribute(it.first, it.second) 19 | } 20 | coseAdapter.sign(RandomEcKeyCryptoService().getCborSigningKey()) 21 | return coseAdapter.encode() 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/NoopCompressorService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCompressorService 4 | 5 | /** 6 | * Does not compress the input at all -- SPEC says input SHALL be compressed = required 7 | * 8 | * **Should not be used in production.** 9 | */ 10 | class NoopCompressorService : DefaultCompressorService() { 11 | 12 | override fun encode(input: ByteArray): ByteArray { 13 | return input 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/NoopContextIdentifierService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultContextIdentifierService 4 | 5 | /** 6 | * Does not add a prefix to the input, thus violates the specification. 7 | * 8 | * **Should not be used in production.** 9 | */ 10 | class NoopContextIdentifierService(prefix: String = "HC1:") : DefaultContextIdentifierService(prefix) { 11 | 12 | override fun encode(input: String): String { 13 | return input 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/UnprotectedCoseService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoService 4 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCoseService 5 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 6 | import ehn.techiop.hcert.kotlin.trust.CoseCreationAdapter 7 | 8 | /** 9 | * Puts the KID header entry into the unprotected COSE header. 10 | * 11 | * Actually, this conforms to the specification, but we'll prefer to put the entries into the protected COSE header. 12 | */ 13 | class UnprotectedCoseService(private val cryptoService: CryptoService) : DefaultCoseService(cryptoService) { 14 | 15 | override fun encode(input: ByteArray): ByteArray { 16 | val coseAdapter = CoseCreationAdapter(input) 17 | cryptoService.getCborHeaders().forEach { 18 | if (it.first == CoseHeaderKeys.KID) 19 | coseAdapter.addUnprotectedAttribute(it.first, it.second) 20 | else 21 | coseAdapter.addProtectedAttribute(it.first, it.second) 22 | } 23 | coseAdapter.sign(cryptoService.getCborSigningKey()) 24 | return coseAdapter.encode() 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/jvmMain/datagen/ehn/techiop/hcert/kotlin/chain/faults/WrongUnprotectedCoseService.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.faults 2 | 3 | import ehn.techiop.hcert.kotlin.chain.CryptoService 4 | import ehn.techiop.hcert.kotlin.chain.impl.DefaultCoseService 5 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 6 | import ehn.techiop.hcert.kotlin.trust.CoseCreationAdapter 7 | 8 | /** 9 | * Puts all header entries into the unprotected COSE header, and garbles the KID 10 | */ 11 | class WrongUnprotectedCoseService(private val cryptoService: CryptoService) : DefaultCoseService(cryptoService) { 12 | 13 | override fun encode(input: ByteArray): ByteArray { 14 | val coseAdapter = CoseCreationAdapter(input) 15 | cryptoService.getCborHeaders().forEach { 16 | if (it.first == CoseHeaderKeys.KID) 17 | coseAdapter.addUnprotectedAttribute(it.first, "foo".encodeToByteArray()) 18 | else 19 | coseAdapter.addUnprotectedAttribute(it.first, it.second) 20 | } 21 | coseAdapter.sign(cryptoService.getCborSigningKey()) 22 | return coseAdapter.encode() 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/ehn/techiop/hcert/kotlin/chain/Extensions.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain 2 | 3 | import org.bouncycastle.util.encoders.Base64 4 | 5 | actual fun ByteArray.asBase64() = Base64.toBase64String(this) 6 | 7 | actual fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) } 8 | 9 | actual fun String.fromBase64() = Base64.decode(this) 10 | 11 | actual fun String.fromHexString() = chunked(2).map { it.toInt(16).toByte() }.toByteArray() 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/CompressorAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.chain.impl.CompressionConstants.MAX_DECOMPRESSED_SIZE 4 | import java.io.ByteArrayOutputStream 5 | import java.io.OutputStream 6 | import java.util.zip.Deflater 7 | import java.util.zip.DeflaterInputStream 8 | import java.util.zip.InflaterInputStream 9 | 10 | actual open class CompressorAdapter { 11 | 12 | actual fun encode(input: ByteArray, level: Int) = 13 | DeflaterInputStream(input.inputStream(), Deflater(level)).readBytes() 14 | 15 | actual fun decode(input: ByteArray) = 16 | InflaterInputStream(input.inputStream()).readBytes().also { 17 | val inflaterStream = InflaterInputStream(input.inputStream()) 18 | val outputStream = ByteArrayOutputStream(DEFAULT_BUFFER_SIZE) 19 | inflaterStream.copyTo(outputStream) 20 | outputStream.toByteArray() 21 | } 22 | 23 | } 24 | 25 | // Adapted from kotlin-stdblib's kotlin.io.IOStreams.kt 26 | private fun InflaterInputStream.copyTo(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE): Long { 27 | var bytesCopied: Long = 0 28 | val buffer = ByteArray(bufferSize) 29 | var bytes = read(buffer) 30 | while (bytes >= 0) { 31 | out.write(buffer, 0, bytes) 32 | bytesCopied += bytes 33 | bytes = read(buffer) 34 | // begin patch 35 | if (bytesCopied > MAX_DECOMPRESSED_SIZE) { 36 | throw IllegalArgumentException("Decompression exceeded $MAX_DECOMPRESSED_SIZE bytes, is: $bytesCopied! Input must be invalid.") 37 | } 38 | // end patch 39 | } 40 | return bytesCopied 41 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/ehn/techiop/hcert/kotlin/chain/impl/SchemaValidationAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.data.CborObject 4 | import ehn.techiop.hcert.kotlin.data.GreenCertificate 5 | import ehn.techiop.hcert.kotlin.trust.JvmCwtAdapter 6 | import kotlinx.serialization.decodeFromString 7 | import kotlinx.serialization.json.Json 8 | import net.pwall.json.schema.JSONSchema 9 | import net.pwall.json.schema.parser.Parser 10 | import java.io.InputStream 11 | import java.net.URI 12 | 13 | class JvmSchemaLoader(vararg validVersions: String) : SchemaLoader(*validVersions) { 14 | 15 | override fun loadSchema(version: String) = getSchemaResource(version).use(this@JvmSchemaLoader::parse) 16 | 17 | override fun loadFallbackSchema() = getFallbackSchema().use(this@JvmSchemaLoader::parse) 18 | 19 | private fun parse(resource: InputStream) = Parser(uriResolver = { resource }).parse(URI.create("dummy:///")) 20 | 21 | private fun getSchemaResource(version: String) = 22 | classLoader().getResourceAsStream("json/schema/$version/DCC.combined-schema.json") 23 | ?: throw IllegalArgumentException("Schema not found: $version") 24 | 25 | private fun getFallbackSchema() = 26 | classLoader().getResourceAsStream("json/schema/fallback/DCC.combined-schema.json") 27 | ?: throw IllegalArgumentException("Fallback schema not found") 28 | 29 | private fun classLoader() = SchemaValidationAdapter::class.java.classLoader 30 | 31 | } 32 | 33 | actual class SchemaValidationAdapter actual constructor(private val cbor: CborObject, validVersions: Array) { 34 | 35 | private val schemaLoader = JvmSchemaLoader(*validVersions) 36 | private val json = (cbor as JvmCwtAdapter.JvmCborObject).toJsonString() 37 | 38 | actual fun hasValidator(versionString: String): Boolean { 39 | return schemaLoader.validators[versionString] != null 40 | } 41 | 42 | actual fun validateBasic(versionString: String): Collection { 43 | val validator = schemaLoader.validators[versionString] ?: throw IllegalArgumentException("versionString") 44 | return validate(validator) 45 | } 46 | 47 | actual fun validateWithFallback(): Collection { 48 | val validator = schemaLoader.loadFallbackSchema() 49 | return validate(validator) 50 | } 51 | 52 | private fun validate(validator: JSONSchema): Collection { 53 | val result = validator.validateBasic(json) 54 | return result.errors?.map { SchemaError("${it.error}, ${it.keywordLocation}, ${it.instanceLocation}") } 55 | ?: listOf() 56 | } 57 | 58 | actual fun toJson(): GreenCertificate { 59 | return Json.decodeFromString(json) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/ehn/techiop/hcert/kotlin/crypto/CertificateAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | import ehn.techiop.hcert.kotlin.chain.fromBase64 4 | import ehn.techiop.hcert.kotlin.trust.ContentType 5 | import ehn.techiop.hcert.kotlin.trust.Hash 6 | import ehn.techiop.hcert.kotlin.trust.TrustedCertificateV2 7 | import kotlinx.datetime.Instant 8 | import java.security.cert.CertificateFactory 9 | import java.security.cert.X509Certificate 10 | 11 | actual class CertificateAdapter(val certificate: X509Certificate) { 12 | 13 | actual constructor(pemEncoded: String) : this( 14 | CertificateFactory.getInstance("X.509").generateCertificate( 15 | pemEncoded 16 | .replace("-----BEGIN CERTIFICATE-----", "") 17 | .replace("-----END CERTIFICATE-----", "") 18 | .lines().joinToString(separator = "") 19 | .fromBase64().inputStream() 20 | ) as X509Certificate 21 | ) 22 | 23 | actual constructor(_encoded: ByteArray) : this( 24 | CertificateFactory.getInstance("X.509") 25 | .generateCertificate(_encoded.inputStream()) as X509Certificate 26 | ) 27 | 28 | actual val validContentTypes: List 29 | get() { 30 | val contentTypes = mutableSetOf() 31 | certificate.extendedKeyUsage?.let { 32 | it.forEach { oid -> 33 | ContentType.findByOid(oid)?.let { contentTypes.add(it) } 34 | } 35 | } 36 | return contentTypes.toList() 37 | } 38 | 39 | actual val validFrom = Instant.fromEpochMilliseconds(certificate.notBefore.time) 40 | 41 | actual val validUntil = Instant.fromEpochMilliseconds(certificate.notAfter.time) 42 | 43 | actual val subjectCountry = Regex("C=[^,]*").find(certificate.subjectX500Principal.name)?.value?.replace("C=", "") 44 | 45 | actual val publicKey: PubKey = JvmPubKey(certificate.publicKey) 46 | 47 | actual fun toTrustedCertificate() = TrustedCertificateV2(kid, certificate.encoded) 48 | 49 | actual val kid = certificate.kid 50 | 51 | actual val encoded = certificate.encoded 52 | actual fun prettyPrint() = certificate.toString() 53 | 54 | } 55 | 56 | val X509Certificate.kid: ByteArray 57 | get() = Hash(encoded).calc().copyOf(8) 58 | 59 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/ehn/techiop/hcert/kotlin/crypto/JvmPrivKey.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | import COSE.OneKey 4 | import java.security.PrivateKey 5 | 6 | open class JvmPrivKey(private val privateKey: PrivateKey) : PrivKey { 7 | 8 | open fun toCoseRepresentation() = OneKey(null, privateKey) 9 | 10 | open fun toPlatformPrivateKey() = privateKey 11 | 12 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/ehn/techiop/hcert/kotlin/crypto/JvmPubKey.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.crypto 2 | 3 | import COSE.OneKey 4 | import java.security.PublicKey 5 | 6 | open class JvmPubKey(private val publicKey: PublicKey) : PubKey { 7 | 8 | open fun toCoseRepresentation() = OneKey(publicKey, null) 9 | 10 | open fun toPlatformPublicKey() = publicKey 11 | 12 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/ehn/techiop/hcert/kotlin/data/ValueSetsInstanceHolder.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.data 2 | 3 | import kotlinx.serialization.decodeFromString 4 | import kotlinx.serialization.json.Json 5 | 6 | actual object ValueSetsInstanceHolder { 7 | 8 | actual val INSTANCE: ValueSetHolder by lazy { 9 | ValueSetHolder(inputPaths 10 | .mapNotNull { this::class.java.getResource(it) } 11 | .map { it.readText() } 12 | .map { Json.decodeFromString(it) } 13 | ) 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/ehn/techiop/hcert/kotlin/trust/CoseCreationAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import COSE.Attribute 4 | import COSE.Sign1Message 5 | import com.upokecenter.cbor.CBORObject 6 | import ehn.techiop.hcert.kotlin.crypto.CoseHeaderKeys 7 | import ehn.techiop.hcert.kotlin.crypto.CwtAlgorithm 8 | import ehn.techiop.hcert.kotlin.crypto.JvmPrivKey 9 | import ehn.techiop.hcert.kotlin.crypto.PrivKey 10 | import org.bouncycastle.jce.provider.BouncyCastleProvider 11 | import java.security.Security 12 | 13 | actual class CoseCreationAdapter actual constructor(private val content: ByteArray) { 14 | 15 | private val sign1Message = Sign1Message().also { it.SetContent(content) } 16 | 17 | init { 18 | Security.addProvider(BouncyCastleProvider()) // for SHA256withRSA/PSS 19 | } 20 | 21 | actual fun addProtectedAttribute(key: CoseHeaderKeys, value: Any) { 22 | val content = if (value is CwtAlgorithm) value.intVal else value 23 | sign1Message.addAttribute( 24 | CBORObject.FromObject(key.intVal), 25 | CBORObject.FromObject(content), 26 | Attribute.PROTECTED 27 | ) 28 | } 29 | 30 | actual fun addUnprotectedAttribute(key: CoseHeaderKeys, value: Any) { 31 | val content = if (value is CwtAlgorithm) value.intVal else value 32 | sign1Message.addAttribute( 33 | CBORObject.FromObject(key.intVal), 34 | CBORObject.FromObject(content), 35 | Attribute.UNPROTECTED 36 | ) 37 | } 38 | 39 | actual fun sign(key: PrivKey) { 40 | sign1Message.sign((key as JvmPrivKey).toCoseRepresentation()) 41 | } 42 | 43 | actual fun encode() = sign1Message.EncodeToBytes() 44 | 45 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/ehn/techiop/hcert/kotlin/trust/CoseWorkaround.kt: -------------------------------------------------------------------------------- 1 | package COSE 2 | 3 | val Sign1Message.signature: ByteArray 4 | get() = rgbSignature 5 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/ehn/techiop/hcert/kotlin/trust/CwtCreationAdapter.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import com.upokecenter.cbor.CBORObject 4 | 5 | actual class CwtCreationAdapter actual constructor() { 6 | 7 | private val map = mutableMapOf() 8 | 9 | actual fun add(key: Int, value: Any) { 10 | map[CBORObject.FromObject(key)] = CBORObject.FromObject(value) 11 | } 12 | 13 | actual fun addDgc(key: Int, innerKey: Int, input: ByteArray) { 14 | map[CBORObject.FromObject(key)] = CBORObject.NewMap().also { 15 | try { 16 | it[CBORObject.FromObject(innerKey)] = CBORObject.DecodeFromBytes(input) 17 | } catch (e: Exception) { 18 | it[CBORObject.FromObject(innerKey)] = CBORObject.FromObject(input) 19 | } 20 | } 21 | } 22 | 23 | actual fun encode(): ByteArray { 24 | val result = CBORObject.NewMap() 25 | map.forEach { result[it.key] = it.value } 26 | return result.EncodeToBytes() 27 | } 28 | 29 | 30 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/ehn/techiop/hcert/kotlin/trust/Hash.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.trust 2 | 3 | import java.security.MessageDigest 4 | 5 | actual class Hash actual constructor(private val input: ByteArray) { 6 | actual fun calc() = MessageDigest.getInstance("SHA-256").digest(input) 7 | } -------------------------------------------------------------------------------- /src/jvmTest/kotlin/ehn/techiop/hcert/kotlin/chain/ext/loadResource.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.ext 2 | 3 | import java.io.File 4 | 5 | actual fun allOfficialTestCases(): Map { 6 | val baseDir = File("src/commonTest/resources/dgc-testdata") 7 | return baseDir.walkTopDown() 8 | .filter { it.name.endsWith(".json") } 9 | .map { it.relativeTo(baseDir).path to it.readText() } 10 | .sortedBy { it.first } 11 | .toMap() 12 | } -------------------------------------------------------------------------------- /src/jvmTest/kotlin/ehn/techiop/hcert/kotlin/chain/impl/DefaultTwoDimCodeServiceJvmTest.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import com.google.zxing.BarcodeFormat 4 | import ehn.techiop.hcert.kotlin.chain.asBase64 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.datatest.withData 7 | import io.kotest.matchers.ints.shouldBeGreaterThan 8 | import io.kotest.matchers.shouldBe 9 | import io.kotest.matchers.shouldNotBe 10 | import kotlin.random.Random 11 | 12 | class DefaultTwoDimCodeServiceJvmTest : FunSpec({ 13 | withData( 14 | TestInput(Random.nextBytes(32).asBase64(), BarcodeFormat.AZTEC, 300, 0), 15 | TestInput(Random.nextBytes(32).asBase64(), BarcodeFormat.AZTEC, 500, 2), 16 | TestInput(Random.nextBytes(32).asBase64(), BarcodeFormat.QR_CODE, 300, 4), 17 | TestInput(Random.nextBytes(32).asBase64(), BarcodeFormat.QR_CODE, 500, 8) 18 | ) { input -> 19 | val service = DefaultTwoDimCodeService(input.size, input.format) 20 | 21 | val encoded = service.encode(input.input) 22 | encoded shouldNotBe null 23 | encoded.asBase64().length shouldBeGreaterThan input.size 24 | 25 | val decoded = service.decode(encoded) 26 | decoded shouldBe input.input 27 | } 28 | }) 29 | 30 | data class TestInput(val input: String, val format: BarcodeFormat, val size: Int, val marginSize: Int) 31 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/ehn/techiop/hcert/kotlin/chain/impl/FileBasedCryptoServiceJvmTest.kt: -------------------------------------------------------------------------------- 1 | package ehn.techiop.hcert.kotlin.chain.impl 2 | 3 | import ehn.techiop.hcert.kotlin.crypto.JvmPrivKey 4 | import io.kotest.core.spec.style.DescribeSpec 5 | import io.kotest.datatest.withData 6 | import io.kotest.matchers.shouldBe 7 | 8 | class FileBasedCryptoServiceJvmTest : DescribeSpec({ 9 | 10 | withData(256, 384) { keySize -> 11 | val input = RandomEcKeyCryptoService(keySize) 12 | val privateKeyPem = input.exportPrivateKeyAsPem() 13 | val certificatePem = input.exportCertificateAsPem() 14 | val parsed = FileBasedCryptoService(privateKeyPem, certificatePem) 15 | 16 | (input.getCborSigningKey() as JvmPrivKey).toCoseRepresentation().EncodeToBytes() 17 | .shouldBe((parsed.getCborSigningKey() as JvmPrivKey).toCoseRepresentation().EncodeToBytes()) 18 | input.getCertificate().kid shouldBe (parsed.getCertificate().kid) 19 | 20 | parsed.getCertificate().certificate.verify(parsed.getCertificate().certificate.publicKey) 21 | } 22 | 23 | withData(2048, 3072) { keySize -> 24 | val input = RandomRsaKeyCryptoService(keySize) 25 | val privateKeyPem = input.exportPrivateKeyAsPem() 26 | val certificatePem = input.exportCertificateAsPem() 27 | val parsed = FileBasedCryptoService(privateKeyPem, certificatePem) 28 | 29 | (input.getCborSigningKey() as JvmPrivKey).toCoseRepresentation().EncodeToBytes() 30 | .shouldBe((parsed.getCborSigningKey() as JvmPrivKey).toCoseRepresentation().EncodeToBytes()) 31 | input.getCertificate().kid shouldBe (parsed.getCertificate().kid) 32 | 33 | parsed.getCertificate().certificate.verify(parsed.getCertificate().certificate.publicKey) 34 | } 35 | 36 | }) -------------------------------------------------------------------------------- /webpack-templates/patch-browser.js: -------------------------------------------------------------------------------- 1 | // Workaround for https://youtrack.jetbrains.com/issue/KT-46082 2 | 3 | const webpack = require('webpack') 4 | 5 | // Make sure dependencies that explicitly require these (built-in) node.js libraries can find them 6 | config.resolve.alias = { 7 | crypto: require.resolve("crypto-browserify"), 8 | buffer: require.resolve("buffer"), 9 | stream: require.resolve("stream-browserify"), 10 | util: require.resolve("util"), 11 | 'node-inspect-extracted': require.resolve("node-inspect-extracted"), 12 | url: require.resolve("url"), 13 | assert: require.resolve("assert"), 14 | constants: require.resolve("constants-browserify") 15 | } 16 | 17 | // Add polyfills for implicitly required node.js built-ins 18 | config.plugins.push( 19 | new webpack.ProvidePlugin({ 20 | process: 'process/browser.js', 21 | Buffer: ['buffer', 'Buffer'], 22 | })) 23 | config.performance = { 24 | maxEntrypointSize: 512000*5, 25 | maxAssetSize: 512000*5 26 | } -------------------------------------------------------------------------------- /webpack-templates/patch-node.js: -------------------------------------------------------------------------------- 1 | config.target="node" 2 | config.output.filename="hcert-node.js" 3 | config.performance = { 4 | maxEntrypointSize: 512000 * 5, 5 | maxAssetSize: 512000 * 5 6 | } 7 | config.resolve.fallback = {"node-inspect-extracted":false} --------------------------------------------------------------------------------