├── .gitignore ├── Tests ├── EUDCCTests │ └── EUDCCTests.swift ├── EUDCCVerifierTests │ └── EUDCCVerifierTests.swift ├── EUDCCValidatorTests │ └── EUDCCValidatorTests.swift ├── EUDCCDecoderTests │ └── EUDCCDecoderTests.swift └── _EUDCCKitTests │ └── EUDCCKitTests.swift ├── Sources ├── EUDCC │ ├── Models │ │ ├── EUDCC+Content.swift │ │ ├── EUDCC+CryptographicSignature.swift │ │ ├── EUDCC+Country.swift │ │ ├── EUDCC+DiseaseAgentTargeted.swift │ │ ├── Vaccination │ │ │ ├── EUDCC+Vaccination+VaccineOrProphylaxis.swift │ │ │ ├── EUDCC+Vaccination+VaccineMedicinalProduct.swift │ │ │ ├── EUDCC+Vaccination+VaccineMarketingAuthorizationHolder.swift │ │ │ └── EUDCC+Vaccination.swift │ │ ├── Test │ │ │ ├── EUDCC+Test+TestType.swift │ │ │ ├── EUDCC+Test+TestResult.swift │ │ │ └── EUDCC+Test.swift │ │ ├── EUDCC+Name.swift │ │ ├── EUDCC.swift │ │ ├── Recovery │ │ │ └── EUDCC+Recovery.swift │ │ └── EUDCC+Codable.swift │ ├── DateFormatter │ │ ├── AnyDateFormatter.swift │ │ ├── EUDCCTimestampFormatter.swift │ │ └── EUDCCDateFormatter.swift │ └── Extensions │ │ └── Codable+DateFormatter.swift ├── EUDCCVerifier │ ├── TrustService │ │ ├── EUDCCTrustService.swift │ │ ├── Implementations │ │ │ ├── EUCentralEUDCCTrustService.swift │ │ │ └── RobertKochInstituteEUDCCTrustService.swift │ │ └── GroupableEUDCCTrustService.swift │ ├── Extensions │ │ ├── EUDCC+Verify.swift │ │ └── Data+encodedASN1.swift │ ├── Models │ │ ├── EUDCC+VerificationCandidate.swift │ │ ├── EUDCC+TrustCertificate.swift │ │ ├── EUDCC+SignedPayload.swift │ │ └── EUDCC+TrustCertificate+KeyID.swift │ └── Verifier │ │ └── EUDCCVerifier.swift ├── EUDCCValidator │ ├── Extensions │ │ └── EUDCC+Validate.swift │ ├── Models │ │ ├── EUDCC+ValidationRule+If.swift │ │ ├── EUDCC+ValidationRule+ComparisonOperators.swift │ │ ├── EUDCC+ValidationRule+Tag.swift │ │ ├── EUDCC+ValidationRule+LogicalOperators.swift │ │ ├── EUDCC+ValidationRule.swift │ │ ├── EUDCC+ValidationRule+CompareAgainst.swift │ │ └── EUDCC+ValidationRule+Defaults.swift │ └── Validator │ │ └── EUDCCValidator.swift └── EUDCCDecoder │ ├── Extension │ ├── EUDCC+Decode.swift │ ├── Data+Compression.swift │ ├── Data+Base45.swift │ └── CBOR+DictionaryRepresentation.swift │ └── Decoder │ └── EUDCCDecoder.swift ├── Package.resolved ├── .github └── ISSUE_TEMPLATE │ ├── feature-request.md │ └── bug-report.md ├── LICENSE ├── Package.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/ 8 | -------------------------------------------------------------------------------- /Tests/EUDCCTests/EUDCCTests.swift: -------------------------------------------------------------------------------- 1 | import EUDCCKitTests 2 | @testable import EUDCC 3 | 4 | final class EUDCCTests: EUDCCKitTests { 5 | } 6 | -------------------------------------------------------------------------------- /Tests/EUDCCVerifierTests/EUDCCVerifierTests.swift: -------------------------------------------------------------------------------- 1 | import EUDCCKitTests 2 | @testable import EUDCCVerifier 3 | 4 | final class EUDCCVerifierTests: EUDCCKitTests { 5 | } 6 | -------------------------------------------------------------------------------- /Tests/EUDCCValidatorTests/EUDCCValidatorTests.swift: -------------------------------------------------------------------------------- 1 | import EUDCCKitTests 2 | @testable import EUDCCValidator 3 | 4 | final class EUDCCValidatorTests: EUDCCKitTests { 5 | } 6 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/EUDCC+Content.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Content 4 | 5 | public extension EUDCC { 6 | 7 | /// The EUDCC Content 8 | enum Content: Hashable { 9 | /// Vaccination 10 | case vaccination(Vaccination) 11 | /// Test 12 | case test(Test) 13 | /// Recovery 14 | case recovery(Recovery) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SwiftCBOR", 6 | "repositoryURL": "https://github.com/unrelentingtech/SwiftCBOR.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "668c26fc3373d5f1bccbaad7665ca6048797e324", 10 | "version": "0.4.3" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Sources/EUDCCVerifier/TrustService/EUDCCTrustService.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - EUDCCTrustService 5 | 6 | /// An EUDCC TrustService 7 | public protocol EUDCCTrustService { 8 | 9 | /// Retrieve EUDCC TrustCertificates 10 | /// - Parameter completion: The completion closure 11 | func getTrustCertificates( 12 | completion: @escaping (Result<[EUDCC.TrustCertificate], Error>) -> Void 13 | ) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for EUDCCKit 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Motivation 11 | > ℹ Please replace this with your motivation. For example if your feature request is related to a problem. 12 | 13 | # Solution 14 | > ℹ Please replace this with your proposed solution. 15 | 16 | # Additional context 17 | > ℹ Please replace this with any other context or screenshots about your feature request (optional). 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## EUDCCKit Environment 11 | 12 | - EUDCCKit version: 13 | - macOS version: 14 | - Xcode version: 15 | - Dependency manager (Carthage, CocoaPods, SPM, Manually): 16 | 17 | ## What did you do? 18 | 19 | > ℹ Please replace this with what you did. 20 | 21 | ## What did you expect to happen? 22 | 23 | > ℹ Please replace this with what you expected to happen. 24 | 25 | ## What happened instead? 26 | 27 | > ℹ Please replace this with of what happened instead. 28 | -------------------------------------------------------------------------------- /Sources/EUDCCVerifier/Extensions/EUDCC+Verify.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - EUDCC+verify 5 | 6 | public extension EUDCC { 7 | 8 | /// Verify `EUDCC` using an `EUDCCVerifier` 9 | /// - Parameters: 10 | /// - verifier: The EUDCCVerifier 11 | /// - completion: The verification completion closure 12 | func verify( 13 | using verifier: EUDCCVerifier, 14 | completion: @escaping (EUDCCVerifier.VerificationResult) -> Void 15 | ) { 16 | verifier.verify( 17 | eudcc: self, 18 | completion: completion 19 | ) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Sources/EUDCCValidator/Extensions/EUDCC+Validate.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - EUDCC+validate 5 | 6 | public extension EUDCC { 7 | 8 | /// Valide `EUDCC` using a `ValidationRule` 9 | /// - Parameters: 10 | /// - rule: The ValidationRule that hsould be used to value the EUDCC. Default value `.default` 11 | /// - validator: The EUDCCValidator. Default value `.init()` 12 | /// - Returns: The ValidationResult 13 | func validate( 14 | rule: EUDCC.ValidationRule = .default, 15 | using validator: EUDCCValidator = .init() 16 | ) -> EUDCCValidator.ValidationResult { 17 | validator.validate( 18 | eudcc: self, 19 | rule: rule 20 | ) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Sources/EUDCCDecoder/Extension/EUDCC+Decode.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - Convenience Decode 5 | 6 | public extension EUDCC { 7 | 8 | /// Decode `EUDCC` from EUDCC String representation 9 | /// - Parameters: 10 | /// - eudccStringRepresentation: The EUDCC String representation 11 | /// - decoder: The `EUDCCDecoder`. Default value `.init()` 12 | /// - Returns: A Result contains either the successfully decoded EUDCC or an DecodingError 13 | static func decode( 14 | from eudccStringRepresentation: String, 15 | using decoder: EUDCCDecoder = .init() 16 | ) -> Result { 17 | decoder.decode( 18 | from: eudccStringRepresentation 19 | ) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Sources/EUDCC/DateFormatter/AnyDateFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - AnyDateFormatter 4 | 5 | /// Protocol acting as a common API for all types of date formatters 6 | protocol AnyDateFormatter { 7 | 8 | /// The Formatter Input. Default value `Date` 9 | associatedtype Input = Date 10 | 11 | /// Format a given Input into a Date 12 | /// - Parameter string: The String to format 13 | func date(from input: Input) -> Date? 14 | 15 | /// Format a Date into the Input 16 | /// - Parameter date: The Date to format 17 | func string(from date: Date) -> Input 18 | 19 | } 20 | 21 | // MARK: - DateFormatter+AnyDateFormatter 22 | 23 | extension DateFormatter: AnyDateFormatter {} 24 | 25 | // MARK: - ISO8601DateFormatter+AnyDateFormatter 26 | 27 | extension ISO8601DateFormatter: AnyDateFormatter {} 28 | -------------------------------------------------------------------------------- /Sources/EUDCC/DateFormatter/EUDCCTimestampFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - EUDCCTimestampFormatter 4 | 5 | /// A EUDCC Timestamp Formatter 6 | final class EUDCCTimestampFormatter: AnyDateFormatter { 7 | 8 | // MARK: Static-Properties 9 | 10 | /// The default `EUDCCTimestampFormatter` instance 11 | static let `default` = EUDCCTimestampFormatter() 12 | 13 | // MARK: AnyDateFormatter 14 | 15 | /// Format a given Input into a Date 16 | /// - Parameter string: The String to format 17 | func date(from input: Int) -> Date? { 18 | .init(timeIntervalSince1970: .init(input)) 19 | } 20 | 21 | /// Format a Date into the Input 22 | /// - Parameter date: The Date to format 23 | func string(from date: Date) -> Int { 24 | .init(date.timeIntervalSince1970) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Sources/EUDCCValidator/Models/EUDCC+ValidationRule+If.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - ValidationRule+if 5 | 6 | public extension EUDCC.ValidationRule { 7 | 8 | /// Conditionally execute a ValidationRule 9 | /// - Parameters: 10 | /// - condition: The ValidationRule that will be evaluated 11 | /// - then: The then ValidationRule 12 | /// - else: The optional ValidationRule 13 | static func `if`( 14 | _ condition: Self, 15 | then: Self, 16 | else: Self 17 | ) -> Self { 18 | .init( 19 | tag: """ 20 | if \(condition.tag) { 21 | \(then.tag) 22 | } else { 23 | \(`else`.tag) 24 | } 25 | """ 26 | ) { eudcc in 27 | condition(eudcc) ? then(eudcc) : `else`(eudcc) 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Sources/EUDCC/DateFormatter/EUDCCDateFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - EUDCCDateFormatter 4 | 5 | /// The EUDCCDateFormatter 6 | final class EUDCCDateFormatter: ISO8601DateFormatter { 7 | 8 | // MARK: Static-Properties 9 | 10 | /// The default `EUDCCDateFormatter` instance 11 | static let `default` = EUDCCDateFormatter() 12 | 13 | /// The Date-Only `ISO8601DateFormatter` instance 14 | private static let dateFormatter: ISO8601DateFormatter = { 15 | let formatter = ISO8601DateFormatter() 16 | formatter.formatOptions.remove([.withTime, .withTimeZone]) 17 | return formatter 18 | }() 19 | 20 | // MARK: Date from String 21 | 22 | /// Format Date from String 23 | /// - Parameter string: The Date String 24 | override func date(from string: String) -> Date? { 25 | super.date(from: string) 26 | ?? Self.dateFormatter.date(from: string) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Sven Tiigi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Tests/EUDCCDecoderTests/EUDCCDecoderTests.swift: -------------------------------------------------------------------------------- 1 | import EUDCCKitTests 2 | @testable import EUDCCDecoder 3 | 4 | final class EUDCCDecoderTests: EUDCCKitTests { 5 | 6 | func testDecode() throws { 7 | let decodingResult = EUDCCDecoder().decode( 8 | from: self.validEUDCCBase45Representation 9 | ) 10 | try XCTAssertNoThrow(decodingResult.get()) 11 | } 12 | 13 | func testDecodeFailure() { 14 | let decodingResult = EUDCCDecoder().decode( 15 | from: UUID().uuidString 16 | ) 17 | switch decodingResult { 18 | case .success: 19 | XCTFail("DecodingResult must be a failure") 20 | case .failure: 21 | break 22 | } 23 | } 24 | 25 | func testEncode() throws { 26 | let decodingResult = EUDCCDecoder().decode( 27 | from: self.validEUDCCBase45Representation 28 | ) 29 | guard case .success(let eudcc) = decodingResult else { 30 | return XCTFail("\(decodingResult)") 31 | } 32 | let data = try JSONEncoder().encode(eudcc) 33 | let decodedEUDCC = try JSONDecoder().decode(EUDCC.self, from: data) 34 | XCTAssert(decodedEUDCC == eudcc) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Sources/EUDCCDecoder/Extension/Data+Compression.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Compression 3 | 4 | // MARK: - Data+decompress 5 | 6 | extension Data { 7 | 8 | /// Retrieve a decompressed representation of the current Data object 9 | /// - Parameter algorithm: The Compression Algorithm that should be used. Default value `COMPRESSION_ZLIB` 10 | func decompressed( 11 | algorithm: compression_algorithm = COMPRESSION_ZLIB 12 | ) -> Self { 13 | guard self.count > 2 else { 14 | return self 15 | } 16 | let size = 4 * self.count + 8 * 1024 17 | let buffer = UnsafeMutablePointer.allocate(capacity: size) 18 | let result = self.subdata(in: 2 ..< self.count).withUnsafeBytes { 19 | let read = Compression.compression_decode_buffer( 20 | buffer, 21 | size, 22 | $0.baseAddress!.bindMemory(to: UInt8.self, capacity: 1), 23 | self.count - 2, 24 | nil, 25 | algorithm 26 | ) 27 | return .init( 28 | bytes: buffer, 29 | count: read 30 | ) 31 | } as Data 32 | buffer.deallocate() 33 | return result 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Sources/EUDCCValidator/Models/EUDCC+ValidationRule+ComparisonOperators.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - ValidationRule+Equal 5 | 6 | public extension EUDCC.ValidationRule { 7 | 8 | /// Returns a ValidationRule where two given ValidationRules results will be compared to equality 9 | /// - Parameters: 10 | /// - lhs: The left-hand side of the operation 11 | /// - rhs: The right-hand side of the operation 12 | static func == ( 13 | lhs: Self, 14 | rhs: Self 15 | ) -> Self { 16 | .init(tag: "(\(lhs.tag) == \(rhs.tag))") { eudcc in 17 | lhs(eudcc) == rhs(eudcc) 18 | } 19 | } 20 | 21 | } 22 | 23 | // MARK: - ValidationRule+Unequal 24 | 25 | public extension EUDCC.ValidationRule { 26 | 27 | /// Returns a ValidationRule where two given ValidationRule results will be compared to unequality 28 | /// - Parameters: 29 | /// - lhs: The left-hand side of the operation 30 | /// - rhs: The right-hand side of the operation 31 | static func != ( 32 | lhs: Self, 33 | rhs: Self 34 | ) -> Self { 35 | .init(tag: "(\(lhs.tag) != \(rhs.tag))") { eudcc in 36 | lhs(eudcc) == rhs(eudcc) 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /Sources/EUDCCVerifier/Models/EUDCC+VerificationCandidate.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - EUDCC+VerificationCandidate 5 | 6 | public extension EUDCC { 7 | 8 | /// An EUDCC Verification Candidate 9 | struct VerificationCandidate: Hashable { 10 | 11 | // MARK: Properties 12 | 13 | /// The Signature 14 | public let signature: Data 15 | 16 | /// The SignedPayload 17 | public let signedPayload: SignedPayload 18 | 19 | /// The TrustCertificate 20 | public let trustCertificate: TrustCertificate 21 | 22 | // MARK: Initializer 23 | 24 | /// Creates a new instance of `EUDCC.VerificationCandidate` 25 | /// - Parameters: 26 | /// - signature: The EUDCC Signature 27 | /// - signedPayload: The EUDCC SignedPayload 28 | /// - trustCertificate: The EUDCC TrustCertificate 29 | public init( 30 | signature: Data, 31 | signedPayload: SignedPayload, 32 | trustCertificate: TrustCertificate 33 | ) { 34 | self.signature = signature 35 | self.signedPayload = signedPayload 36 | self.trustCertificate = trustCertificate 37 | } 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Sources/EUDCCValidator/Models/EUDCC+ValidationRule+Tag.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - Tag 5 | 6 | public extension EUDCC.ValidationRule { 7 | 8 | /// An EUDCC ValidationRule Tag 9 | struct Tag: Codable, Hashable { 10 | 11 | // MARK: Properties 12 | 13 | /// The tag name 14 | public let name: String 15 | 16 | // MARK: Initializer 17 | 18 | /// Creates a new instance of `EUDCC.ValidationRule.Tag` 19 | /// - Parameter name: The tag name. Default value `UUID` 20 | public init( 21 | name: String = UUID().uuidString 22 | ) { 23 | self.name = name 24 | } 25 | 26 | } 27 | 28 | } 29 | 30 | // MARK: - CustomStringConvertible 31 | 32 | extension EUDCC.ValidationRule.Tag: CustomStringConvertible { 33 | 34 | /// A textual representation of this instance. 35 | public var description: String { 36 | self.name 37 | } 38 | 39 | } 40 | 41 | // MARK: - ExpressibleByStringLiteral 42 | 43 | extension EUDCC.ValidationRule.Tag: ExpressibleByStringInterpolation { 44 | 45 | /// Creates an instance initialized to the given string value. 46 | /// - Parameter value: The value of the new instance. 47 | public init( 48 | stringLiteral value: String 49 | ) { 50 | self.init(name: value) 51 | } 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /Sources/EUDCCVerifier/Models/EUDCC+TrustCertificate.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | import Security 4 | 5 | // MARK: - EUDCC+TrustCertificate 6 | 7 | public extension EUDCC { 8 | 9 | /// An EUDCC TrustCertificate 10 | struct TrustCertificate: Codable, Hashable { 11 | 12 | // MARK: Properties 13 | 14 | /// The KeyID 15 | public let keyID: KeyID 16 | 17 | /// The contents of the certificate 18 | public let contents: String 19 | 20 | // MARK: Initializer 21 | 22 | /// Creates a new instance of `EUDCC.TrustCertificate` 23 | /// - Parameters: 24 | /// - keyID: The KeyID 25 | /// - contents: The contents of the certificate 26 | public init( 27 | keyID: KeyID, 28 | contents: String 29 | ) { 30 | self.keyID = keyID 31 | self.contents = contents 32 | } 33 | 34 | } 35 | 36 | } 37 | 38 | // MARK: - PublicKey 39 | 40 | public extension EUDCC.TrustCertificate { 41 | 42 | /// The PublicKey from SignerCertificate contents if available 43 | var publicKey: Security.SecKey? { 44 | Data( 45 | base64Encoded: self.contents 46 | ) 47 | .flatMap { Security.SecCertificateCreateWithData(nil, $0 as CFData) } 48 | .flatMap(Security.SecCertificateCopyKey) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/EUDCC+CryptographicSignature.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - CryptographicSignature 4 | 5 | public extension EUDCC { 6 | 7 | /// The EUDCC CryptographicSignature 8 | struct CryptographicSignature: Codable, Hashable { 9 | 10 | // MARK: Properties 11 | 12 | /// The protected parameter 13 | public let protected: Data 14 | 15 | /// The unprotected parameter 16 | public let unprotected: [Data: Data] 17 | 18 | /// The payload parameter 19 | public let payload: Data 20 | 21 | /// The signature parameter 22 | public let signature: Data 23 | 24 | // MARK: Initializer 25 | 26 | /// Creates a new instance of `EUDCC.CryptographicSignature` 27 | /// - Parameters: 28 | /// - protected: The protected paramter 29 | /// - unprotected: The unprotected paramter 30 | /// - payload: The payload parameter 31 | /// - signature: The signature parameter 32 | public init( 33 | protected: Data, 34 | unprotected: [Data: Data], 35 | payload: Data, 36 | signature: Data 37 | ) { 38 | self.protected = protected 39 | self.unprotected = unprotected 40 | self.payload = payload 41 | self.signature = signature 42 | } 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/EUDCCVerifier/Models/EUDCC+SignedPayload.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | import SwiftCBOR 4 | 5 | // MARK: - SignedPayload 6 | 7 | public extension EUDCC { 8 | 9 | /// The EUDCC SignedPayload 10 | struct SignedPayload: Codable, Hashable { 11 | 12 | // MARK: Properties 13 | 14 | /// The signed payload data raw value 15 | public let rawValue: Data 16 | 17 | // MARK: Initializer 18 | 19 | /// Creates a new instance of `EUDCC.SignedPayload` 20 | /// - Parameters: 21 | /// - prefix: The SignedPayload prefix. Default value `Signature1` 22 | /// - cryptographicSignature: The EUDCC CryptographicSignature 23 | public init( 24 | prefix: String = "Signature1", 25 | cryptographicSignature: EUDCC.CryptographicSignature 26 | ) { 27 | self.rawValue = .init( 28 | SwiftCBOR.CBOR.encode( 29 | [ 30 | .init(stringLiteral: prefix), 31 | SwiftCBOR.CBOR 32 | .byteString(.init(cryptographicSignature.protected)), 33 | SwiftCBOR.CBOR 34 | .byteString(.init()), 35 | SwiftCBOR.CBOR 36 | .byteString(.init(cryptographicSignature.payload)) 37 | ] 38 | ) 39 | ) 40 | } 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /Sources/EUDCCValidator/Models/EUDCC+ValidationRule+LogicalOperators.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - ValidationRule+Not 5 | 6 | public extension EUDCC.ValidationRule { 7 | 8 | /// Performs a logical `NOT` (`!`) operation on a ValidationRule 9 | /// - Parameter rule: The ValidationRule to negate 10 | static prefix func ! ( 11 | rule: EUDCC.ValidationRule 12 | ) -> Self { 13 | .init(tag: "!(\(rule.tag))") { eudcc in 14 | !rule(eudcc) 15 | } 16 | } 17 | 18 | } 19 | 20 | // MARK: - ValidationRule+And 21 | 22 | public extension EUDCC.ValidationRule { 23 | 24 | /// Performs a logical `AND` (`&&`) operation on two ValidationRules 25 | /// - Parameters: 26 | /// - lhs: The left-hand side of the operation 27 | /// - rhs: The right-hand side of the operation 28 | static func && ( 29 | lhs: Self, 30 | rhs: Self 31 | ) -> Self { 32 | .init(tag: "(\(lhs.tag) && \(rhs.tag))") { eudcc in 33 | lhs(eudcc) && rhs(eudcc) 34 | } 35 | } 36 | 37 | } 38 | 39 | // MARK: - ValidationRule+Or 40 | 41 | public extension EUDCC.ValidationRule { 42 | 43 | /// Performs a logical `OR` (`||`) operation on two ValidationRules 44 | /// - Parameters: 45 | /// - lhs: The left-hand side of the operation 46 | /// - rhs: The right-hand side of the operation 47 | static func || ( 48 | lhs: Self, 49 | rhs: Self 50 | ) -> Self { 51 | .init(tag: "(\(lhs.tag) || \(rhs.tag))") { eudcc in 52 | lhs(eudcc) || rhs(eudcc) 53 | } 54 | } 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/EUDCC+Country.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Country 4 | 5 | public extension EUDCC { 6 | 7 | /// The EUDCC Country 8 | struct Country: Hashable { 9 | 10 | // MARK: Properties 11 | 12 | /// The string value 13 | public let value: String 14 | 15 | // MARK: Initializer 16 | 17 | /// Creates a new instance of `EUDCC.Test.Country` 18 | /// - Parameter value: The string value 19 | public init(value: String) { 20 | self.value = value 21 | } 22 | 23 | } 24 | 25 | } 26 | 27 | // MARK: - Localized String 28 | 29 | public extension EUDCC.Country { 30 | 31 | /// Localized string of Country 32 | /// - Parameter locale: The Locale. Default value `.current` 33 | func localizedString( 34 | locale: Locale = .current 35 | ) -> String? { 36 | locale.localizedString( 37 | forRegionCode: self.value 38 | ) 39 | } 40 | 41 | } 42 | 43 | // MARK: - Codable 44 | 45 | extension EUDCC.Country: Codable { 46 | 47 | /// Creates a new instance by decoding from the given decoder. 48 | /// - Parameter decoder: The decoder to read data from. 49 | public init(from decoder: Decoder) throws { 50 | let container = try decoder.singleValueContainer() 51 | self.value = try container.decode(String.self) 52 | } 53 | 54 | /// Encodes this value into the given encoder. 55 | /// - Parameter encoder: The encoder to write data to. 56 | public func encode(to encoder: Encoder) throws { 57 | var container = encoder.singleValueContainer() 58 | try container.encode(self.value) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /Sources/EUDCC/Extensions/Codable+DateFormatter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - KeyedDecodingContainerProtocol+decode 4 | 5 | extension KeyedDecodingContainerProtocol { 6 | 7 | /// Decodes a Date for the given key using a DateFormatter 8 | /// - Parameters: 9 | /// - key: The Key 10 | /// - dateFormatter: The DateFormatter 11 | func decode( 12 | forKey key: Key, 13 | using dateFormatter: DateFormatter 14 | ) throws -> Date where DateFormatter.Input: Decodable { 15 | let dateString = try self.decode(DateFormatter.Input.self, forKey: key) 16 | guard let date = dateFormatter.date(from: dateString) else { 17 | throw DecodingError.dataCorruptedError( 18 | forKey: key, 19 | in: self, 20 | debugDescription: [ 21 | String(describing: DateFormatter.self), 22 | "unable to format date string", 23 | "\(dateString)" 24 | ].joined(separator: " ") 25 | ) 26 | } 27 | return date 28 | } 29 | 30 | } 31 | 32 | // MARK: - KeyedEncodingContainerProtocol+encodeEUDCCDate 33 | 34 | extension KeyedEncodingContainerProtocol { 35 | 36 | /// Encodes a Date for the goven key using a DateFormatter 37 | /// - Parameters: 38 | /// - date: The Date 39 | /// - key: The Key 40 | /// - dateFormatter: The DateFormatter 41 | mutating func encode( 42 | _ date: Date, 43 | forKey key: Key, 44 | using dateFormatter: DateFormatter 45 | ) throws where DateFormatter.Input: Encodable { 46 | try self.encode( 47 | dateFormatter.string(from: date), 48 | forKey: key 49 | ) 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/EUDCC+DiseaseAgentTargeted.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - DiseaseAgentTargeted 4 | 5 | public extension EUDCC { 6 | 7 | /// The EUDCC disease or agent targeted 8 | struct DiseaseAgentTargeted: Hashable { 9 | 10 | // MARK: Properties 11 | 12 | /// The string value 13 | public let value: String 14 | 15 | // MARK: Initializer 16 | 17 | /// Creates a new instance of `EUDCC.DiseaseAgentTargeted` 18 | /// - Parameter value: The string value 19 | public init(value: String) { 20 | self.value = value 21 | } 22 | 23 | } 24 | 25 | } 26 | 27 | // MARK: - WellKnownValue 28 | 29 | public extension EUDCC.DiseaseAgentTargeted { 30 | 31 | /// The WellKnownValue 32 | enum WellKnownValue: String, Codable, Hashable, CaseIterable { 33 | /// COVID-19 34 | case covid19 = "840539006" 35 | } 36 | 37 | /// The WellKnownValue if available 38 | var wellKnownValue: WellKnownValue? { 39 | .init(rawValue: self.value) 40 | } 41 | 42 | } 43 | 44 | // MARK: - Codable 45 | 46 | extension EUDCC.DiseaseAgentTargeted: Codable { 47 | 48 | /// Creates a new instance by decoding from the given decoder. 49 | /// - Parameter decoder: The decoder to read data from. 50 | public init(from decoder: Decoder) throws { 51 | let container = try decoder.singleValueContainer() 52 | self.value = try container.decode(String.self) 53 | } 54 | 55 | /// Encodes this value into the given encoder. 56 | /// - Parameter encoder: The encoder to write data to. 57 | public func encode(to encoder: Encoder) throws { 58 | var container = encoder.singleValueContainer() 59 | try container.encode(self.value) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Sources/EUDCCDecoder/Extension/Data+Base45.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Data+Base45 4 | 5 | extension Data { 6 | 7 | /// The Base45 Encoding Error 8 | enum Base45EncodingError: Error { 9 | /// Invalid Character 10 | case invalidCharacter(Character) 11 | /// Invalid length 12 | case invalidLength 13 | /// Data overflow 14 | case dataOverflow 15 | } 16 | 17 | /// Initialize a `Data` from a Base-45 encoded String 18 | /// - Parameter base45String: The Base-45 encoded String 19 | /// - Throws: If decoding fails 20 | init( 21 | base45Encoded base45String: String 22 | ) throws { 23 | let base45Charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:" 24 | var base45EncodedData = Data() 25 | var base45DecodedData = Data() 26 | for character in base45String.uppercased() { 27 | guard let characterIndex = base45Charset.firstIndex(of: character) else { 28 | throw Base45EncodingError.invalidCharacter(character) 29 | } 30 | let index = base45Charset.distance(from: base45Charset.startIndex, to: characterIndex) 31 | base45EncodedData.append(UInt8(index)) 32 | } 33 | for index in stride(from:0, to: base45EncodedData.count, by: 3) { 34 | if base45EncodedData.count - index < 2 { 35 | throw Base45EncodingError.invalidLength 36 | } 37 | var x = UInt32(base45EncodedData[index]) + UInt32(base45EncodedData[index + 1]) * 45 38 | if base45EncodedData.count - index >= 3 { 39 | x += 45 * 45 * .init(base45EncodedData[index + 2]) 40 | guard x / 256 <= UInt8.max else { 41 | throw Base45EncodingError.dataOverflow 42 | } 43 | base45DecodedData.append(.init(x / 256)) 44 | } 45 | base45DecodedData.append(.init(x % 256)) 46 | } 47 | self = base45DecodedData 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /Tests/_EUDCCKitTests/EUDCCKitTests.swift: -------------------------------------------------------------------------------- 1 | // MARK: - Exported Imports 2 | 3 | @_exported import EUDCC 4 | @_exported import EUDCCDecoder 5 | @_exported import EUDCCValidator 6 | @_exported import EUDCCVerifier 7 | @_exported import XCTest 8 | 9 | // MARK: - EUDCCKitTests 10 | 11 | /// An EUDCCKitTests acting as a base/common TestCase 12 | open class EUDCCKitTests: XCTestCase { 13 | 14 | // MARK: Properties 15 | 16 | /// The valid EUDCC Base-45 representation 17 | public let validEUDCCBase45Representation = "HC1:NCFP70M90T9WTWGVLKJ99K83X4C8DTTMMX*4DBB3XK4F3A:OK.G2F3K*S7Y0/IC6TAY50.FK6ZK7:EDOLFVC*70B$D% D3IA4W5646946846.966KCN9E%961A6DL6FA7D46XJCCWENF6OF63W5KF60A6WJCT3EHS8WJC0FDTA6AIA%G7X+AQB9746IG77TA$96T476:6/Q6M*8CR63Y8R46WX8F46VL6/G8SF6DR64S8+96QK4.JCP9EJY8L/5M/5546.96VF6%JC QE/IAYJC5LEW34U3ET7DXC9 QE-ED8%E3KC.SC4KCD3DX47B46IL6646I*6..DX%DLPCG/DI C+0AD1AZJC1/D/IA:JC5WEI3D4WE*Y9 JC/.DQZ9$PC5$CUZCY$5Y$5JPCT3E5JDNA79%6F464W5%:6378POQUVQ2XNBOKE9V3DRXF3FW01Q5 EC EQ-*MJ94T-12AT$3LR3T: HZEBAQ7PDWT38Y53%VHW8G-4EP3R4$M40SD99C+0.YUC-V%3" 18 | 19 | // MARK: XCTestCase-Lifecycle 20 | 21 | /// SetUp 22 | open override func setUp() { 23 | super.setUp() 24 | // Disable continueAfterFailure 25 | self.continueAfterFailure = false 26 | } 27 | 28 | // MARK: Helper-Functions 29 | 30 | /// Perform Test 31 | /// - Parameters: 32 | /// - name: The name of the test 33 | /// - timeout: The timeout 34 | /// - test: The test execution 35 | public final func performTest( 36 | name: String = "\(#file) L\(#line):\(#column) \(#function)", 37 | timeout: TimeInterval = 10, 38 | test: (XCTestExpectation) -> Void 39 | ) { 40 | // Create expectation with function name 41 | let expectation = self.expectation(description: name) 42 | // Perform test with expectation 43 | test(expectation) 44 | // Wait for expectation been fulfilled with custom or default timeout 45 | self.waitForExpectations( 46 | timeout: timeout, 47 | handler: nil 48 | ) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/Vaccination/EUDCC+Vaccination+VaccineOrProphylaxis.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - VaccineOrProphylaxis 4 | 5 | public extension EUDCC.Vaccination { 6 | 7 | /// The EUDCC vaccination type of the vaccine or prophylaxis used. 8 | struct VaccineOrProphylaxis: Hashable { 9 | 10 | // MARK: Properties 11 | 12 | /// The string value 13 | public let value: String 14 | 15 | // MARK: Initializer 16 | 17 | /// Creates a new instance of `EUDCC.Vaccination.VaccineOrProphylaxis` 18 | /// - Parameter value: The string value 19 | public init(value: String) { 20 | self.value = value 21 | } 22 | 23 | } 24 | 25 | } 26 | 27 | // MARK: - WellKnownValue 28 | 29 | public extension EUDCC.Vaccination.VaccineOrProphylaxis { 30 | 31 | /// The WellKnownValue 32 | enum WellKnownValue: String, Codable, Hashable, CaseIterable { 33 | /// SARS-CoV-2 mRNA vaccine 34 | case sarsCoV2mRNAVaccine = "1119349007" 35 | /// SARS-CoV-2 antigen Vaccine 36 | case sarsCoV2AntigenVaccine = "1119305005" 37 | /// COVID-19 vaccines 38 | case covid19Vaccines = "J07BX03" 39 | } 40 | 41 | /// The WellKnownValue if available 42 | var wellKnownValue: WellKnownValue? { 43 | .init(rawValue: self.value) 44 | } 45 | 46 | } 47 | 48 | // MARK: - Codable 49 | 50 | extension EUDCC.Vaccination.VaccineOrProphylaxis: Codable { 51 | 52 | /// Creates a new instance by decoding from the given decoder. 53 | /// - Parameter decoder: The decoder to read data from. 54 | public init(from decoder: Decoder) throws { 55 | let container = try decoder.singleValueContainer() 56 | self.value = try container.decode(String.self) 57 | } 58 | 59 | /// Encodes this value into the given encoder. 60 | /// - Parameter encoder: The encoder to write data to. 61 | public func encode(to encoder: Encoder) throws { 62 | var container = encoder.singleValueContainer() 63 | try container.encode(self.value) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /Sources/EUDCCVerifier/Extensions/Data+encodedASN1.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Data+encodedASN1 4 | 5 | extension Data { 6 | 7 | /// Encode Data as ASN.1 8 | /// - Parameter digestLengthInBytes: The digest length in bytes. Default value `32` 9 | /// - Returns: The ASN.1 encoded Data if available 10 | func encodedASN1( 11 | digestLengthInBytes: Int = 32 12 | ) -> Data? { 13 | func encodeInt(_ data: [UInt8]) -> [UInt8] { 14 | guard !data.isEmpty else { 15 | return data 16 | } 17 | let firstBitIsSet: UInt8 = 0b10000000 18 | let tagInteger: UInt8 = 0x02 19 | if data[0] >= firstBitIsSet { 20 | return [tagInteger, UInt8(data.count + 1)] + [0] + data 21 | } else if data.first == 0x00 { 22 | return encodeInt([UInt8](data.dropFirst())) 23 | } else { 24 | return [tagInteger, UInt8(data.count)] + data 25 | } 26 | } 27 | func length(_ num: Int) -> [UInt8] { 28 | var bits = 0 29 | var numBits = num 30 | while numBits > 0 { 31 | numBits = numBits >> 1 32 | bits += 1 33 | } 34 | var bytes: [UInt8] = .init() 35 | var num = num 36 | while num > 0 { 37 | bytes += [UInt8(num & 0b11111111)] 38 | num = num >> 8 39 | } 40 | return [0b10000000 + UInt8((bits - 1) / 8 + 1)] + bytes.reversed() 41 | } 42 | let data = [UInt8](self) 43 | guard data.count > digestLengthInBytes else { 44 | return nil 45 | } 46 | let sigR = encodeInt([UInt8](data.prefix(data.count - digestLengthInBytes))) 47 | let sigS = encodeInt([UInt8](data.suffix(digestLengthInBytes))) 48 | let tagSequence: UInt8 = 0x30 49 | if sigR.count + sigS.count < 128 { 50 | return .init([tagSequence, UInt8(sigR.count + sigS.count)] + sigR + sigS) 51 | } else { 52 | return .init([tagSequence] + length(sigR.count + sigS.count) + sigR + sigS) 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Sources/EUDCCValidator/Validator/EUDCCValidator.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - EUDCCValidator 5 | 6 | /// An EUDCC Validator 7 | public struct EUDCCValidator { 8 | 9 | /// Creates a new instance of `EUDCCValidator` 10 | public init() {} 11 | 12 | } 13 | 14 | // MARK: - ValidationResult 15 | 16 | public extension EUDCCValidator { 17 | 18 | /// The ValidationResult 19 | typealias ValidationResult = Result 20 | 21 | } 22 | 23 | // MARK: - Failure 24 | 25 | public extension EUDCCValidator { 26 | 27 | /// An EUDCCValidator Failure 28 | struct Failure: LocalizedError { 29 | 30 | // MARK: Properties 31 | 32 | /// The unsatisfied ValidationRule 33 | public let unsatisfiedRule: EUDCC.ValidationRule 34 | 35 | /// The failure reason 36 | public var failureReason: String? { 37 | self.unsatisfiedRule.tag.name 38 | } 39 | 40 | // MARK: Initializer 41 | 42 | /// Creates a new instance of `EUDCCValidator.Failure` 43 | /// - Parameter unsatisfiedRule: The unsatisfied ValidationRule 44 | public init( 45 | unsatisfiedRule: EUDCC.ValidationRule 46 | ) { 47 | self.unsatisfiedRule = unsatisfiedRule 48 | } 49 | 50 | } 51 | 52 | } 53 | 54 | // MARK: - Validate 55 | 56 | public extension EUDCCValidator { 57 | 58 | /// Validate an `EUDCC` using a `ValidationRule` 59 | /// - Parameters: 60 | /// - eudcc: The EUDCC that should be validated 61 | /// - rule: The ValidationRule that should be used to validate the EUDCC. Default value `.default` 62 | /// - Returns: The ValidationResult 63 | func validate( 64 | eudcc: EUDCC, 65 | rule: EUDCC.ValidationRule = .default 66 | ) -> ValidationResult { 67 | // Verify ValidationRule satisfies 68 | guard rule(eudcc) else { 69 | // Otherwise return failure with unsatisfied ValidationRule 70 | return .failure(.init(unsatisfiedRule: rule)) 71 | } 72 | // Return success as ValidationRule succeeded 73 | return .success(()) 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/Test/EUDCC+Test+TestType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - TestType 4 | 5 | public extension EUDCC.Test { 6 | 7 | /// The EUDCC type of test 8 | struct TestType: Hashable { 9 | 10 | // MARK: Properties 11 | 12 | /// The string value 13 | public let value: String 14 | 15 | // MARK: Initializer 16 | 17 | /// Creates a new instance of `EUDCC.Test.TestType` 18 | /// - Parameter value: The string value 19 | public init(value: String) { 20 | self.value = value 21 | } 22 | 23 | } 24 | 25 | } 26 | 27 | // MARK: - WellKnownValue 28 | 29 | public extension EUDCC.Test.TestType { 30 | 31 | /// The WellKnownValue 32 | enum WellKnownValue: String, Codable, Hashable, CaseIterable { 33 | /// Nucleic acid amplification with probe detection 34 | case nucleicACIDAmplificationWithProbeDetection = "LP6464-4" 35 | /// Rapid immunoassay 36 | case rapidImmunoassay = "LP217198-3" 37 | } 38 | 39 | /// The WellKnownValue if available 40 | var wellKnownValue: WellKnownValue? { 41 | .init(rawValue: self.value) 42 | } 43 | 44 | } 45 | 46 | // MARK: - WellKnownValue+Convenience 47 | 48 | public extension EUDCC.Test.TestType.WellKnownValue { 49 | 50 | /// PCR represented by `nucleicACIDAmplificationWithProbeDetection` case 51 | static let pcr: Self = .nucleicACIDAmplificationWithProbeDetection 52 | 53 | } 54 | 55 | // MARK: - Codable 56 | 57 | extension EUDCC.Test.TestType: Codable { 58 | 59 | /// Creates a new instance by decoding from the given decoder. 60 | /// - Parameter decoder: The decoder to read data from. 61 | public init(from decoder: Decoder) throws { 62 | let container = try decoder.singleValueContainer() 63 | self.value = try container.decode(String.self) 64 | } 65 | 66 | /// Encodes this value into the given encoder. 67 | /// - Parameter encoder: The encoder to write data to. 68 | public func encode(to encoder: Encoder) throws { 69 | var container = encoder.singleValueContainer() 70 | try container.encode(self.value) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/Test/EUDCC+Test+TestResult.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - TestResult 4 | 5 | public extension EUDCC.Test { 6 | 7 | /// The EUDCC result of the test 8 | struct TestResult: Hashable { 9 | 10 | // MARK: Properties 11 | 12 | /// The string value 13 | public let value: String 14 | 15 | // MARK: Initializer 16 | 17 | /// Creates a new instance of `EUDCC.Test.TestResult` 18 | /// - Parameter value: The string value 19 | public init(value: String) { 20 | self.value = value 21 | } 22 | 23 | } 24 | 25 | } 26 | 27 | // MARK: - WellKnownValue 28 | 29 | public extension EUDCC.Test.TestResult { 30 | 31 | /// The WellKnownValue 32 | enum WellKnownValue: String, Codable, Hashable, CaseIterable { 33 | /// Not detected 34 | case notDetected = "260415000" 35 | /// Detected 36 | case detected = "260373001" 37 | } 38 | 39 | /// The WellKnownValue if available 40 | var wellKnownValue: WellKnownValue? { 41 | .init(rawValue: self.value) 42 | } 43 | 44 | } 45 | 46 | // MARK: - WellKnownValue+Convenience 47 | 48 | public extension EUDCC.Test.TestResult.WellKnownValue { 49 | 50 | /// Positive TestResult value represented by `detected` case 51 | static let positive: Self = .detected 52 | 53 | /// Negative TestResult value represented by `notDetected` case 54 | static let negative: Self = .notDetected 55 | 56 | } 57 | 58 | // MARK: - Codable 59 | 60 | extension EUDCC.Test.TestResult: Codable { 61 | 62 | /// Creates a new instance by decoding from the given decoder. 63 | /// - Parameter decoder: The decoder to read data from. 64 | public init(from decoder: Decoder) throws { 65 | let container = try decoder.singleValueContainer() 66 | self.value = try container.decode(String.self) 67 | } 68 | 69 | /// Encodes this value into the given encoder. 70 | /// - Parameter encoder: The encoder to write data to. 71 | public func encode(to encoder: Encoder) throws { 72 | var container = encoder.singleValueContainer() 73 | try container.encode(self.value) 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/EUDCC+Name.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Name 4 | 5 | public extension EUDCC { 6 | 7 | /// The EUDCC Person Name 8 | struct Name: Hashable { 9 | 10 | // MARK: Properties 11 | 12 | /// The optional first name 13 | public let firstName: String? 14 | 15 | /// The optional standardised first name 16 | public let standardisedFirstName: String? 17 | 18 | /// The optional last name 19 | public let lastName: String? 20 | 21 | /// The optional standardised last name 22 | public let standardisedLastName: String 23 | 24 | // MARK: Initializer 25 | 26 | /// Creates a new instance of `EUDCC.Person` 27 | /// - Parameters: 28 | /// - firstName: The optional first name. Default value `nil` 29 | /// - standardisedFirstName: The optional standardised first name. Default value `nil` 30 | /// - lastName: The optional last name. Default value `nil` 31 | /// - standardisedLastName: The standardised last name 32 | public init( 33 | firstName: String? = nil, 34 | standardisedFirstName: String? = nil, 35 | lastName: String? = nil, 36 | standardisedLastName: String 37 | ) { 38 | self.firstName = firstName 39 | self.standardisedFirstName = standardisedFirstName 40 | self.lastName = lastName 41 | self.standardisedLastName = standardisedLastName 42 | } 43 | 44 | } 45 | 46 | } 47 | 48 | // MARK: - Formatted 49 | 50 | public extension EUDCC.Name { 51 | 52 | /// Retrieve the formatted full name 53 | /// - Parameters: 54 | /// - formatter: The PersonNameComponentsFormatter. Default value `.init()` 55 | /// - components: The PersonNameComponents. Default value `.init()` 56 | /// - Returns: The formatted full name 57 | func formatted( 58 | using formatter: PersonNameComponentsFormatter = .init(), 59 | components: PersonNameComponents = .init() 60 | ) -> String { 61 | var components = components 62 | components.givenName = self.firstName ?? self.standardisedFirstName 63 | components.familyName = self.lastName ?? self.standardisedLastName 64 | return formatter.string(from: components) 65 | } 66 | 67 | } 68 | 69 | // MARK: - Codable 70 | 71 | extension EUDCC.Name: Codable { 72 | 73 | /// The CodingKeys 74 | private enum CodingKeys: String, CodingKey { 75 | case firstName = "gn" 76 | case standardisedFirstName = "gnt" 77 | case lastName = "fn" 78 | case standardisedLastName = "fnt" 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "EUDCCKit", 7 | platforms: [ 8 | .iOS(.v12), 9 | .tvOS(.v12), 10 | .watchOS(.v5), 11 | .macOS(.v10_15) 12 | ], 13 | products: [ 14 | .library( 15 | name: "EUDCC", 16 | targets: [ 17 | "EUDCC" 18 | ] 19 | ), 20 | .library( 21 | name: "EUDCCDecoder", 22 | targets: [ 23 | "EUDCCDecoder" 24 | ] 25 | ), 26 | .library( 27 | name: "EUDCCVerifier", 28 | targets: [ 29 | "EUDCCVerifier" 30 | ] 31 | ), 32 | .library( 33 | name: "EUDCCValidator", 34 | targets: [ 35 | "EUDCCValidator" 36 | ] 37 | ), 38 | ], 39 | dependencies: [ 40 | .package( 41 | url: "https://github.com/unrelentingtech/SwiftCBOR.git", 42 | .exact("0.4.3") 43 | ) 44 | ], 45 | targets: [ 46 | .target( 47 | name: "EUDCC" 48 | ), 49 | .target( 50 | name: "EUDCCDecoder", 51 | dependencies: [ 52 | "EUDCC", 53 | "SwiftCBOR" 54 | ] 55 | ), 56 | .target( 57 | name: "EUDCCVerifier", 58 | dependencies: [ 59 | "EUDCC", 60 | "SwiftCBOR" 61 | ] 62 | ), 63 | .target( 64 | name: "EUDCCValidator", 65 | dependencies: [ 66 | "EUDCC" 67 | ] 68 | ), 69 | .target( 70 | name: "EUDCCKitTests", 71 | dependencies: [ 72 | "EUDCC", 73 | "EUDCCDecoder", 74 | "EUDCCValidator", 75 | "EUDCCVerifier" 76 | ], 77 | path: "Tests/_EUDCCKitTests" 78 | ), 79 | .testTarget( 80 | name: "EUDCCTests", 81 | dependencies: [ 82 | "EUDCCKitTests" 83 | ] 84 | ), 85 | .testTarget( 86 | name: "EUDCCDecoderTests", 87 | dependencies: [ 88 | "EUDCCKitTests" 89 | ] 90 | ), 91 | .testTarget( 92 | name: "EUDCCVerifierTests", 93 | dependencies: [ 94 | "EUDCCKitTests" 95 | ] 96 | ), 97 | .testTarget( 98 | name: "EUDCCValidatorTests", 99 | dependencies: [ 100 | "EUDCCKitTests" 101 | ] 102 | ) 103 | ] 104 | ) 105 | -------------------------------------------------------------------------------- /Sources/EUDCCVerifier/Models/EUDCC+TrustCertificate+KeyID.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | import SwiftCBOR 4 | 5 | // MARK: - KeyID 6 | 7 | public extension EUDCC.TrustCertificate { 8 | 9 | /// The EUDCC TrustCertificate KeyID 10 | struct KeyID: Codable, Hashable { 11 | 12 | // MARK: Properties 13 | 14 | /// The key id String raw value 15 | public let rawValue: String 16 | 17 | // MARK: Initializer 18 | 19 | /// Creates a new instance of `EUDCC.TrustCertificate.KeyID` 20 | /// - Parameter rawValue: The key id String raw value 21 | public init(rawValue: String) { 22 | self.rawValue = rawValue 23 | } 24 | 25 | } 26 | 27 | } 28 | 29 | // MARK: Convenience Initializer with CryptographicSignature 30 | 31 | public extension EUDCC.TrustCertificate.KeyID { 32 | 33 | /// Creats a new instance of `EUDCC.TrustCertificate.KeyID` 34 | /// - Parameter cryptographicSignature: The EUDCC CryptographicSignature 35 | init?( 36 | cryptographicSignature: EUDCC.CryptographicSignature 37 | ) { 38 | // Decode protected CBOR 39 | let protectedCBOR = try? SwiftCBOR.CBORDecoder( 40 | input: [UInt8](cryptographicSignature.protected) 41 | ).decodeItem() 42 | // Verify protected CBOR Map is available 43 | guard case .map(let protectedCBORMap) = protectedCBOR else { 44 | // Otherwise return nil 45 | return nil 46 | } 47 | // Map unprotected to CBOR Map 48 | let unprotectedCBORMap: [SwiftCBOR.CBOR : SwiftCBOR.CBOR] = Dictionary( 49 | uniqueKeysWithValues: cryptographicSignature 50 | .unprotected 51 | .compactMap { key, value in 52 | // Verify Key and Value can be decoded 53 | guard let key = try? SwiftCBOR.CBORDecoder(input: [UInt8](key)).decodeItem(), 54 | let value = try? SwiftCBOR.CBORDecoder(input: [UInt8](value)).decodeItem() else { 55 | // Otherwise return nil 56 | return nil 57 | } 58 | // Return key and value 59 | return (key, value) 60 | } 61 | ) 62 | // Initialize KID Key 63 | let kidKey = SwiftCBOR.CBOR.unsignedInt(4) 64 | // Retrieve KID CBOR for key either from protected or unprotected CBOR Map 65 | let kidCBOR = protectedCBORMap[kidKey] ?? unprotectedCBORMap[kidKey] 66 | // Verify KID bytes are available 67 | guard case .byteString(let kidBytes) = kidCBOR else { 68 | // Otherwise return nil 69 | return nil 70 | } 71 | // Initialize KID Base-64 encoded string 72 | self.rawValue = Data(kidBytes.prefix(8)).base64EncodedString() 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/Vaccination/EUDCC+Vaccination+VaccineMedicinalProduct.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - VaccineMedicinalProduct 4 | 5 | public extension EUDCC.Vaccination { 6 | 7 | /// The EUDCC vaccination medicinal product used for this specific dose of vaccination 8 | struct VaccineMedicinalProduct: Hashable { 9 | 10 | // MARK: Properties 11 | 12 | /// The string value 13 | public let value: String 14 | 15 | // MARK: Initializer 16 | 17 | /// Creates a new instance of `EUDCC.Vaccination.VaccineMedicinalProduct` 18 | /// - Parameter value: The string value 19 | public init(value: String) { 20 | self.value = value 21 | } 22 | 23 | } 24 | 25 | } 26 | 27 | // MARK: - WellKnownValue 28 | 29 | public extension EUDCC.Vaccination.VaccineMedicinalProduct { 30 | 31 | /// The WellKnownValue 32 | enum WellKnownValue: String, Codable, Hashable, CaseIterable { 33 | /// Comirnaty 34 | case comirnaty = "EU/1/20/1528" 35 | /// COVID-19 Vaccine Moderna 36 | case covid19VaccineModerna = "EU/1/20/1507" 37 | /// Vaxzevria 38 | case vaxzevria = "EU/1/21/1529" 39 | /// COVID-19 Vaccine Janssen 40 | case covid19VaccineJanssen = "EU/1/20/1525" 41 | /// CVnCoV 42 | case cvnCoV = "CVnCoV" 43 | /// Sputnik-V 44 | case sputnikV = "Sputnik-V" 45 | /// Convidecia 46 | case convidecia = "Convidecia" 47 | /// EpiVacCorona 48 | case epiVacCorona = "EpiVacCorona" 49 | /// BBIBP-CorV 50 | case bbibpCorV = "BBIBP-CorV" 51 | /// Inactivated SARS-CoV-2 (Vero Cell) 52 | case inactivatedSARSCoV2VeroCell = "Inactivated-SARS-CoV-2-Vero-Cell" 53 | /// CoronaVac 54 | case coronaVac = "CoronaVac" 55 | /// Covaxin (also known as BBV152 A, B, C 56 | case covaxin = "Covaxin" 57 | } 58 | 59 | /// The WellKnownValue if available 60 | var wellKnownValue: WellKnownValue? { 61 | .init(rawValue: self.value) 62 | } 63 | 64 | } 65 | 66 | // MARK: - WellKnownValue+Convenience 67 | 68 | public extension EUDCC.Vaccination.VaccineMedicinalProduct.WellKnownValue { 69 | 70 | /// AstraZeneca represented by `vaxzevria` case 71 | static let astraZeneca: Self = .vaxzevria 72 | 73 | } 74 | 75 | // MARK: - Codable 76 | 77 | extension EUDCC.Vaccination.VaccineMedicinalProduct: Codable { 78 | 79 | /// Creates a new instance by decoding from the given decoder. 80 | /// - Parameter decoder: The decoder to read data from. 81 | public init(from decoder: Decoder) throws { 82 | let container = try decoder.singleValueContainer() 83 | self.value = try container.decode(String.self) 84 | } 85 | 86 | /// Encodes this value into the given encoder. 87 | /// - Parameter encoder: The encoder to write data to. 88 | public func encode(to encoder: Encoder) throws { 89 | var container = encoder.singleValueContainer() 90 | try container.encode(self.value) 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /Sources/EUDCCValidator/Models/EUDCC+ValidationRule.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - ValidationRule 5 | 6 | public extension EUDCC { 7 | 8 | /// An `EUDCC` ValidationRule 9 | struct ValidationRule { 10 | 11 | // MARK: Typealias 12 | 13 | /// The predicate typealias 14 | public typealias Predicate = (EUDCC) -> Bool 15 | 16 | // MARK: Properties 17 | 18 | /// The Tag 19 | public let tag: Tag 20 | 21 | /// The predicate 22 | let predicate: Predicate 23 | 24 | // MARK: Initializer 25 | 26 | /// Creates a new instance of `EUDCC.ValidationRule` 27 | /// - Parameters: 28 | /// - tag: The Tag. Default value `.init()` 29 | /// - predicate: The predicate closure 30 | public init( 31 | tag: Tag = .init(), 32 | predicate: @escaping Predicate 33 | ) { 34 | self.tag = tag 35 | self.predicate = predicate 36 | } 37 | 38 | // MARK: Call-As-Function 39 | 40 | /// Call `ValidationRule` as function 41 | /// - Parameter eudcc: The EUDCC that should be validated 42 | /// - Returns: The Bool value representing the result 43 | func callAsFunction( 44 | _ eudcc: EUDCC 45 | ) -> Bool { 46 | self.predicate(eudcc) 47 | } 48 | 49 | } 50 | 51 | } 52 | 53 | // MARK: - Default 54 | 55 | public extension EUDCC.ValidationRule { 56 | 57 | /// The default `EUDCC.ValidationRule` 58 | static var `default`: Self { 59 | .if( 60 | .isVaccination, 61 | then: .isFullyImmunized() 62 | && .isWellKnownVaccineMedicinalProduct 63 | && !.isVaccinationExpired(), 64 | else: .if( 65 | .isTest, 66 | then: .isTestedNegative && .isTestValid(), 67 | else: .if( 68 | .isRecovery, 69 | then: .isRecoveryValid, 70 | else: .constant(false) 71 | ) 72 | ) 73 | ) 74 | } 75 | 76 | } 77 | 78 | // MARK: - Equatable 79 | 80 | extension EUDCC.ValidationRule: Equatable { 81 | 82 | /// Returns a Boolean value indicating whether two values are equal. 83 | /// - Parameters: 84 | /// - lhs: A value to compare. 85 | /// - rhs: Another value to compare. 86 | public static func == ( 87 | lhs: Self, 88 | rhs: Self 89 | ) -> Bool { 90 | lhs.tag == rhs.tag 91 | } 92 | 93 | } 94 | 95 | // MARK: - Hashable 96 | 97 | extension EUDCC.ValidationRule: Hashable { 98 | 99 | /// Hashes the essential components of this value by feeding them into the 100 | /// given hasher. 101 | /// - Parameter hasher: The hasher to use when combining the components of this instance. 102 | public func hash( 103 | into hasher: inout Hasher 104 | ) { 105 | hasher.combine(self.tag) 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/EUDCC.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - EUDCC 4 | 5 | /// The European Digital COVID Certificate (EUCC) 6 | public struct EUDCC: Hashable { 7 | 8 | // MARK: Properties 9 | 10 | /// The issuer 11 | public let issuer: String 12 | 13 | /// The issued at Date 14 | public let issuedAt: Date 15 | 16 | /// The expiry Date 17 | public let expiresAt: Date 18 | 19 | /// The Schema version 20 | public let schmemaVersion: String 21 | 22 | /// The date of birth 23 | public let dateOfBirth: Date 24 | 25 | /// The Name 26 | public let name: Name 27 | 28 | /// The Content 29 | public let content: Content 30 | 31 | /// The CryptographicSignature 32 | public let cryptographicSignature: CryptographicSignature 33 | 34 | /// The Base-45 representation 35 | public let base45Representation: String 36 | 37 | // MARK: Initializer 38 | 39 | /// Creates a new instance of `EUDCC` 40 | /// - Parameters: 41 | /// - issuer: The issuer 42 | /// - issuedAt: The issued at Date 43 | /// - expiresAt: The expiry Date 44 | /// - schmemaVersion: The Schema version 45 | /// - dateOfBirth: The date of birth 46 | /// - name: The Name 47 | /// - content: The Content 48 | /// - cryptographicSignature: The CryptographicSignature 49 | /// - base45Representation: The The Base-45 representation 50 | public init( 51 | issuer: String, 52 | issuedAt: Date, 53 | expiresAt: Date, 54 | schmemaVersion: String, 55 | dateOfBirth: Date, 56 | name: Name, 57 | content: Content, 58 | cryptographicSignature: CryptographicSignature, 59 | base45Representation: String 60 | ) { 61 | self.issuer = issuer 62 | self.issuedAt = issuedAt 63 | self.expiresAt = expiresAt 64 | self.schmemaVersion = schmemaVersion 65 | self.dateOfBirth = dateOfBirth 66 | self.name = name 67 | self.content = content 68 | self.cryptographicSignature = cryptographicSignature 69 | self.base45Representation = base45Representation 70 | } 71 | 72 | } 73 | 74 | // MARK: Convenience Content Accessor 75 | 76 | public extension EUDCC { 77 | 78 | /// The Vaccination case of the `Content` if available 79 | var vaccination: Vaccination? { 80 | if case .vaccination(let vaccination) = self.content { 81 | return vaccination 82 | } else { 83 | return nil 84 | } 85 | } 86 | 87 | /// The Test case of the `Content` if available 88 | var test: Test? { 89 | if case .test(let test) = self.content { 90 | return test 91 | } else { 92 | return nil 93 | } 94 | } 95 | 96 | /// The Recovery case of the `Content` if available 97 | var recovery: Recovery? { 98 | if case .recovery(let recovery) = self.content { 99 | return recovery 100 | } else { 101 | return nil 102 | } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/Vaccination/EUDCC+Vaccination+VaccineMarketingAuthorizationHolder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - VaccineMarketingAuthorizationHolder 4 | 5 | public extension EUDCC.Vaccination { 6 | 7 | /// The EUDCC vaccination vaccine marketing authorisation holder or manufacturer 8 | struct VaccineMarketingAuthorizationHolder: Hashable { 9 | 10 | // MARK: Properties 11 | 12 | /// The string value 13 | public let value: String 14 | 15 | // MARK: Initializer 16 | 17 | /// Creates a new instance of `EUDCC.Vaccination.VaccineMedicinalProduct` 18 | /// - Parameter value: The string value 19 | public init(value: String) { 20 | self.value = value 21 | } 22 | 23 | } 24 | 25 | } 26 | 27 | // MARK: - WellKnownValue 28 | 29 | public extension EUDCC.Vaccination.VaccineMarketingAuthorizationHolder { 30 | 31 | /// The WellKnownValue 32 | enum WellKnownValue: String, Codable, Hashable, CaseIterable { 33 | /// AstraZeneca AB 34 | case astraZenecaAB = "ORG-100001699" 35 | /// Biontech Manufacturing GmbH 36 | case biontechManufacturingGmbH = "ORG-100030215" 37 | /// Janssen-Cilag International 38 | case janssenCilagInternational = "ORG-100001417" 39 | /// Moderna Biotech Spain S.L. 40 | case modernaBiotechSpainSL = "ORG-100031184" 41 | /// Curevac AG 42 | case curevacAG = "ORG-100006270" 43 | /// CanSino Biologics 44 | case canSinoBiologics = "ORG-100013793" 45 | /// China Sinopharm International Corp. - Beijing location 46 | case chinaSinopharmInternationalCorp = "ORG-100020693" 47 | /// Sinopharm Weiqida Europe Pharmaceutical s.r.o. - Prague location 48 | case sinopharmWeiqidaEuropePharmaceutical = "ORG-100010771" 49 | /// Sinopharm Zhijun (Shenzhen) Pharmaceutical Co. Ltd. - Shenzhen location 50 | case sinopharmZhijunPharmaceuticalCoLtd = "ORG-100024420" 51 | /// Novavax CZ AS 52 | case novavaxCzAs = "ORG-100032020" 53 | /// Gamaleya Research Institute 54 | case gamaleyaResearchInstitute = "Gamaleya-Research-Institute" 55 | /// Vector Institute 56 | case vectorInstitute = "Vector-Institute" 57 | /// Sinovac Biotech 58 | case sinovacBiotech = "Sinovac-Biotech" 59 | /// Bharat Biotech 60 | case bharatBiotech = "Bharat-Biotech" 61 | } 62 | 63 | /// The WellKnownValue if available 64 | var wellKnownValue: WellKnownValue? { 65 | .init(rawValue: self.value) 66 | } 67 | 68 | } 69 | 70 | // MARK: - Codable 71 | 72 | extension EUDCC.Vaccination.VaccineMarketingAuthorizationHolder: Codable { 73 | 74 | /// Creates a new instance by decoding from the given decoder. 75 | /// - Parameter decoder: The decoder to read data from. 76 | public init(from decoder: Decoder) throws { 77 | let container = try decoder.singleValueContainer() 78 | self.value = try container.decode(String.self) 79 | } 80 | 81 | /// Encodes this value into the given encoder. 82 | /// - Parameter encoder: The encoder to write data to. 83 | public func encode(to encoder: Encoder) throws { 84 | var container = encoder.singleValueContainer() 85 | try container.encode(self.value) 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /Sources/EUDCCDecoder/Extension/CBOR+DictionaryRepresentation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftCBOR 3 | 4 | // MARK: - CBOR+dictionaryRepresentation 5 | 6 | extension SwiftCBOR.CBOR { 7 | 8 | /// Retrieve a Dictionary representation of the current CBOR object if available 9 | func dictionaryRepresentation() -> [String: Any?]? { 10 | // Verify CBOR is a map 11 | guard case .map(let cborMap) = self else { 12 | // Otherwise return nil 13 | return nil 14 | } 15 | // Convert CBOR Map to `Dictionary` 16 | let dictionary = cborMap.convert() 17 | // Return nil if Dictionary is empty otherwise return the dictionary 18 | return dictionary.isEmpty ? nil : dictionary 19 | } 20 | 21 | } 22 | 23 | // MARK: - Dictionary 24 | 25 | private extension Dictionary where Key == CBOR, Value == CBOR { 26 | 27 | /// Convert the CBOR Dictionary to a `Dictionary` 28 | func convert() -> [String: Any?] { 29 | // Initialize result Dictionary 30 | var dictionary: [String: Any?] = .init() 31 | // For each Key and Value CBOR 32 | for (key, value) in self { 33 | // Verify dictionary Key is available 34 | guard let key = key.dictionaryKey else { 35 | // Otherwise continue 36 | continue 37 | } 38 | // Update Value for Key 39 | dictionary.updateValue(value.dictionaryValue, forKey: key) 40 | } 41 | // Return converted Dictionary 42 | return dictionary 43 | } 44 | 45 | } 46 | 47 | // MARK: - CBOR+dictionaryKey 48 | 49 | private extension SwiftCBOR.CBOR { 50 | 51 | /// The dictionary Key 52 | var dictionaryKey: String? { 53 | switch self { 54 | case .utf8String(let string): 55 | return string 56 | case .unsignedInt(let int): 57 | return .init(int) 58 | case .negativeInt(let int): 59 | return "-\(int + 1)" 60 | case .half(let float): 61 | return .init(float) 62 | case .float(let float): 63 | return .init(float) 64 | case .double(let double): 65 | return .init(double) 66 | default: 67 | return nil 68 | } 69 | } 70 | 71 | } 72 | 73 | // MARK: - CBOR+dictionaryValue 74 | 75 | private extension SwiftCBOR.CBOR { 76 | 77 | /// The dictionary Value 78 | var dictionaryValue: Any? { 79 | switch self { 80 | case .boolean(let boolValue): 81 | return boolValue 82 | case .unsignedInt(let uIntValue): 83 | return Int(uIntValue) 84 | case .negativeInt(let negativeIntValue): 85 | return -Int(negativeIntValue) - 1 86 | case .double(let doubleValue): 87 | return doubleValue 88 | case .float(let floatValue): 89 | return floatValue 90 | case .half(let halfValue): 91 | return halfValue 92 | case .simple(let simpleValue): 93 | return simpleValue 94 | case .byteString(let byteStringValue): 95 | return byteStringValue 96 | case .date(let dateValue): 97 | return dateValue 98 | case .utf8String(let stringValue): 99 | return stringValue 100 | case .array(let arrayValue): 101 | return arrayValue.map(\.dictionaryValue) 102 | case .map(let mapValue): 103 | return mapValue.convert() 104 | case .null, .undefined, .tagged, .break: 105 | return nil 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /Sources/EUDCCVerifier/TrustService/Implementations/EUCentralEUDCCTrustService.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - EUCentralEUDCCTrustService 5 | 6 | /// The EU Central EUDCC TrustService 7 | public final class EUCentralEUDCCTrustService { 8 | 9 | // MARK: Properties 10 | 11 | /// The URL 12 | let url: URL 13 | 14 | // MARK: Initializer 15 | 16 | /// Designated Initializer 17 | /// - Parameter url: The URL 18 | public init( 19 | url: URL = .init(string: "https://dgca-verifier-service.cfapps.eu10.hana.ondemand.com/signercertificateUpdate")! 20 | ) { 21 | self.url = url 22 | } 23 | 24 | } 25 | 26 | // MARK: - EUDCCTrustService 27 | 28 | extension EUCentralEUDCCTrustService: EUDCCTrustService { 29 | 30 | /// Retrieve EUDCC TrustCertificates 31 | /// - Parameter completion: The completion closure 32 | public func getTrustCertificates( 33 | completion: @escaping (Result<[EUDCC.TrustCertificate], Error>) -> Void 34 | ) { 35 | // Fetch Certificates 36 | self.fetchCertificates { trustCertificates in 37 | // Dispatch on Main-Queue 38 | DispatchQueue.main.async { 39 | // Complete with success 40 | completion(.success(trustCertificates)) 41 | } 42 | } 43 | } 44 | 45 | } 46 | 47 | // MARK: - Fetch Certificates 48 | 49 | private extension EUCentralEUDCCTrustService { 50 | 51 | /// Fetch Certificates recursively 52 | /// - Parameters: 53 | /// - resumeToken: The current Resume-Token 54 | /// - trustCertificates: The TrustCertificates 55 | /// - completion: The completion closure 56 | func fetchCertificates( 57 | resumeToken: String? = nil, 58 | trustCertificates: [EUDCC.TrustCertificate] = .init(), 59 | completion: @escaping ([EUDCC.TrustCertificate]) -> Void 60 | ) { 61 | // Initialize URLRequest 62 | var urlRequest = URLRequest(url: self.url) 63 | // Check if a ResumeToken is available 64 | if let resumeToken = resumeToken { 65 | // Add ResumeToken 66 | urlRequest.setValue( 67 | resumeToken, 68 | forHTTPHeaderField: "X-RESUME-TOKEN" 69 | ) 70 | } 71 | // Perform DataTask 72 | let dataTask = URLSession.shared.dataTask( 73 | with: urlRequest 74 | ) { [weak self] data, response, error in 75 | // Verify HTTPResponse is available and succeessfull 76 | guard let httpResponse = response as? HTTPURLResponse, 77 | httpResponse.statusCode == 200 else { 78 | // Otherwise complete with current TrustCertificates 79 | return completion(trustCertificates) 80 | } 81 | // Verify KeyID and Data is available 82 | guard let keyId = httpResponse.allHeaderFields["x-kid"] as? String, 83 | let data = data else { 84 | // Otherwise complete with current TrustCertificates 85 | return completion(trustCertificates) 86 | } 87 | // Retrieve certificates contents as String from payload 88 | let certificateContents = String(decoding: data, as: UTF8.self) 89 | // Initialize TrustCertificates 90 | let trustCertificate = EUDCC.TrustCertificate( 91 | keyID: .init(rawValue: keyId), 92 | contents: certificateContents 93 | ) 94 | // Append TrustCertificate to current TrustCertificates 95 | let trustCertificates = trustCertificates + [trustCertificate] 96 | // Verify the next ResumeToken is available 97 | guard let nextResumeToken = httpResponse.allHeaderFields["x-resume-token"] as? String else { 98 | // Otherwise complete with current SignerCertificates 99 | return completion(trustCertificates) 100 | } 101 | // Re-Fetch Certificates with next ResumeToken 102 | self?.fetchCertificates( 103 | resumeToken: nextResumeToken, 104 | trustCertificates: trustCertificates, 105 | completion: completion 106 | ) 107 | } 108 | // Execute Request 109 | dataTask.resume() 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /Sources/EUDCCVerifier/TrustService/GroupableEUDCCTrustService.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - GroupableEUDCCTrustService 5 | 6 | /// A groupable EUDCCTrustService 7 | public struct GroupableEUDCCTrustService { 8 | 9 | // MARK: Properties 10 | 11 | /// The array of EUDCCTrustServices 12 | let trustServices: [EUDCCTrustService] 13 | 14 | // MARK: Initializer 15 | 16 | /// Creates a new instance of `GroupEUDCCTrustService` 17 | /// - Parameter trustServices: An array of `EUDCCTrustService` elements 18 | public init( 19 | trustServices: [EUDCCTrustService] 20 | ) { 21 | self.trustServices = trustServices 22 | } 23 | 24 | } 25 | 26 | // MARK: - Failure 27 | 28 | public extension GroupableEUDCCTrustService { 29 | 30 | /// The GroupableEUDCCTrustService Failure 31 | struct Failure: Error { 32 | 33 | // MARK: Properties 34 | 35 | /// The error reason 36 | public let reason: String 37 | 38 | /// The optional array of underlying Error objects 39 | public let underlyingErrors: [Error]? 40 | 41 | /// Creates a new instance of `GroupEUDCCTrustService.Failure` 42 | /// - Parameters: 43 | /// - reason: The error reason 44 | /// - underlyingErrors: The optional array of underlying Error objects. Default value `nil` 45 | public init( 46 | reason: String, 47 | underlyingErrors: [Error]? = nil 48 | ) { 49 | self.reason = reason 50 | self.underlyingErrors = underlyingErrors 51 | } 52 | 53 | } 54 | 55 | } 56 | 57 | // MARK: - EUDCCTrustService 58 | 59 | extension GroupableEUDCCTrustService: EUDCCTrustService { 60 | 61 | /// Retrieve EUDCC TrustCertificates 62 | /// - Parameter completion: The completion closure 63 | public func getTrustCertificates( 64 | completion: @escaping (Result<[EUDCC.TrustCertificate], Error>) -> Void 65 | ) { 66 | // Verify TrustServices are not empty 67 | guard !self.trustServices.isEmpty else { 68 | // Otherwise complete with failure 69 | return completion( 70 | .failure( 71 | Failure(reason: "No EUDCCTrustServices are available") 72 | ) 73 | ) 74 | } 75 | // Initialize array of results 76 | var results: [Result<[EUDCC.TrustCertificate], Error>] = .init() 77 | // Initialize a DispatchGroup 78 | let dispatchGroup = DispatchGroup() 79 | // Initializ a DispatchQueue 80 | let dispatchQueue = DispatchQueue( 81 | label: "de.tiigi.EUDCCKit.GroupEUDCCTrustService" 82 | ) 83 | // For each TrustService 84 | for trustService in self.trustServices { 85 | // Enter the DispatchGroup 86 | dispatchGroup.enter() 87 | // Retrieve TrustCertificates 88 | trustService.getTrustCertificates { result in 89 | // Dispatch on DispatchQueue 90 | dispatchQueue.async { 91 | // Append Result 92 | results.append(result) 93 | // Leave DispatchGroup 94 | dispatchGroup.leave() 95 | } 96 | } 97 | } 98 | // DispatchGroup notify on main thread 99 | dispatchGroup.notify( 100 | queue: .main 101 | ) { 102 | // Map results to certificates 103 | let certificates = results 104 | .compactMap { try? $0.get() } 105 | .flatMap { $0 } 106 | // Verify Certificates are not empty 107 | guard !certificates.isEmpty else { 108 | // Otherwise complete with failure 109 | return completion( 110 | .failure( 111 | Failure( 112 | reason: "No TrustCertificates have been retrieved", 113 | underlyingErrors: results 114 | .compactMap { result in 115 | if case .failure(let error) = result { 116 | return error 117 | } else { 118 | return nil 119 | } 120 | } 121 | ) 122 | ) 123 | ) 124 | } 125 | // Complete with success 126 | completion(.success(certificates)) 127 | } 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /Sources/EUDCCVerifier/TrustService/Implementations/RobertKochInstituteEUDCCTrustService.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - RobertKochInstituteEUDCCTrustService 5 | 6 | /// The Robert-Koch-Institute EUDCC TrustService 7 | public final class RobertKochInstituteEUDCCTrustService { 8 | 9 | // MARK: Properties 10 | 11 | /// The URL 12 | let url: URL 13 | 14 | // MARK: Initializer 15 | 16 | /// Designated Initializer 17 | /// - Parameter url: The URL 18 | public init( 19 | url: URL = .init(string: "https://de.dscg.ubirch.com/trustList/DSC/")! 20 | ) { 21 | self.url = url 22 | } 23 | 24 | } 25 | 26 | // MARK: - TrustList & TrustCertificate 27 | 28 | extension RobertKochInstituteEUDCCTrustService { 29 | 30 | /// The TrustList Response 31 | struct RKITrustList: Codable { 32 | 33 | /// The TrustCertificates 34 | let certificates: [RKITrustCertificate] 35 | 36 | } 37 | 38 | /// A TrustCertificate 39 | struct RKITrustCertificate: Codable { 40 | 41 | /// The certificate type 42 | let certificateType: String 43 | 44 | /// The country 45 | let country: String 46 | 47 | /// The KeyID 48 | let kid: String 49 | 50 | /// The raw certificate data 51 | let rawData: String 52 | 53 | /// The signature 54 | let signature: String 55 | 56 | /// The tumbprint 57 | let thumbprint: String 58 | 59 | /// The timestamp 60 | let timestamp: String 61 | 62 | } 63 | 64 | } 65 | 66 | // MARK: - Failure 67 | 68 | public extension RobertKochInstituteEUDCCTrustService { 69 | 70 | /// The Failure 71 | enum Failure: Error { 72 | /// Request errored 73 | case requestError(Error?) 74 | /// Certificates missing 75 | case certificatesMissing 76 | /// Decoding Error 77 | case decodingError(Error) 78 | } 79 | 80 | } 81 | 82 | // MARK: - EUDCCTrustService 83 | 84 | extension RobertKochInstituteEUDCCTrustService: EUDCCTrustService { 85 | 86 | /// Retrieve EUDCC TrustCertificates 87 | /// - Parameter completion: The completion closure 88 | public func getTrustCertificates( 89 | completion: @escaping (Result<[EUDCC.TrustCertificate], Error>) -> Void 90 | ) { 91 | // Perform DataTask 92 | let dataTask = URLSession.shared.dataTask( 93 | with: self.url 94 | ) { data, response, error in 95 | // Verify Data is available 96 | guard let data = data else { 97 | // Complete with failure 98 | return DispatchQueue.main.async { 99 | completion(.failure(Failure.requestError(error))) 100 | } 101 | } 102 | // Decode response body as String 103 | let response = String(decoding: data, as: UTF8.self) 104 | // Split response String 105 | let responseComponents = response.components(separatedBy: "\n") 106 | // Verify componets contains two items 107 | guard responseComponents.indices.contains(1) else { 108 | // Otherwise complete with falure 109 | return DispatchQueue.main.async { 110 | completion(.failure(Failure.certificatesMissing)) 111 | } 112 | } 113 | // Declare TrustList 114 | let trustList: RKITrustList 115 | do { 116 | // Try to decode TrustList 117 | trustList = try JSONDecoder().decode( 118 | RKITrustList.self, 119 | from: .init(responseComponents[1].utf8) 120 | ) 121 | } catch { 122 | // On Error complete with failure 123 | return DispatchQueue.main.async { 124 | completion(.failure(Failure.decodingError(error))) 125 | } 126 | } 127 | // Map TrustList to TrustCertificates 128 | let trustCertificates: [EUDCC.TrustCertificate] = trustList 129 | .certificates 130 | .map { certificate in 131 | .init( 132 | keyID: .init(rawValue: certificate.kid), 133 | contents: certificate.rawData 134 | ) 135 | } 136 | // Complete with success 137 | DispatchQueue.main.async { 138 | completion(.success(trustCertificates)) 139 | } 140 | } 141 | // Execute Request 142 | dataTask.resume() 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/Recovery/EUDCC+Recovery.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Recovery 4 | 5 | public extension EUDCC { 6 | 7 | /// The EUDCC Recovery Entry 8 | struct Recovery: Hashable { 9 | 10 | // MARK: Properties 11 | 12 | /// Disease or agent targeted 13 | public let diseaseAgentTargeted: DiseaseAgentTargeted 14 | 15 | /// Date of first positive NAA test result 16 | public let dateOfFirstPositiveTestResult: Date 17 | 18 | /// Country of Test 19 | public let countryOfTest: Country 20 | 21 | /// Certificate issuer 22 | public let certificateIssuer: String 23 | 24 | /// Certificate Valid From 25 | public let certificateValidFrom: Date 26 | 27 | /// Certificate Valid Until 28 | public let certificateValidUntil: Date 29 | 30 | /// Unique Certificate Identifier (UVCI) 31 | public let certificateIdentifier: String 32 | 33 | /// Creates a new instance of `EUDCC.Recovery` 34 | /// - Parameters: 35 | /// - diseaseAgentTargeted: Disease or agent targeted 36 | /// - dateOfFirstPositiveTestResult: Date of first positive NAA test result 37 | /// - countryOfTest: Country of Test 38 | /// - certificateIssuer: Certificate issuer 39 | /// - certificateValidFrom: Certificate Valid From 40 | /// - certificateValidUntil: Certificate Valid Until 41 | /// - certificateIdentifier: Unique Certificate Identifier (UVCI) 42 | public init( 43 | diseaseAgentTargeted: DiseaseAgentTargeted, 44 | dateOfFirstPositiveTestResult: Date, 45 | countryOfTest: Country, 46 | certificateIssuer: String, 47 | certificateValidFrom: Date, 48 | certificateValidUntil: Date, 49 | certificateIdentifier: String 50 | ) { 51 | self.diseaseAgentTargeted = diseaseAgentTargeted 52 | self.dateOfFirstPositiveTestResult = dateOfFirstPositiveTestResult 53 | self.countryOfTest = countryOfTest 54 | self.certificateIssuer = certificateIssuer 55 | self.certificateValidFrom = certificateValidFrom 56 | self.certificateValidUntil = certificateValidUntil 57 | self.certificateIdentifier = certificateIdentifier 58 | } 59 | 60 | } 61 | 62 | } 63 | 64 | // MARK: - Codable 65 | 66 | extension EUDCC.Recovery: Codable { 67 | 68 | /// The CodingKeys 69 | private enum CodingKeys: String, CodingKey { 70 | case diseaseAgentTargeted = "tg" 71 | case dateOfFirstPositiveTestResult = "fr" 72 | case countryOfTest = "co" 73 | case certificateIssuer = "is" 74 | case certificateValidFrom = "df" 75 | case certificateValidUntil = "du" 76 | case certificateIdentifier = "ci" 77 | } 78 | 79 | /// Creates a new instance by decoding from the given decoder. 80 | /// - Parameter decoder: The decoder to read data from. 81 | public init(from decoder: Decoder) throws { 82 | let container = try decoder.container(keyedBy: CodingKeys.self) 83 | self.diseaseAgentTargeted = try container.decode(EUDCC.DiseaseAgentTargeted.self, forKey: .diseaseAgentTargeted) 84 | self.dateOfFirstPositiveTestResult = try container.decode(forKey: .dateOfFirstPositiveTestResult, using: EUDCCDateFormatter.default) 85 | self.countryOfTest = try container.decode(EUDCC.Country.self, forKey: .countryOfTest) 86 | self.certificateIssuer = try container.decode(String.self, forKey: .certificateIssuer) 87 | self.certificateValidFrom = try container.decode(forKey: .certificateValidFrom, using: EUDCCDateFormatter.default) 88 | self.certificateValidUntil = try container.decode(forKey: .certificateValidUntil, using: EUDCCDateFormatter.default) 89 | self.certificateIdentifier = try container.decode(String.self, forKey: .certificateIdentifier) 90 | } 91 | 92 | /// Encodes this value into the given encoder. 93 | /// - Parameter encoder: The encoder to write data to. 94 | public func encode(to encoder: Encoder) throws { 95 | var container = encoder.container(keyedBy: CodingKeys.self) 96 | try container.encode(self.diseaseAgentTargeted, forKey: .diseaseAgentTargeted) 97 | try container.encode(self.dateOfFirstPositiveTestResult, forKey: .dateOfFirstPositiveTestResult, using: EUDCCDateFormatter.default) 98 | try container.encode(self.countryOfTest, forKey: .countryOfTest) 99 | try container.encode(self.certificateIssuer, forKey: .certificateIssuer) 100 | try container.encode(self.certificateValidFrom, forKey: .certificateValidFrom, using: EUDCCDateFormatter.default) 101 | try container.encode(self.certificateValidUntil, forKey: .certificateValidUntil, using: EUDCCDateFormatter.default) 102 | try container.encode(self.certificateIdentifier, forKey: .certificateIdentifier) 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/Test/EUDCC+Test.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Test 4 | 5 | public extension EUDCC { 6 | 7 | /// The EUDCC Test Entry 8 | struct Test: Hashable { 9 | 10 | // MARK: Properties 11 | 12 | /// Disease or agent targeted 13 | public let diseaseAgentTargeted: DiseaseAgentTargeted 14 | 15 | /// Type of Test 16 | public let typeOfTest: TestType 17 | 18 | /// Test Name 19 | public let testName: String? 20 | 21 | /// RAT Test name and manufacturer 22 | public let testNameAndManufacturer: String? 23 | 24 | /// Date/Time of sample collection 25 | public let dateOfSampleCollection: Date 26 | 27 | /// Test Result 28 | public let testResult: TestResult 29 | 30 | /// Testing Centre 31 | public let testingCentre: String? 32 | 33 | /// Country of Test 34 | public let countryOfTest: Country 35 | 36 | /// Certificate Issuer 37 | public let certificateIssuer: String 38 | 39 | /// Unique Certificate Identifier (UVCI) 40 | public let certificateIdentifier: String 41 | 42 | // MARK: Initializer 43 | 44 | /// Creates a new instance of `EUDCC.Test` 45 | /// - Parameters: 46 | /// - diseaseAgentTargeted: Disease or agent targeted 47 | /// - typeOfTest: Type of Test 48 | /// - testName: Test Name 49 | /// - testNameAndManufacturer: RAT Test name and manufacturer 50 | /// - dateOfSampleCollection: Date/Time of sample collection 51 | /// - testResult: Test Result 52 | /// - testingCentre: Testing Centre 53 | /// - countryOfTest: Country of Test 54 | /// - certificateIssuer: Certificate Issuer 55 | /// - certificateIdentifier: Unique Certificate Identifier (UVCI) 56 | public init( 57 | diseaseAgentTargeted: DiseaseAgentTargeted, 58 | typeOfTest: TestType, 59 | testName: String?, 60 | testNameAndManufacturer: String?, 61 | dateOfSampleCollection: Date, 62 | testResult: TestResult, 63 | testingCentre: String, 64 | countryOfTest: Country, 65 | certificateIssuer: String, 66 | certificateIdentifier: String 67 | ) { 68 | self.diseaseAgentTargeted = diseaseAgentTargeted 69 | self.typeOfTest = typeOfTest 70 | self.testName = testName 71 | self.testNameAndManufacturer = testNameAndManufacturer 72 | self.dateOfSampleCollection = dateOfSampleCollection 73 | self.testResult = testResult 74 | self.testingCentre = testingCentre 75 | self.countryOfTest = countryOfTest 76 | self.certificateIssuer = certificateIssuer 77 | self.certificateIdentifier = certificateIdentifier 78 | } 79 | 80 | } 81 | 82 | } 83 | 84 | // MARK: - Codable 85 | 86 | extension EUDCC.Test: Codable { 87 | 88 | /// The CodingKeys 89 | private enum CodingKeys: String, CodingKey { 90 | case diseaseAgentTargeted = "tg" 91 | case typeOfTest = "tt" 92 | case testName = "nm" 93 | case testNameAndManufacturer = "ma" 94 | case dateOfSampleCollection = "sc" 95 | case testResult = "tr" 96 | case testingCentre = "tc" 97 | case countryOfTest = "co" 98 | case certificateIssuer = "is" 99 | case certificateIdentifier = "ci" 100 | } 101 | 102 | /// Creates a new instance by decoding from the given decoder. 103 | /// - Parameter decoder: The decoder to read data from. 104 | public init(from decoder: Decoder) throws { 105 | let container = try decoder.container(keyedBy: CodingKeys.self) 106 | self.diseaseAgentTargeted = try container.decode(EUDCC.DiseaseAgentTargeted.self, forKey: .diseaseAgentTargeted) 107 | self.typeOfTest = try container.decode(TestType.self, forKey: .typeOfTest) 108 | self.testName = try container.decodeIfPresent(String.self, forKey: .testName) 109 | self.testNameAndManufacturer = try container.decodeIfPresent(String.self, forKey: .testNameAndManufacturer) 110 | self.dateOfSampleCollection = try container.decode(forKey: .dateOfSampleCollection, using: EUDCCDateFormatter.default) 111 | self.testResult = try container.decode(TestResult.self, forKey: .testResult) 112 | self.testingCentre = try container.decodeIfPresent(String.self, forKey: .testingCentre) 113 | self.countryOfTest = try container.decode(EUDCC.Country.self, forKey: .countryOfTest) 114 | self.certificateIssuer = try container.decode(String.self, forKey: .certificateIssuer) 115 | self.certificateIdentifier = try container.decode(String.self, forKey: .certificateIdentifier) 116 | } 117 | 118 | /// Encodes this value into the given encoder. 119 | /// - Parameter encoder: The encoder to write data to. 120 | public func encode(to encoder: Encoder) throws { 121 | var container = encoder.container(keyedBy: CodingKeys.self) 122 | try container.encode(self.diseaseAgentTargeted, forKey: .diseaseAgentTargeted) 123 | try container.encode(self.typeOfTest, forKey: .typeOfTest) 124 | try container.encode(self.testName, forKey: .testName) 125 | try container.encode(self.testNameAndManufacturer, forKey: .testNameAndManufacturer) 126 | try container.encode(self.dateOfSampleCollection, forKey: .dateOfSampleCollection, using: EUDCCDateFormatter.default) 127 | try container.encode(self.testResult, forKey: .testResult) 128 | try container.encode(self.testingCentre, forKey: .testingCentre) 129 | try container.encode(self.countryOfTest, forKey: .countryOfTest) 130 | try container.encode(self.certificateIssuer, forKey: .certificateIssuer) 131 | try container.encode(self.certificateIdentifier, forKey: .certificateIdentifier) 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/EUDCC+Codable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - CodingKeys 4 | 5 | private extension EUDCC { 6 | 7 | /// The Top-Level CodingKeys 8 | enum TopLevelCodingKeys: String, CodingKey { 9 | case issuer = "1" 10 | case issuedAt = "6" 11 | case expiresAt = "4" 12 | case eudcc = "-260" 13 | } 14 | 15 | /// The EUDCC Version CodingKeys 16 | enum EUDCCVersionCodingKeys: String, CodingKey { 17 | case v1 = "1" 18 | } 19 | 20 | /// The EUDCC CodingKeys 21 | enum EUDCCCodingKeys: String, CodingKey { 22 | case schmemaVersion = "ver" 23 | case dateOfBirth = "dob" 24 | case name = "nam" 25 | case vaccinations = "v" 26 | case tests = "t" 27 | case recoveries = "r" 28 | case cryptographicSignature = "cryptographicSignature" 29 | case base45Representation = "base45Representation" 30 | } 31 | 32 | } 33 | 34 | // MARK: - Decodable 35 | 36 | extension EUDCC: Decodable { 37 | 38 | /// Creates a new instance by decoding from the given decoder. 39 | /// - Parameter decoder: The decoder to read data from. 40 | public init(from decoder: Decoder) throws { 41 | let topLevelContainer = try decoder.container(keyedBy: TopLevelCodingKeys.self) 42 | self.issuer = try topLevelContainer.decode(String.self, forKey: .issuer) 43 | self.issuedAt = try topLevelContainer.decode( 44 | forKey: .issuedAt, 45 | using: EUDCCTimestampFormatter.default 46 | ) 47 | self.expiresAt = try topLevelContainer.decode( 48 | forKey: .expiresAt, 49 | using: EUDCCTimestampFormatter.default 50 | ) 51 | let eudccContainer = try topLevelContainer 52 | .nestedContainer(keyedBy: EUDCCVersionCodingKeys.self, forKey: .eudcc) 53 | .nestedContainer(keyedBy: EUDCCCodingKeys.self, forKey: .v1) 54 | self.schmemaVersion = try eudccContainer.decode( 55 | String.self, 56 | forKey: .schmemaVersion 57 | ) 58 | self.dateOfBirth = try eudccContainer.decode( 59 | forKey: .dateOfBirth, 60 | using: EUDCCDateFormatter.default 61 | ) 62 | self.name = try eudccContainer.decode( 63 | Name.self, 64 | forKey: .name 65 | ) 66 | if let vaccinations = try? eudccContainer.decode([EUDCC.Vaccination].self, forKey: .vaccinations), 67 | let vaccination = vaccinations.first { 68 | self.content = .vaccination(vaccination) 69 | } else if let tests = try? eudccContainer.decode([EUDCC.Test].self, forKey: .tests), 70 | let test = tests.first { 71 | self.content = .test(test) 72 | } else if let recoveries = try? eudccContainer.decode([EUDCC.Recovery].self, forKey: .recoveries), 73 | let recovery = recoveries.first { 74 | self.content = .recovery(recovery) 75 | } else { 76 | throw DecodingError.dataCorrupted( 77 | .init( 78 | codingPath: eudccContainer.codingPath, 79 | debugDescription: "EUDCC Content missing" 80 | ) 81 | ) 82 | } 83 | if let cryptographicSignature = try? eudccContainer.decodeIfPresent( 84 | CryptographicSignature.self, 85 | forKey: .cryptographicSignature 86 | ) { 87 | self.cryptographicSignature = cryptographicSignature 88 | } else { 89 | self.cryptographicSignature = .init( 90 | protected: .init(), 91 | unprotected: .init(), 92 | payload: .init(), 93 | signature: .init() 94 | ) 95 | } 96 | if let base45Representation = try? eudccContainer.decode(String.self, forKey: .base45Representation) { 97 | self.base45Representation = base45Representation 98 | } else { 99 | self.base45Representation = .init() 100 | } 101 | } 102 | 103 | } 104 | 105 | // MARK: - EncoderUserInfoKeys 106 | 107 | public extension EUDCC { 108 | 109 | /// The EUDCC Encoder UserInfo Keys 110 | enum EncoderUserInfoKeys { 111 | 112 | /// Skip Cryptographic Signature encoding CodingUserInfoKey 113 | static var skipCryptographicSignature: CodingUserInfoKey { 114 | .init(rawValue: "skip-cryptographic-signature")! 115 | } 116 | 117 | /// Skip Base-45 Representation encoding CodingUserInfoKey 118 | static var skipBase45Representation: CodingUserInfoKey { 119 | .init(rawValue: "skip-base-45-representation")! 120 | } 121 | 122 | } 123 | 124 | } 125 | 126 | // MARK: - Encodable 127 | 128 | extension EUDCC: Encodable { 129 | 130 | /// Encodes this value into the given encoder. 131 | /// - Parameter encoder: The encoder to write data to. 132 | public func encode(to encoder: Encoder) throws { 133 | var topLevelContainer = encoder.container(keyedBy: TopLevelCodingKeys.self) 134 | try topLevelContainer.encode(self.issuer, forKey: .issuer) 135 | try topLevelContainer.encode(self.issuedAt, forKey: .issuedAt, using: EUDCCTimestampFormatter.default) 136 | try topLevelContainer.encode(self.expiresAt, forKey: .expiresAt, using: EUDCCTimestampFormatter.default) 137 | var eudccVersionContainer = topLevelContainer.nestedContainer(keyedBy: EUDCCVersionCodingKeys.self, forKey: .eudcc) 138 | var eudccContainer = eudccVersionContainer.nestedContainer(keyedBy: EUDCCCodingKeys.self, forKey: .v1) 139 | try eudccContainer.encode(self.schmemaVersion, forKey: .schmemaVersion) 140 | try eudccContainer.encode(self.dateOfBirth, forKey: .dateOfBirth, using: EUDCCDateFormatter.default) 141 | try eudccContainer.encode(self.name, forKey: .name) 142 | switch self.content { 143 | case .vaccination(let vaccination): 144 | try eudccContainer.encode([vaccination], forKey: .vaccinations) 145 | case .test(let test): 146 | try eudccContainer.encode([test], forKey: .tests) 147 | case .recovery(let recovery): 148 | try eudccContainer.encode([recovery], forKey: .recoveries) 149 | } 150 | if !(encoder.userInfo[EncoderUserInfoKeys.skipCryptographicSignature] as? Bool == true) { 151 | try eudccContainer.encode(self.cryptographicSignature, forKey: .cryptographicSignature) 152 | } 153 | if !(encoder.userInfo[EncoderUserInfoKeys.skipBase45Representation] as? Bool == true) { 154 | try eudccContainer.encode(self.base45Representation, forKey: .base45Representation) 155 | } 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /Sources/EUDCC/Models/Vaccination/EUDCC+Vaccination.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // MARK: - Vaccination 4 | 5 | public extension EUDCC { 6 | 7 | /// The EUDCC Vaccination Entry 8 | struct Vaccination: Hashable { 9 | 10 | // MARK: Properties 11 | 12 | /// Disease or agent targeted 13 | public let diseaseAgentTargeted: DiseaseAgentTargeted 14 | 15 | /// Type of the vaccine or prophylaxis used. 16 | public let vaccineOrProphylaxis: VaccineOrProphylaxis 17 | 18 | /// Medicinal product used for this specific dose of vaccination 19 | public let vaccineMedicinalProduct: VaccineMedicinalProduct 20 | 21 | /// Vaccine marketing authorisation holder or manufacturer 22 | public let vaccineMarketingAuthorizationHolder: VaccineMarketingAuthorizationHolder 23 | 24 | /// Number in a series of doses 25 | public let doseNumber: Int 26 | 27 | /// The overall number of doses in the series 28 | public let totalSeriesOfDoses: Int 29 | 30 | /// Date of Vaccination 31 | public let dateOfVaccination: Date 32 | 33 | /// Member State or third country in which the vaccine was administered 34 | public let countryOfVaccination: Country 35 | 36 | /// Certificate Issuer 37 | public let certificateIssuer: String 38 | 39 | /// Unique Certificate Identifier (UVCI) 40 | public let certificateIdentifier: String 41 | 42 | // MARK: Initializer 43 | 44 | /// Creates a new instance of `EUDCC.Vaccination` 45 | /// - Parameters: 46 | /// - diseaseAgentTargeted: Disease or agent targeted 47 | /// - vaccineOrProphylaxis: Vaccine or prophylaxis 48 | /// - vaccineMedicinalProduct: Medicinal product used for this specific dose of vaccination 49 | /// - vaccineMarketingAuthorizationHolder: Vaccine marketing authorisation holder or manufacturer 50 | /// - doseNumber: Number in a series of doses 51 | /// - totalSeriesOfDoses: The overall number of doses in the series 52 | /// - dateOfVaccination: Date of Vaccination 53 | /// - countryOfVaccination: Member State or third country in which the vaccine was administered 54 | /// - certificateIssuer: Certificate Issuer 55 | /// - certificateIdentifier: Unique Certificate Identifier (UVCI) 56 | public init( 57 | diseaseAgentTargeted: DiseaseAgentTargeted, 58 | vaccineOrProphylaxis: VaccineOrProphylaxis, 59 | vaccineMedicinalProduct: VaccineMedicinalProduct, 60 | vaccineMarketingAuthorizationHolder: VaccineMarketingAuthorizationHolder, 61 | doseNumber: Int, 62 | totalSeriesOfDoses: Int, 63 | dateOfVaccination: Date, 64 | countryOfVaccination: Country, 65 | certificateIssuer: String, 66 | certificateIdentifier: String 67 | ) { 68 | self.diseaseAgentTargeted = diseaseAgentTargeted 69 | self.vaccineOrProphylaxis = vaccineOrProphylaxis 70 | self.vaccineMedicinalProduct = vaccineMedicinalProduct 71 | self.vaccineMarketingAuthorizationHolder = vaccineMarketingAuthorizationHolder 72 | self.doseNumber = doseNumber 73 | self.totalSeriesOfDoses = totalSeriesOfDoses 74 | self.dateOfVaccination = dateOfVaccination 75 | self.countryOfVaccination = countryOfVaccination 76 | self.certificateIssuer = certificateIssuer 77 | self.certificateIdentifier = certificateIdentifier 78 | } 79 | 80 | } 81 | 82 | } 83 | 84 | // MARK: - Codable 85 | 86 | extension EUDCC.Vaccination: Codable { 87 | 88 | /// The CodingKeys 89 | private enum CodingKeys: String, CodingKey { 90 | case diseaseAgentTargeted = "tg" 91 | case vaccineOrProphylaxis = "vp" 92 | case vaccineMedicinalProduct = "mp" 93 | case vaccineMarketingAuthorizationHolder = "ma" 94 | case doseNumber = "dn" 95 | case totalSeriesOfDoses = "sd" 96 | case dateOfVaccination = "dt" 97 | case countryOfVaccination = "co" 98 | case certificateIssuer = "is" 99 | case certificateIdentifier = "ci" 100 | } 101 | 102 | /// Creates a new instance by decoding from the given decoder. 103 | /// - Parameter decoder: The decoder to read data from. 104 | public init(from decoder: Decoder) throws { 105 | let container = try decoder.container(keyedBy: CodingKeys.self) 106 | self.diseaseAgentTargeted = try container.decode(EUDCC.DiseaseAgentTargeted.self, forKey: .diseaseAgentTargeted) 107 | self.vaccineOrProphylaxis = try container.decode(VaccineOrProphylaxis.self, forKey: .vaccineOrProphylaxis) 108 | self.vaccineMedicinalProduct = try container.decode(VaccineMedicinalProduct.self, forKey: .vaccineMedicinalProduct) 109 | self.vaccineMarketingAuthorizationHolder = try container.decode(VaccineMarketingAuthorizationHolder.self, forKey: .vaccineMarketingAuthorizationHolder) 110 | self.doseNumber = try container.decode(Int.self, forKey: .doseNumber) 111 | self.totalSeriesOfDoses = try container.decode(Int.self, forKey: .totalSeriesOfDoses) 112 | self.dateOfVaccination = try container.decode(forKey: .dateOfVaccination, using: EUDCCDateFormatter.default) 113 | self.countryOfVaccination = try container.decode(EUDCC.Country.self, forKey: .countryOfVaccination) 114 | self.certificateIssuer = try container.decode(String.self, forKey: .certificateIssuer) 115 | self.certificateIdentifier = try container.decode(String.self, forKey: .certificateIdentifier) 116 | } 117 | 118 | /// Encodes this value into the given encoder. 119 | /// - Parameter encoder: The encoder to write data to. 120 | public func encode(to encoder: Encoder) throws { 121 | var container = encoder.container(keyedBy: CodingKeys.self) 122 | try container.encode(self.diseaseAgentTargeted, forKey: .diseaseAgentTargeted) 123 | try container.encode(self.vaccineOrProphylaxis, forKey: .vaccineOrProphylaxis) 124 | try container.encode(self.vaccineMedicinalProduct, forKey: .vaccineMedicinalProduct) 125 | try container.encode(self.vaccineMarketingAuthorizationHolder, forKey: .vaccineMarketingAuthorizationHolder) 126 | try container.encode(self.doseNumber, forKey: .doseNumber) 127 | try container.encode(self.totalSeriesOfDoses, forKey: .totalSeriesOfDoses) 128 | try container.encode(self.dateOfVaccination, forKey: .dateOfVaccination, using: EUDCCDateFormatter.default) 129 | try container.encode(self.countryOfVaccination, forKey: .countryOfVaccination) 130 | try container.encode(self.certificateIssuer, forKey: .certificateIssuer) 131 | try container.encode(self.certificateIdentifier, forKey: .certificateIdentifier) 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /Sources/EUDCCValidator/Models/EUDCC+ValidationRule+CompareAgainst.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - ValidationRule+CompareAgainst 5 | 6 | public extension EUDCC.ValidationRule { 7 | 8 | /// The CompareAgainst enumeration 9 | enum CompareAgainst { 10 | /// Constant Value 11 | case constant(Value) 12 | /// Value retrieved from keyPath 13 | case keyPath(KeyPath) 14 | } 15 | 16 | } 17 | 18 | // MARK: - CompareAgainst+value(for:) 19 | 20 | public extension EUDCC.ValidationRule.CompareAgainst { 21 | 22 | /// Retrieve CompareAgainst Value for an given EUDCC 23 | /// - Parameter eudcc: The EUDCC 24 | /// - Returns: The Valuze 25 | func value( 26 | for eudcc: EUDCC 27 | ) -> Value { 28 | switch self { 29 | case .constant(let value): 30 | return value 31 | case .keyPath(let keyPath): 32 | return eudcc[keyPath: keyPath] 33 | } 34 | } 35 | 36 | } 37 | 38 | // MARK: - ValidationRule+Compare 39 | 40 | public extension EUDCC.ValidationRule { 41 | 42 | /// Compare value of a given KeyPath to another value using an operator 43 | /// - Parameters: 44 | /// - keyPath: The KeyPath to the value of the EUDCC 45 | /// - compareAgainstValue: The value to compare against 46 | /// - operator: The operator used for comparison 47 | /// - tag: The Tag. Default value `.init()` 48 | /// - Returns: A ValidationRule 49 | static func compare( 50 | value: CompareAgainst, 51 | to compareAgainstValue: CompareAgainst, 52 | operator: @escaping (Value, Value) -> Bool, 53 | tag: Tag = .init() 54 | ) -> Self { 55 | .init(tag: tag) { eudcc in 56 | `operator`( 57 | value.value(for: eudcc), 58 | compareAgainstValue.value(for: eudcc) 59 | ) 60 | } 61 | } 62 | 63 | } 64 | 65 | // MARK: - ValidationRule+CompareDate 66 | 67 | public extension EUDCC.ValidationRule { 68 | 69 | /// The CompareAgainstDate Parameter 70 | struct CompareAgainstDate { 71 | 72 | // MARK: Static-Properties 73 | 74 | /// The current Date CompareAgainstDate 75 | public static let currentDate: Self = .init( 76 | .constant(.init()) 77 | ) 78 | 79 | // MARK: Typealias 80 | 81 | /// The Date Adding typealias representing a Tuple with Calendar Component and Value 82 | public typealias Adding = (component: Calendar.Component, value: Int) 83 | 84 | // MARK: Properties 85 | 86 | /// The CompareAgainst Date 87 | public let compareAgainst: CompareAgainst 88 | 89 | /// The optional Adding closure which takes in an EUDCC 90 | public let adding: ((EUDCC) -> Adding?)? 91 | 92 | // MARK: Initializer 93 | 94 | /// Creates a new instance of `CompareAgainstDate` 95 | /// - Parameters: 96 | /// - compareAgainst: The CompareAgainst Date 97 | /// - adding: The Adding closure which takes in an EUDCC 98 | public init( 99 | _ compareAgainst: CompareAgainst, 100 | adding: @escaping (EUDCC) -> Adding? 101 | ) { 102 | self.compareAgainst = compareAgainst 103 | self.adding = adding 104 | } 105 | 106 | /// Creates a new instance of `CompareAgainstDate` which takes in an optional constant `Adding` value 107 | /// - Parameters: 108 | /// - compareAgainst: The CompareAgainst Date 109 | /// - adding: The optional Adding value. Default value `nil` 110 | public init( 111 | _ compareAgainst: CompareAgainst, 112 | adding: Adding? = nil 113 | ) { 114 | self.compareAgainst = compareAgainst 115 | self.adding = adding.flatMap { adding in { _ in adding } } 116 | } 117 | 118 | } 119 | 120 | /// Compare two Dates using a given operator 121 | /// - Parameters: 122 | /// - lhsDate: The left-hand-side CompareAgainstDate 123 | /// - rhsDate: The right-hand-side CompareAgainstDate 124 | /// - operator: The operator closure 125 | /// - calendar: The Calendar. Default value `.current` 126 | /// - tag: The Tag. Default value `.init()` 127 | static func compare( 128 | lhsDate: CompareAgainstDate, 129 | rhsDate: CompareAgainstDate, 130 | operator: @escaping (Date, Date) -> Bool, 131 | using calendar: Calendar = .current, 132 | tag: Tag = .init() 133 | ) -> Self { 134 | .init(tag: tag) { eudcc in 135 | // Verify LHS and RHS Date values are available 136 | guard var lhsDateValue = lhsDate.compareAgainst.value(for: eudcc), 137 | var rhsDateValue = rhsDate.compareAgainst.value(for: eudcc) else { 138 | // Otherwise return false 139 | return false 140 | } 141 | // Check if LHS Date should be re-calculated 142 | if let lhsAddingClosure = lhsDate.adding { 143 | // Verify LHS Adding value is available 144 | guard let lhsAdding = lhsAddingClosure(eudcc) else { 145 | // Otherwise return false 146 | return false 147 | } 148 | // Verify updated LHS value for Adding is available 149 | guard let lhsUpdatedDateValue = calendar.date( 150 | byAdding: lhsAdding.component, 151 | value: lhsAdding.value, 152 | to: lhsDateValue 153 | ) else { 154 | // Otherwise return false 155 | return false 156 | } 157 | // Update LHS Date value 158 | lhsDateValue = lhsUpdatedDateValue 159 | } 160 | // Check if RHS Date should be re-calculated 161 | if let rhsAddingClosure = rhsDate.adding { 162 | // Verify LHS Adding value is available 163 | guard let rhsAdding = rhsAddingClosure(eudcc) else { 164 | // Otherwise return false 165 | return false 166 | } 167 | // Verify updated RHS value for Adding is available 168 | guard let rhsUpdatedDateValue = calendar.date( 169 | byAdding: rhsAdding.component, 170 | value: rhsAdding.value, 171 | to: rhsDateValue 172 | ) else { 173 | // Otherwise return false 174 | return false 175 | } 176 | // Update RHS Date value 177 | rhsDateValue = rhsUpdatedDateValue 178 | } 179 | // Return comparison result of operator 180 | return `operator`(lhsDateValue, rhsDateValue) 181 | } 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /Sources/EUDCCVerifier/Verifier/EUDCCVerifier.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | import Security 4 | 5 | // MARK: - EUDCCVerifier 6 | 7 | /// An EUDCC Verifier 8 | public struct EUDCCVerifier { 9 | 10 | // MARK: Properties 11 | 12 | /// The EUDCCTrustService 13 | let trustService: EUDCCTrustService 14 | 15 | // MARK: Initializer 16 | 17 | /// Creates a new instance of `EUDCCVerifier` 18 | /// - Parameter trustService: The EUDCCTrustService 19 | public init( 20 | trustService: EUDCCTrustService 21 | ) { 22 | self.trustService = trustService 23 | } 24 | 25 | } 26 | 27 | // MARK: - VerificationResult 28 | 29 | public extension EUDCCVerifier { 30 | 31 | /// The VerificationResult 32 | enum VerificationResult { 33 | /// Valid 34 | case success(EUDCC.TrustCertificate) 35 | /// Invalid 36 | case invalid 37 | /// Failure 38 | case failure(Failure) 39 | } 40 | 41 | } 42 | 43 | // MARK: - Failure 44 | 45 | public extension EUDCCVerifier { 46 | 47 | /// The EUDCCVerifier Failure 48 | enum Failure: Error { 49 | /// Malformed Certificate KeyID from EUDCC 50 | case malformedCertificateKeyID(EUDCC) 51 | /// TrustService Error 52 | case trustServiceError(Error) 53 | /// No matching TrustCertificate for EUDCC KeyID 54 | case noMatchingTrustCertificate(EUDCC.TrustCertificate.KeyID) 55 | } 56 | 57 | } 58 | 59 | // MARK: - Verify EUDCC 60 | 61 | public extension EUDCCVerifier { 62 | 63 | /// Verify `EUDCC` and retrieve a `VerificationResult` 64 | /// - Parameters: 65 | /// - eudcc: The `EUDCC` that should be verified 66 | /// - completion: The completion handler taking in the `VerificationResult` 67 | func verify( 68 | eudcc: EUDCC, 69 | completion: @escaping (VerificationResult) -> Void 70 | ) { 71 | // Retrieve TrustCertificates from TrustService 72 | self.trustService.getTrustCertificates { result in 73 | /// Complete with VerificationResult 74 | /// - Parameter verificationResult: The VerificationResult 75 | func complete( 76 | with verificationResult: VerificationResult 77 | ) { 78 | // Check if Thread is MainThred 79 | if Thread.isMainThread { 80 | // Invoke completion with Verification 81 | completion(verificationResult) 82 | } else { 83 | // Dispatch on MainThread 84 | DispatchQueue.main.async { 85 | // Invoke completion with Verification 86 | completion(verificationResult) 87 | } 88 | } 89 | } 90 | // Switch on Result 91 | switch result { 92 | case .success(let trustCertificates): 93 | // Verify EUDCC against TrustCertificates 94 | let verificationResult = self.verify( 95 | eudcc: eudcc, 96 | against: trustCertificates 97 | ) 98 | // Complete with VerificationResult 99 | complete(with: verificationResult) 100 | case .failure(let error): 101 | // Complete with failure 102 | complete(with: .failure(.trustServiceError(error))) 103 | } 104 | } 105 | } 106 | 107 | } 108 | 109 | // MARK: - Verify against TrustCertificates 110 | 111 | public extension EUDCCVerifier { 112 | 113 | /// Verify EUDCC against a given sequence of EUDCC TrustCertificates 114 | /// - Parameters: 115 | /// - eudcc: The EUDCC 116 | /// - trustCertificates: The sequence of EUDCC TrustCertificate 117 | /// - Returns: A VerificationResult 118 | func verify( 119 | eudcc: EUDCC, 120 | against trustCertificates: Certificates 121 | ) -> VerificationResult where Certificates.Element == EUDCC.TrustCertificate { 122 | // Verify EUDCC KeyID is available from CryptographicSignature 123 | guard let eudccKeyID = EUDCC.TrustCertificate.KeyID( 124 | cryptographicSignature: eudcc.cryptographicSignature 125 | ) else { 126 | // Otherwise complete with failure 127 | return .failure(.malformedCertificateKeyID(eudcc)) 128 | } 129 | // Retrieve matching TrustCertificates by KeyID 130 | let matchingTrustCertificates = trustCertificates.filter { $0.keyID == eudccKeyID } 131 | // Verify matching TrustCertificates are not empty 132 | guard !matchingTrustCertificates.isEmpty else { 133 | // Otherwise complete with failure 134 | return .failure(.noMatchingTrustCertificate(eudccKeyID)) 135 | } 136 | // Retrieve EUDCC Signature bytes 137 | let eudccSignature = Data(eudcc.cryptographicSignature.signature) 138 | // Initialize EUDCC SignedPayload 139 | let eudccSignedPayload = EUDCC.SignedPayload(cryptographicSignature: eudcc.cryptographicSignature) 140 | // Map matching SignerCertificates to VerificationCandidates 141 | let verificationCandidates: [EUDCC.VerificationCandidate] = matchingTrustCertificates 142 | .map { trustCertificate in 143 | .init( 144 | signature: eudccSignature, 145 | signedPayload: eudccSignedPayload, 146 | trustCertificate: trustCertificate 147 | ) 148 | } 149 | // Verify the first VerificationCandidate that is verified successfully is available 150 | guard let matchingVerificationCandidate = verificationCandidates.first(where: self.verify) else { 151 | // Otherwise complete with failure as no VerificationCandidate verified successfully 152 | return .invalid 153 | } 154 | // Complete with success 155 | return .success(matchingVerificationCandidate.trustCertificate) 156 | } 157 | 158 | } 159 | 160 | // MARK: - Verify EUDCC VerificationCandidate 161 | 162 | public extension EUDCCVerifier { 163 | 164 | /// Verify EUDCC VerificationCandidate 165 | /// - Parameter candidate: The EUDCC VerificationCandidate that should be verified 166 | /// - Returns: Bool value if VerificationCandidate verified successfully 167 | func verify( 168 | candidate: EUDCC.VerificationCandidate 169 | ) -> Bool { 170 | // Verify Public Key of TrustCertificate is available 171 | guard let publicKey = candidate.trustCertificate.publicKey else { 172 | // Oterhwise return false 173 | return false 174 | } 175 | // Initialize mutable Signature 176 | var signature = candidate.signature 177 | // Declare SecKeyAlgorithm 178 | let algorithm: Security.SecKeyAlgorithm 179 | // Check PublicKey Algorithm 180 | if Security.SecKeyIsAlgorithmSupported( 181 | publicKey, 182 | .verify, 183 | .ecdsaSignatureMessageX962SHA256 184 | ) { 185 | // Use X962 186 | algorithm = .ecdsaSignatureMessageX962SHA256 187 | // Verify encoded ASN1 Signature is available 188 | guard let encodedASN1Signature = signature.encodedASN1() else { 189 | // Otherwise return falses 190 | return false 191 | } 192 | // Mutate Signature with encoded ASN1 193 | signature = encodedASN1Signature 194 | } else if Security.SecKeyIsAlgorithmSupported( 195 | publicKey, 196 | .verify, .rsaSignatureMessagePSSSHA256 197 | ) { 198 | // Use PSS 199 | algorithm = .rsaSignatureMessagePSSSHA256 200 | } else { 201 | // Otherwise return false as Algorithm is not supported 202 | return false 203 | } 204 | // Declare Error 205 | var error: Unmanaged? 206 | // Verify Signature 207 | let verificationResult = Security.SecKeyVerifySignature( 208 | publicKey, 209 | algorithm, 210 | candidate.signedPayload.rawValue as NSData, 211 | signature as NSData, 212 | &error 213 | ) 214 | // Release Error 215 | error?.release() 216 | // Return VerificationResult 217 | return verificationResult 218 | } 219 | 220 | } 221 | -------------------------------------------------------------------------------- /Sources/EUDCCValidator/Models/EUDCC+ValidationRule+Defaults.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | 4 | // MARK: - Constant 5 | 6 | public extension EUDCC.ValidationRule { 7 | 8 | /// Constant ValidationRule that will always return the given Bool result value 9 | /// - Parameter result: The Bool resul value 10 | static func constant( 11 | _ result: Bool 12 | ) -> Self { 13 | .init(tag: "\(result)") { _ in result } 14 | } 15 | 16 | } 17 | 18 | // MARK: - EUDCC Content Type 19 | 20 | public extension EUDCC.ValidationRule { 21 | 22 | /// Has vaccination content ValidationRule 23 | static var isVaccination: Self { 24 | self.compare( 25 | value: .keyPath(\.vaccination), 26 | to: .constant(nil), 27 | operator: !=, 28 | tag: "isVaccination" 29 | ) 30 | } 31 | 32 | /// Has test content ValidationRule 33 | static var isTest: Self { 34 | self.compare( 35 | value: .keyPath(\.test), 36 | to: .constant(nil), 37 | operator: !=, 38 | tag: "isTest" 39 | ) 40 | } 41 | 42 | /// Has recover content ValidationRule 43 | static var isRecovery: Self { 44 | self.compare( 45 | value: .keyPath(\.recovery), 46 | to: .constant(nil), 47 | operator: !=, 48 | tag: "isRecovery" 49 | ) 50 | } 51 | 52 | } 53 | 54 | // MARK: - EUDCC Vaccination 55 | 56 | public extension EUDCC.ValidationRule { 57 | 58 | /// Has received all vaccination doses 59 | static var isVaccinationComplete: Self { 60 | .isVaccination 61 | && .compare( 62 | value: .keyPath(\.vaccination?.doseNumber), 63 | to: .keyPath(\.vaccination?.totalSeriesOfDoses), 64 | operator: ==, 65 | tag: "isVaccinationComplete" 66 | ) 67 | } 68 | 69 | /// Vaccination can be considered as fully immunized 70 | /// - Parameters: 71 | /// - minimumDaysPast: The amount of minimum days past since the last vaccination. Default value `15` 72 | /// - calendar: The Calendar that should be used. Default value `.current` 73 | static func isFullyImmunized( 74 | minimumDaysPast: Int = 15, 75 | using calendar: Calendar = .current 76 | ) -> Self { 77 | .isVaccinationComplete 78 | && .compare( 79 | lhsDate: .currentDate, 80 | rhsDate: .init( 81 | .keyPath(\.vaccination?.dateOfVaccination), 82 | adding: (.day, minimumDaysPast) 83 | ), 84 | operator: >, 85 | using: calendar, 86 | tag: "isFullyImmunized-after-\(minimumDaysPast)-days" 87 | ) 88 | } 89 | 90 | /// Validates if the vaccination expired by comparing if the current Date 91 | /// is greater than the date of vaccination added by the `maximumDaysSinceVaccinationDate` 92 | /// - Parameters: 93 | /// - maximumDaysSinceVaccinationDate: The maximum days since date of vaccination. Default value `365` 94 | /// - calendar: The Calendar. Default value `.current` 95 | static func isVaccinationExpired( 96 | maximumDaysSinceVaccinationDate: Int = 365, 97 | using calendar: Calendar = .current 98 | ) -> Self { 99 | .isVaccination 100 | && .compare( 101 | lhsDate: .currentDate, 102 | rhsDate: .init( 103 | .keyPath(\.vaccination?.dateOfVaccination), 104 | adding: (.day, maximumDaysSinceVaccinationDate) 105 | ), 106 | operator: >, 107 | using: calendar, 108 | tag: "is-vaccination-expired-\(maximumDaysSinceVaccinationDate)-days" 109 | ) 110 | } 111 | 112 | /// Validates if EUDCC contains a Vaccination and the `VaccineMedicinalProduct` value 113 | /// is contained in the `WellKnownValue`enumeration 114 | static var isWellKnownVaccineMedicinalProduct: Self { 115 | .vaccineMedicinalProductIsOneOf( 116 | EUDCC.Vaccination.VaccineMedicinalProduct.WellKnownValue.allCases 117 | ) 118 | } 119 | 120 | /// Validates if the `VaccineMedicinalProduct` is contained in the given Sequence of VaccineMedicinalProduct WellKnownValues 121 | /// - Parameter validVaccineMedicinalProducts: The VaccineMedicinalProduct WellKnownValue Sequence 122 | static func vaccineMedicinalProductIsOneOf( 123 | _ validVaccineMedicinalProducts: Vaccines 124 | ) -> Self where Vaccines.Element == EUDCC.Vaccination.VaccineMedicinalProduct.WellKnownValue { 125 | .isVaccination 126 | && .init( 127 | tag: "isVaccineMedicinalProduct-one-of-\(validVaccineMedicinalProducts)" 128 | ) { eudcc in 129 | // Verify WellKnownValue of VaccineMedicinalProduct is available 130 | guard let vaccineMedicinalProductWellKnownValue = eudcc.vaccination?.vaccineMedicinalProduct.wellKnownValue else { 131 | // Otherwise return false 132 | return false 133 | } 134 | // Return result if VaccineMedicinalProduct WellKnownValue is contained in the given Sequence 135 | return validVaccineMedicinalProducts.contains(vaccineMedicinalProductWellKnownValue) 136 | } 137 | } 138 | 139 | } 140 | 141 | // MARK: - EUDCC Test 142 | 143 | public extension EUDCC.ValidationRule { 144 | 145 | /// TestResult of Test is positive 146 | static var isTestedPositive: Self { 147 | .isTest 148 | && .compare( 149 | value: .keyPath(\.test?.testResult.value), 150 | to: .constant(EUDCC.Test.TestResult.WellKnownValue.positive.rawValue), 151 | operator: ==, 152 | tag: "isTestedPositive" 153 | ) 154 | } 155 | 156 | /// TestResult of Test is negative 157 | static var isTestedNegative: Self { 158 | .isTest 159 | && .compare( 160 | value: .keyPath(\.test?.testResult.value), 161 | to: .constant(EUDCC.Test.TestResult.WellKnownValue.negative.rawValue), 162 | operator: ==, 163 | tag: "isTestedNegative" 164 | ) 165 | } 166 | 167 | /// Is Test valid 168 | /// - Parameters: 169 | /// - maximumHoursPast: The maximum hours past since date of sample collection. Default value `PCR: 72 | RAPID: 48` 170 | /// - calendar: The Calendar that should be used. Default value `.current` 171 | static func isTestValid( 172 | maximumHoursPast: @escaping (EUDCC.Test.TestType.WellKnownValue) -> Int = { $0 == .pcr ? 72 : 48 }, 173 | using calendar: Calendar = .current 174 | ) -> Self { 175 | .isTest 176 | && .compare( 177 | lhsDate: .currentDate, 178 | rhsDate: .init( 179 | .keyPath(\.test?.dateOfSampleCollection), 180 | adding: { eudcc in 181 | // Verify TestType WellKnownValue is available 182 | guard let testTypeWellKnownValue = eudcc.test?.typeOfTest.wellKnownValue else { 183 | // Otherwise return nil 184 | return nil 185 | } 186 | // Return adding hour with maximum hours past for TestType WellKnownValue 187 | return (.hour, maximumHoursPast(testTypeWellKnownValue)) 188 | } 189 | ), 190 | operator: <=, 191 | using: calendar, 192 | tag: "isTestValid" 193 | ) 194 | } 195 | 196 | } 197 | 198 | // MARK: - EUDCC Recovery 199 | 200 | public extension EUDCC.ValidationRule { 201 | 202 | /// Is Recovery valid 203 | static var isRecoveryValid: Self { 204 | .isRecovery 205 | && .init(tag: "isRecoveryValid") { eudcc in 206 | // Verify Recovery is available 207 | guard let recovery = eudcc.recovery else { 208 | // Otherwise return false 209 | return false 210 | } 211 | // Initialize valid Date Range 212 | let validDateRange = recovery.certificateValidFrom...recovery.certificateValidUntil 213 | // Return Bool value if current Date is contained in valid Date Range 214 | return validDateRange.contains(.init()) 215 | } 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /Sources/EUDCCDecoder/Decoder/EUDCCDecoder.swift: -------------------------------------------------------------------------------- 1 | import EUDCC 2 | import Foundation 3 | import SwiftCBOR 4 | 5 | // MARK: - EUDCCDecoder 6 | 7 | /// A `EUDCC` Decoder 8 | public struct EUDCCDecoder { 9 | 10 | // MARK: Properties 11 | 12 | /// The EUDCC Prefix 13 | private let eudccPrefix: String 14 | 15 | /// The EUDCC JSONDecoder 16 | private let eudccJSONDecoder: JSONDecoder 17 | 18 | // MARK: Initializer 19 | 20 | /// Creates a new instance of `EUDCCDecoder` 21 | /// - Parameters: 22 | /// - eudccPrefix: The EUDCC Prefix. Default value `HC1:` 23 | /// - eudccJSONDecoder: The EUDCC JSONDecoder. Default value `.init()` 24 | public init( 25 | eudccPrefix: String = "HC1:", 26 | eudccJSONDecoder: JSONDecoder = .init() 27 | ) { 28 | self.eudccPrefix = eudccPrefix 29 | self.eudccJSONDecoder = eudccJSONDecoder 30 | } 31 | 32 | } 33 | 34 | // MARK: - DecodingError 35 | 36 | public extension EUDCCDecoder { 37 | 38 | /// The EUDCC DecodingError 39 | enum DecodingError: Error { 40 | /// Base45 decoding Error 41 | case base45DecodingError(Error) 42 | /// CBOR decoding Error 43 | case cborDecodingError(Error) 44 | /// Malformed CBOR Error 45 | case malformedCBORError(Data) 46 | /// CBOR Processing Error 47 | case cborProcessingError(CBORProcessingError) 48 | /// COSE Payload CBOR Decoding Error 49 | case coseCBORDecodingError(Error?) 50 | /// COSE Payload JSON Data Error 51 | case cosePayloadJSONDataError(Error?) 52 | /// EUDCC JSONDecoding Error 53 | case eudccJSONDecodingError(Error) 54 | } 55 | 56 | /// The CBORProcessingError 57 | enum CBORProcessingError: Error { 58 | /// Contents is missing 59 | case contentMissing 60 | /// Protected parameter is missing 61 | case protectedParameterMissing 62 | /// Unprotected paramter is missing 63 | case unprotectedParameterMissing 64 | /// Payload parameter is missing 65 | case payloadParameterMissing 66 | /// Signature parameter is missing 67 | case signatureParameterMissing 68 | } 69 | 70 | } 71 | 72 | // MARK: - Decode 73 | 74 | public extension EUDCCDecoder { 75 | 76 | /// Decode EUDCC from EUDCC Base-45 encoded `Data` 77 | /// - Parameter base45EncodedData: The EUDCC Base-45 encoded `Data` 78 | /// - Returns: A Result contains either the successfully decoded EUDCC or an DecodingError 79 | func decode( 80 | from base45EncodedData: Data 81 | ) -> Result { 82 | self.decode( 83 | from: .init( 84 | decoding: base45EncodedData, 85 | as: UTF8.self 86 | ) 87 | ) 88 | } 89 | 90 | /// Decode EUDCC from EUDCC Base-45 encoded `String` 91 | /// - Parameter base45EncodedString: The EUDCC Base-45 encoded `String` 92 | /// - Returns: A Result contains either the successfully decoded EUDCC or an DecodingError 93 | func decode( 94 | from base45EncodedString: String 95 | ) -> Result { 96 | // Drop EUDCC Prefix 97 | self.dropPrefixIfNeeded( 98 | from: base45EncodedString 99 | ) 100 | // Decode Base-45 101 | .flatMap(self.decodeBase45) 102 | // Decompress Data 103 | .flatMap(self.decompress) 104 | // Decode CBOR 105 | .flatMap(self.decodeCBOR) 106 | // Decode COSE 107 | .flatMap(self.decodeCOSE) 108 | // Decode EUDCC 109 | .flatMap(self.decodeEUDCC) 110 | // Set Base-45 Representation 111 | .map { eudcc in 112 | // Initialize mutable EUDCC 113 | var eudcc = eudcc 114 | // Set Base-45 Representation 115 | eudcc.mutate( 116 | base45Representation: base45EncodedString 117 | ) 118 | // Return updated EUDCC 119 | return eudcc 120 | } 121 | } 122 | 123 | } 124 | 125 | // MARK: - Drop Prefix if needed 126 | 127 | private extension EUDCCDecoder { 128 | 129 | /// Drop `EUDCC` prefix if needed 130 | /// - Parameter string: The String 131 | func dropPrefixIfNeeded( 132 | from string: String 133 | ) -> Result { 134 | // Initialize mutable String 135 | var string = string 136 | // Check if starts with EUDCC prefix 137 | if string.starts(with: self.eudccPrefix) { 138 | // Drop EUDCC prefix 139 | string = .init( 140 | string.dropFirst(self.eudccPrefix.count) 141 | ) 142 | } 143 | // Return success with dropped EUDCC prefix 144 | return .success(string) 145 | } 146 | 147 | } 148 | 149 | // MARK: - Decode Base-45 150 | 151 | private extension EUDCCDecoder { 152 | 153 | /// Decode a given String to a valid Base-45 Data object 154 | /// - Parameter base45EncodedString: The Base-45 encoded String 155 | func decodeBase45( 156 | base45EncodedString: String 157 | ) -> Result { 158 | do { 159 | // Try to decode String to Base-45 Data 160 | return .success(try .init(base45Encoded: base45EncodedString)) 161 | } catch { 162 | // Return Base45 Decoding Error 163 | return .failure(.base45DecodingError(error)) 164 | } 165 | } 166 | 167 | } 168 | 169 | // MARK: - Decompress 170 | 171 | private extension EUDCCDecoder { 172 | 173 | /// Decompress Data 174 | /// - Parameter data: The Data object that should be decompressed 175 | func decompress( 176 | data: Data 177 | ) -> Result { 178 | .success(data.decompressed()) 179 | } 180 | 181 | } 182 | 183 | // MARK: - Decode CBOR 184 | 185 | private extension EUDCCDecoder { 186 | 187 | /// Decode CBOR 188 | /// - Parameter data: The Data object used to decode CBOR 189 | func decodeCBOR( 190 | data: Data 191 | ) -> Result { 192 | // Initialize CBORDecoder 193 | let cborDecoder = SwiftCBOR.CBORDecoder( 194 | input: [UInt8](data) 195 | ) 196 | do { 197 | // Try to decodeItem and verify CBOR is available 198 | guard let cbor = try cborDecoder.decodeItem() else { 199 | // Otherwise return malformed CBOR Error 200 | return .failure(.malformedCBORError(data)) 201 | } 202 | // Return success with decoded CBOR 203 | return .success(cbor) 204 | } catch { 205 | // Return CBOR decoding Error 206 | return .failure(.cborDecodingError(error)) 207 | } 208 | } 209 | 210 | } 211 | 212 | // MARK: - Decode COSE 213 | 214 | private extension EUDCCDecoder { 215 | 216 | /// Decode COSE 217 | /// - Parameter input: The CBOR object 218 | func decodeCOSE( 219 | cbor: SwiftCBOR.CBOR 220 | ) -> Result { 221 | // Verify Content is available 222 | guard case .tagged(_, let value) = cbor, 223 | case .array(let contents) = value else { 224 | return .failure(.cborProcessingError(.contentMissing)) 225 | } 226 | // Verify protected parameter is available 227 | guard contents.indices.contains(0), 228 | case .byteString(let protected) = contents[0] else { 229 | return .failure(.cborProcessingError(.protectedParameterMissing)) 230 | } 231 | // Verify unprotected parameter is available 232 | guard contents.indices.contains(1), 233 | case .map(let unprotected) = contents[1] else { 234 | return .failure(.cborProcessingError(.unprotectedParameterMissing)) 235 | } 236 | // Verify payload paramter is available 237 | guard contents.indices.contains(2), 238 | case .byteString(let payload) = contents[2] else { 239 | return .failure(.cborProcessingError(.payloadParameterMissing)) 240 | } 241 | // Verify signature parameter is available 242 | guard contents.indices.contains(3), 243 | case .byteString(let signature) = contents[3] else { 244 | return .failure(.cborProcessingError(.signatureParameterMissing)) 245 | } 246 | // Return success with EUDCC CryptographicSignature 247 | return .success(.init( 248 | protected: .init(protected), 249 | unprotected: .init( 250 | uniqueKeysWithValues: unprotected.map { key, value in 251 | (.init(key.encode()), .init(value.encode())) 252 | } 253 | ), 254 | payload: .init(payload), 255 | signature: .init(signature) 256 | )) 257 | } 258 | 259 | } 260 | 261 | // MARK: - Decode EUDCC 262 | 263 | private extension EUDCCDecoder { 264 | 265 | /// Decode EUDCC 266 | /// - Parameter cryptographicSignature: The EUDCC CryptographicSignature 267 | func decodeEUDCC( 268 | cryptographicSignature: EUDCC.CryptographicSignature 269 | ) -> Result { 270 | // Declare CBOR Payload 271 | let cborPayload: SwiftCBOR.CBOR 272 | do { 273 | // Try to decode COSE Payload as CBOR and verify Item is available 274 | guard let cbor = try SwiftCBOR.CBORDecoder( 275 | input: [UInt8](cryptographicSignature.payload) 276 | ).decodeItem() else { 277 | // Otherwise return COSE Payload JSON Data Error 278 | return .failure(.coseCBORDecodingError(nil)) 279 | } 280 | // Initialize CBOR Payload 281 | cborPayload = cbor 282 | } catch { 283 | // Return COSE Payload JSON Data Error 284 | return .failure(.coseCBORDecodingError(error)) 285 | } 286 | // Verify Dictionary Representation from CBOR Payload is available 287 | guard let dictionaryRepresentation = cborPayload.dictionaryRepresentation() else { 288 | // Otherwise return COSE Payload JSON Data Error 289 | return .failure(.cosePayloadJSONDataError(nil)) 290 | } 291 | // Declare Payload JSON Data 292 | let payloadJSONData: Data 293 | do { 294 | // Try to serialize Dictionary Representation as JSON Data 295 | payloadJSONData = try JSONSerialization.data( 296 | withJSONObject: dictionaryRepresentation 297 | ) 298 | } catch { 299 | // Return COSE Payload JSON Data Error 300 | return .failure(.cosePayloadJSONDataError(error)) 301 | } 302 | // Declare EUDCC 303 | var eudcc: EUDCC 304 | do { 305 | // Try to decode EUDCC 306 | eudcc = try self.eudccJSONDecoder.decode( 307 | EUDCC.self, 308 | from: payloadJSONData 309 | ) 310 | } catch { 311 | // Return EUDCC JSONDecoding Error 312 | return .failure(.eudccJSONDecodingError(error)) 313 | } 314 | // Mutate CryptographicSignature 315 | eudcc.mutate( 316 | cryptographicSignature: cryptographicSignature 317 | ) 318 | // Return success with decoded EUDCC 319 | return .success(eudcc) 320 | } 321 | 322 | } 323 | 324 | // MARK: - EUDCC+mutate 325 | 326 | private extension EUDCC { 327 | 328 | /// Mutate Base-45 representation 329 | /// - Parameter base45Representation: The new Base-45 representation 330 | mutating func mutate( 331 | base45Representation: String 332 | ) { 333 | self = .init( 334 | issuer: self.issuer, 335 | issuedAt: self.issuedAt, 336 | expiresAt: self.expiresAt, 337 | schmemaVersion: self.schmemaVersion, 338 | dateOfBirth: self.dateOfBirth, 339 | name: self.name, 340 | content: self.content, 341 | cryptographicSignature: self.cryptographicSignature, 342 | base45Representation: base45Representation 343 | ) 344 | } 345 | 346 | /// Mutate CryptographicSignature 347 | /// - Parameter cryptographicSignature: The new CryptographicSignature 348 | mutating func mutate( 349 | cryptographicSignature: EUDCC.CryptographicSignature 350 | ) { 351 | self = .init( 352 | issuer: self.issuer, 353 | issuedAt: self.issuedAt, 354 | expiresAt: self.expiresAt, 355 | schmemaVersion: self.schmemaVersion, 356 | dateOfBirth: self.dateOfBirth, 357 | name: self.name, 358 | content: self.content, 359 | cryptographicSignature: cryptographicSignature, 360 | base45Representation: self.base45Representation 361 | ) 362 | } 363 | 364 | } 365 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | logo 4 |

5 | 6 |

EU Digital COVID Certificate Kit

7 | 8 |

9 | A Swift Package to decode, verify and validate EU Digital COVID Certificates
for iOS, tvOS, watchOS and macOS 10 |

11 | 12 |

13 | 14 | Swift 5.4 15 | 16 | 17 | Documentation 18 | 19 | 20 | Twitter 21 | 22 |

23 | 24 | ## Disclaimer 25 | 26 | > The EUDCCKit is not an offical implementation of the EU Digital COVID Certificate 27 | 28 | ## Features 29 | 30 | - [x] Easily decode an EU Digital COVID Certificate 🧾 31 | - [x] Verify cryptographic signature 🔐 32 | - [x] Certificate validation ✅ 33 | 34 | ## Installation 35 | 36 | ### Swift Package Manager 37 | 38 | To integrate using Apple's [Swift Package Manager](https://swift.org/package-manager/), add the following as a dependency to your `Package.swift`: 39 | 40 | ```swift 41 | dependencies: [ 42 | .package(url: "https://github.com/SvenTiigi/EUDCCKit.git", from: "0.0.1") 43 | ] 44 | ``` 45 | 46 | Or navigate to your Xcode project then select `Swift Packages`, click the “+” icon and search for `EUDCCKit`. 47 | 48 | ## Usage 49 | 50 | The `EUDCCKit` Swift Package is made of four distinct libraries to decode, verify and validate an EU Digital COVID Certificate. 51 | 52 | ### EUDCC 53 | 54 | The `EUDCC` library contains the model definition of the EU Digital COVID Certificate 55 | 56 | ```swift 57 | import EUDCC 58 | 59 | // The EU Digital COVID Certificate model 60 | let eudcc: EUDCC 61 | 62 | // Access content of EUDCC 63 | switch eudcc.content { 64 | case .vaccination(let vaccination): 65 | print("Vaccination", vaccination) 66 | case .test(let test): 67 | print("Test", test) 68 | case .recovery(let recovery): 69 | print("Recovery", recovery) 70 | } 71 | ``` 72 | 73 | > Head over to the [advanced section](https://github.com/SvenTiigi/EUDCCKit#eudcc-1) to learn more. 74 | 75 | ### EUDDCDecoder 76 | 77 | The `EUDCCDecoder` library provides an `EUDCCDecoder` object which is capabale of decoding a Base-45 string reperesentation of the EU Digital COVID Certificate which is mostly embedded in a QR-Code. 78 | 79 | ```swift 80 | import EUDCCDecoder 81 | 82 | // Initialize an EUDCCDecoder 83 | let decoder = EUDCCDecoder() 84 | 85 | // The Base-45 encoded EU Digital COVID Certificate from a QR-Code 86 | let qrCodeContent = "HC1:..." 87 | 88 | // Decode EUDCC from QR-Code 89 | let decodingResult = decoder.decode(from: qrCodeContent) 90 | 91 | // Switch on decoding result 92 | switch decodingResult { 93 | case .success(let eudcc): 94 | // Successfully decoded Digital COVID Certificate 95 | print("EU Digital COVID Certificate", eudcc) 96 | case .failure(let decodingError): 97 | // Decoding failed with error 98 | print("Failed to decode EUDCC", decodingError) 99 | } 100 | ``` 101 | 102 | > Head over to the [advanced section](https://github.com/SvenTiigi/EUDCCKit#euddcdecoder-1) to learn more. 103 | 104 | ### EUDCCVerifier 105 | 106 | The `EUDCCVerifier` library provides an `EUDCCVerifier` object which can be used to verify the cryptographic signature of the EU Digital COVID Certificate. 107 | 108 | ```swift 109 | import EUDCCVerifier 110 | 111 | // Initialize an EUDDCCVerifier 112 | let verifier = EUDCCVerifier( 113 | trustService: EUCentralEUDCCTrustService() 114 | ) 115 | 116 | // Verify EU Digital COVID Certificate 117 | verifier.verify(eudcc: eudcc) { verificationResult in 118 | // Switch on verification result 119 | switch verificationResult { 120 | case .success(let trustCertificate): 121 | print("Cryptographically valid", trustCertificate) 122 | case .invald: 123 | print("Invalid EUDCC") 124 | case .failure(let error): 125 | print("Error occured during verification", error) 126 | } 127 | } 128 | ``` 129 | 130 | > Head over to the [advanced section](https://github.com/SvenTiigi/EUDCCKit#eudccverifier-1) to learn more. 131 | 132 | ### EUDCCValidator 133 | 134 | The `EUDCCValidator` library provides an `EUDCCValidator` object which can be used to validate the EU Digital COVID Certifiate based on given rules. 135 | 136 | ```swift 137 | import EUDCCValidator 138 | 139 | // Initialize an EUDCCValidator 140 | let validator = EUDCCValidator() 141 | 142 | // Validate EU Digital COVID Certificate 143 | let validationResult = validator.validate( 144 | eudcc: eudcc, 145 | rule: .isFullyImmunized() && !.isVaccinationExpired() 146 | ) 147 | 148 | // Switch on validation result 149 | switch validationResult { 150 | case .success: 151 | // Successfully validated EU Digital COVID Certificate 152 | print("Successfully validated") 153 | case .failure(let validationError): 154 | // Validation failure 155 | print("Validation failed", validationError) 156 | } 157 | ``` 158 | 159 | > Head over to the [advanced section](https://github.com/SvenTiigi/EUDCCKit#eudccvalidator-1) to learn more. 160 | 161 | ## Advanced 162 | 163 | ### EUDCC 164 | 165 | #### Content 166 | 167 | Beside the `content` property of an `EUDCC` you can make use of the following convenience properties to check if the `EUDCC` contains a vaccination, test or recovery object. 168 | 169 | ```swift 170 | import EUDCC 171 | 172 | // Vaccination 173 | let vaccination: EUDCC.Vaccination? = eudcc.vaccination 174 | 175 | // Test 176 | let test: EUDCC.Test? = eudcc.test 177 | 178 | // Recovery 179 | let recovery: EUDCC.Recovery? = eudcc.recovery 180 | ``` 181 | 182 | #### Well-Known-Value 183 | 184 | Each of the following objects are exposing a `WellKnownValue` enumeration which can be used to retrieve more detailed information about a certain value: 185 | 186 | - `EUDCC.DiseaseAgentTargeted` 187 | - `EUDCC.Test.TestResult` 188 | - `EUDCC.Test.TestType` 189 | - `EUDCC.Vaccination.VaccineMarketingAuthorizationHolder` 190 | - `EUDCC.Vaccination.VaccineMedicinalProduct` 191 | - `EUDCC.Vaccination.VaccineOrProphylaxis` 192 | 193 | ```swift 194 | import EUDCC 195 | 196 | let vaccineMedicinalProduct: EUDCC.Vaccination.VaccineMedicinalProduct 197 | 198 | // Switch on WellKnownValue of VaccineMedicinalProduct 199 | switch vaccineMedicinalProduct.wellKnownValue { 200 | case .covid19VaccineModerna: 201 | break 202 | case .vaxzevria: 203 | break 204 | default: 205 | break 206 | } 207 | ``` 208 | 209 | #### Encoding 210 | 211 | The `EUDCC` contains two properties `cryptographicSignature` and `base45Representation` which are convenience objects that are not an offical part of the EU Digital COVID Certificate JSON Schema. 212 | 213 | If you wish to skip those properties when encoding an `EUDCC` you can set the following `userInfo` configuration to a `JSONEncoder`. 214 | 215 | ```swift 216 | import EUDCC 217 | 218 | let encoder = JSONEncoder() 219 | 220 | encoder.userInfo = [ 221 | // Skip encoding CryptographicSignature 222 | EUDCC.EncoderUserInfoKeys.skipCryptographicSignature: true, 223 | // Skip encoding Base-45 representation 224 | EUDCC.EncoderUserInfoKeys.skipBase45Representation: true, 225 | ] 226 | 227 | let jsonData = try encoder.encode(eudcc) 228 | ``` 229 | 230 | ### EUDDCDecoder 231 | 232 | #### Decoding 233 | 234 | The `EUDCCDecoder` supports decoding a Base-45 encoded `String` and `Data` object. 235 | 236 | ```swift 237 | import EUDCCDecoder 238 | 239 | let eudccDecoder = EUDCCDecoder() 240 | 241 | // Decode from Base-45 encoded String 242 | let eudccBase45EncodedString: String 243 | let stringDecodingResult = eudccDecoder.decode( 244 | from: eudccBase45EncodedString 245 | ) 246 | 247 | // Decode from Base-45 encoded Data 248 | let eudccBase45EncodedData: Data 249 | let dataDecodingResult = eudccDecoder.decode( 250 | from: eudccBase45EncodedData 251 | ) 252 | ``` 253 | 254 | #### Convenience decoding 255 | 256 | By importing the `EUDCCDecoder` library the `EUDCC` object will be extended with a static `decode` function. 257 | 258 | ```swift 259 | import EUDCCDecoder 260 | 261 | let decodingResult = EUDCC.decode(from: "HC1:...") 262 | ``` 263 | 264 | ### EUDCCVerifier 265 | 266 | #### EUDCCTrustService 267 | 268 | In order to verify an `EUDCC` the `EUDCCVerifier` needs to be instantiated with an instance of an `EUDCCTrustService` which is used to retrieve the trust certificates. 269 | 270 | ```swift 271 | import EUDCC 272 | import EUDCCVerifier 273 | 274 | struct SpecificEUDCCTrustService: EUDCCTrustService { 275 | 276 | /// Retrieve EUDCC TrustCertificates 277 | /// - Parameter completion: The completion closure 278 | func getTrustCertificates( 279 | completion: @escaping (Result<[EUDCC.TrustCertificate], Error>) -> Void 280 | ) { 281 | // TODO: Retrieve TrustCertificates and invoke completion handler 282 | } 283 | 284 | } 285 | 286 | let eudccVerifier = EUDCCVerifier( 287 | trustService: SpecificEUDCCTrustService() 288 | ) 289 | ``` 290 | 291 | The `EUDCCKit` comes along with two pre defined `EUDCCTrustService` implementations: 292 | 293 | - `EUCentralEUDCCTrustService` 294 | - `RobertKochInstituteEUDCCTrustService` 295 | 296 | If you wish to retrieve certificates from multiple `EUDCCTrustService` implementation you can make use of the `GroupableEUDCCTrustService`: 297 | 298 | ```swift 299 | let trustService = GroupableEUDCCTrustService( 300 | trustServices: [ 301 | EUCentralEUDCCTrustService(), 302 | RobertKochInstituteEUDCCTrustService() 303 | ] 304 | ) 305 | 306 | trustService.getTrustCertificates { certificates in 307 | // ... 308 | } 309 | ``` 310 | 311 | #### Convenience verification 312 | 313 | By importing the `EUDCCVerifier` library the `EUDCC` object will be extended with a `verify` function. 314 | 315 | ```swift 316 | import EUDCC 317 | import EUDCCVerifier 318 | 319 | let eudcc: EUDCC 320 | 321 | eudcc.verify( 322 | using: EUDCCVerifier( 323 | trustService: EUCentralEUDCCTrustService() 324 | ) 325 | ) { verificationResult in 326 | switch verificationResult { 327 | case .success(let trustCertificate): 328 | break 329 | case .invald: 330 | break 331 | case .failure(let error): 332 | break 333 | } 334 | } 335 | ``` 336 | 337 | ### EUDCCValidator 338 | 339 | #### ValidationRule 340 | 341 | An `EUDCC` can be validated by using an `EUDCCValidator` and a given `EUDCC.ValidationRule`. An `EUDCC.ValidationRule` can be initialized with a simple closure wich takes in an `EUDCC` and returns a `Bool` whether the validation succeed or failed. 342 | 343 | ```swift 344 | import EUDCC 345 | import EUDCCValidator 346 | 347 | // Simple EUDCC ValidationRule instantiation 348 | let validationRule = EUDCC.ValidationRule { eudcc in 349 | // Process EUDCC and return Bool result 350 | } 351 | 352 | // EUDCC ValidationRule with Tag in order to uniquely identify a ValidationRule 353 | let isVaccinationComplete = EUDCC.ValidationRule( 354 | tag: "is-vaccination-complete" 355 | ) { eudcc in 356 | eudcc.vaccination?.doseNumber == eudcc.vaccination?.totalSeriesOfDoses 357 | } 358 | ``` 359 | 360 | The `EUDCCKit` comes along with many pre defined `EUDCC.ValidationRule` like the following ones. 361 | 362 | ```swift 363 | import EUDCC 364 | import EUDCCValidator 365 | 366 | let eudcc: EUDCC 367 | let validator = EUDCCValidator() 368 | 369 | // Is fully immunized 370 | validator.validate( 371 | eudcc: eudcc, 372 | rule: .isFullyImmunized(minimumDaysPast: 15) 373 | ) 374 | 375 | // Is tested positive 376 | validator.validate( 377 | eudcc: eudcc, 378 | rule: .isTestedPositive 379 | ) 380 | ``` 381 | 382 | #### Logical/Conditional operators 383 | 384 | In order to create more complex rules each `EUDCC.ValidationRule` can be chained together by applying standard operators. 385 | 386 | ```swift 387 | import EUDCC 388 | import EUDCCValidator 389 | 390 | let defaultValidationRule: EUDCC.ValidationRule = .if( 391 | .isVaccination, 392 | then: .isFullyImmunized() && .isWellKnownVaccineMedicinalProduct && !.isVaccinationExpired(), 393 | else: .if( 394 | .isTest, 395 | then: .isTestedNegative && .isTestValid(), 396 | else: .if( 397 | .isRecovery, 398 | then: .isRecoveryValid, 399 | else: .constant(false) 400 | ) 401 | ) 402 | ) 403 | ``` 404 | 405 | #### Convenience validation 406 | 407 | By importing the `EUDCCValidator` library the `EUDCC` object will be extended with a `validate` function. 408 | 409 | ```swift 410 | import EUDCC 411 | import EUDCCValidator 412 | 413 | let eudcc: EUDCC 414 | 415 | let validationRule = eudcc.validate( 416 | rule: .isWellKnownVaccineMedicinalProduct 417 | ) 418 | ``` 419 | 420 | ## License 421 | 422 | ``` 423 | EUDCCKit 424 | Copyright (c) 2021 Sven Tiigi sven.tiigi@gmail.com 425 | 426 | Permission is hereby granted, free of charge, to any person obtaining a copy 427 | of this software and associated documentation files (the "Software"), to deal 428 | in the Software without restriction, including without limitation the rights 429 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 430 | copies of the Software, and to permit persons to whom the Software is 431 | furnished to do so, subject to the following conditions: 432 | 433 | The above copyright notice and this permission notice shall be included in 434 | all copies or substantial portions of the Software. 435 | 436 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 437 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 438 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 439 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 440 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 441 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 442 | THE SOFTWARE. 443 | ``` 444 | --------------------------------------------------------------------------------