diff --git a/README.md b/README.md index 93e237b..52573fe 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,16 @@
tsemrtd
simple library for eMRTD datagroups +
+ site +  •  + docs

- -
- +
+ +

@@ -78,8 +82,9 @@ await Bun.write("image.jp2",data[0].imageData) Library doesn't support datagroups #6,8,9,10,13,16 because they are defined for optional information for each state. -## Acknowledgements +## Links - [jmrtd](https://jmrtd.org) - An Open Source Java Implementation of eMRTD - [ICAO 9303](https://www.icao.int/publications/pages/publication.aspx?docnum=9303) - Specifications to MRTD - ISO/IEC 19794(-4/-5/-6) - Specifications to Biometric Information Encoding (BioAPI) +- BSI TR-03110 - Specifications to EAC/PACE \ No newline at end of file diff --git a/jsr.json b/jsr.json index 1548409..f0dc52f 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@li0ard/tsemrtd", - "version": "0.1.6", + "version": "0.1.7", "exports": "./src/index.ts", "publish": { "include": [ diff --git a/package.json b/package.json index 4b585e0..84a67db 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@li0ard/tsemrtd", "main": "dist/index.js", "types": "dist/index.d.ts", - "version": "0.1.6", + "version": "0.1.7", "type": "module", "author": "li0ard", "repository": { diff --git a/src/asn1/eac.ts b/src/asn1/eac.ts index 6fcf548..1cb6a30 100644 --- a/src/asn1/eac.ts +++ b/src/asn1/eac.ts @@ -1,4 +1,5 @@ import { AsnProp, AsnType, AsnPropTypes, AsnTypeTypes, AsnArray } from "@peculiar/asn1-schema"; +import { AlgorithmIdentifier, SubjectPublicKeyInfo } from "@peculiar/asn1-x509"; /** Class for ASN1 schema of EAC/PACE Security info sequence */ export class SecurityInfo { @@ -15,6 +16,77 @@ export class SecurityInfo { optionalData?: any; } +/** EF.CVCA file ID. Described by BSI TR-03110, section A.1.1.3 */ +export class FileID { + /** File ID */ + @AsnProp({ type: AsnPropTypes.OctetString }) + fid: ArrayBuffer = new ArrayBuffer(0) + + /** Short file ID */ + @AsnProp({ type: AsnPropTypes.OctetString, optional: true }) + sfid?: ArrayBuffer; +} + +/** Information on an implementation of Terminal Authentication. Described by BSI TR-03110, section A.1.1.3 */ +export class TerminalAuthenticationInfo { + /** OID of protocol */ + @AsnProp({ type: AsnPropTypes.ObjectIdentifier }) + protocol: string = ""; + + /** Terminal Authentication version */ + @AsnProp({ type: AsnPropTypes.Integer }) + version: number = 0; // MUST be 1 for TAv1 or 2 for TAv2 + + /** Indicate a (short) file identifier of the file EF.CVCA (v1 only) */ + @AsnProp({ type: FileID, optional: true }) + efCVCA?: FileID; +} + +/** Information on an implementation of Chip Authentication. Described by BSI TR-03110, section A.1.1.2 */ +export class ChipAuthenticationInfo { + /** OID of protocol */ + @AsnProp({ type: AsnPropTypes.ObjectIdentifier }) + protocol: string = ""; + + /** Chip Authentication version */ + @AsnProp({ type: AsnPropTypes.Integer }) + version: number = 0; // MUST be 1 for CAv1 or 2 for CAv2 or 3 for CAv3 + + /** Indicate the local key identifier (and domain parameters for key agreement) */ + @AsnProp({ type: AsnPropTypes.Integer, optional: true }) + keyId?: number; +} + +/** A public key for Chip Authentication. Described by BSI TR-03110, section A.1.1.2 */ +export class ChipAuthenticationPublicKeyInfo { + /** OID of protocol */ + @AsnProp({ type: AsnPropTypes.ObjectIdentifier }) + protocol: string = ""; + + /** Public key in encoded form ([RFC 5480](https://datatracker.ietf.org/doc/html/rfc5480#section-2)) */ + @AsnProp({ type: SubjectPublicKeyInfo }) + chipAuthenticationPublicKey: SubjectPublicKeyInfo = new SubjectPublicKeyInfo() + + /** Indicate the local key identifier (and domain parameters for key agreement) */ + @AsnProp({ type: AsnPropTypes.Integer, optional: true }) + keyId?: number; +} + +/** One set of domain parameters for Chip Authentication version 2 and version 3. Described by BSI TR-03110, section A.1.1.2 */ +export class ChipAuthenticationDomainParameterInfo { + /** OID of protocol */ + @AsnProp({ type: AsnPropTypes.ObjectIdentifier }) + protocol: string = ""; + + /** Domain parameters */ + @AsnProp({ type: AlgorithmIdentifier }) + domainParameter: AlgorithmIdentifier = new AlgorithmIdentifier(); + + /** Indicate the local key identifier (and domain parameters for key agreement) */ + @AsnProp({ type: AsnPropTypes.Integer, optional: true }) + keyId?: number; +} + /** Class for ASN1 schema of SecurityInfo set. Described by ICAO 9303 p.10 section 4.7.14.2 */ @AsnType({ type: AsnTypeTypes.Set, itemType: SecurityInfo }) -export class SecurityInfos extends AsnArray {} \ No newline at end of file +export class SecurityInfos extends AsnArray {} \ No newline at end of file diff --git a/src/asn1/index.ts b/src/asn1/index.ts new file mode 100644 index 0000000..bb8af07 --- /dev/null +++ b/src/asn1/index.ts @@ -0,0 +1,3 @@ +export * as EAC from "./eac" +export * as PKD from "./pkd" +export * as SOD from "./sod" \ No newline at end of file diff --git a/src/consts/interfaces.ts b/src/consts/interfaces.ts index 95ec24d..8a55ccf 100644 --- a/src/consts/interfaces.ts +++ b/src/consts/interfaces.ts @@ -1,5 +1,7 @@ import type TLV from "node-tlv"; import type { EyeColor, FaceType, FingerImageType, FingerType, FingerprintImageType, Gender, HairColor, ImageColorSpace, ImageType, ImageUnit, IrisEyeSubtype, IrisImageFormat, SourceType } from "./enums"; +import type { CertificateSet, SignerInfos } from "@peculiar/asn1-cms"; +import type { LDSObject } from "../asn1/sod"; /** Template for BioAPI decoded datagroup */ interface AbstractBioTemplate { @@ -171,4 +173,14 @@ export interface DecodedAdditionalDocumentData { dateOfPersonalization: number, /** Serial number of personalization system */ personalizationNumber: string +} + +/** Decoded EF.SOD datagroup */ +export interface DecodedSecurtyObjectOfDocument { + /** Included certificates (ex. Document Signer Certificate) */ + certificates: CertificateSet, + /** LDS object with datagroup's hashes */ + ldsObject: LDSObject, + /** SOD signatures */ + signatures: SignerInfos } \ No newline at end of file diff --git a/src/consts/oids.ts b/src/consts/oids.ts new file mode 100644 index 0000000..3b04243 --- /dev/null +++ b/src/consts/oids.ts @@ -0,0 +1,26 @@ +/** BSI TR-03110. Terminal authentication */ +export const TerminalAuthentication = "0.4.0.127.0.7.2.2.2" + +/** BSI TR-03110. Chip authentication */ +export enum ChipAuthDomainParameters { + CA_DH = "0.4.0.127.0.7.2.2.3.1", + CA_ECDH = "0.4.0.127.0.7.2.2.3.2" +} + +/** BSI TR-03110. Chip authentication */ +export enum ChipAuthPublicKey { + PK_DH = "0.4.0.127.0.7.2.2.1.1", + PK_ECDH = "0.4.0.127.0.7.2.2.1.2", +} + +/** BSI TR-03110. Chip authentication */ +export enum ChipAuthInfo { + CA_DH_3DES_CBC_CBC = "0.4.0.127.0.7.2.2.3.1.1", + CA_DH_AES_CBC_CMAC_128 = "0.4.0.127.0.7.2.2.3.1.2", + CA_DH_AES_CBC_CMAC_192 = "0.4.0.127.0.7.2.2.3.1.3", + CA_DH_AES_CBC_CMAC_256 = "0.4.0.127.0.7.2.2.3.1.4", + CA_ECDH_3DES_CBC_CBC = "0.4.0.127.0.7.2.2.3.2.1", + CA_ECDH_AES_CBC_CMAC_128 = "0.4.0.127.0.7.2.2.3.2.2", + CA_ECDH_AES_CBC_CMAC_192 = "0.4.0.127.0.7.2.2.3.2.3", + CA_ECDH_AES_CBC_CMAC_256 = "0.4.0.127.0.7.2.2.3.2.4", +} \ No newline at end of file diff --git a/src/dg14.ts b/src/dg14.ts index 5b0ce51..a02d902 100644 --- a/src/dg14.ts +++ b/src/dg14.ts @@ -1,7 +1,7 @@ import TLV from "node-tlv" -import { Enums } from "./index"; +import { Enums, Oids } from "./index"; import { AsnConvert } from "@peculiar/asn1-schema"; -import { SecurityInfos } from "./asn1/eac"; +import { ChipAuthenticationDomainParameterInfo, ChipAuthenticationInfo, ChipAuthenticationPublicKeyInfo, SecurityInfos, TerminalAuthenticationInfo } from "./asn1/eac"; /** * Class for working with DG14 (EAC/PACE authentication info) @@ -13,8 +13,28 @@ export class DG14 { */ static load(data: string | Buffer): SecurityInfos { let tlv = TLV.parse(data) - if(parseInt(tlv.tag, 16) != Enums.TAGS.DG14) throw new Error(`Invalid DG14 tag "0x${tlv.tag}", expected 0x${Enums.TAGS.DG14.toString(16)}`); + if(parseInt(tlv.tag, 16) != Enums.TAGS.DG14) throw new Error(`Invalid DG14 tag "0x${tlv.tag}", expected 0x${Enums.TAGS.DG14.toString(16)}`); - return AsnConvert.parse(tlv.bValue, SecurityInfos) + let infos = AsnConvert.parse(tlv.bValue, SecurityInfos) + let set = new SecurityInfos() + for(let i of infos) { + if(i.protocol == Oids.TerminalAuthentication) { + set.push(AsnConvert.parse(AsnConvert.serialize(i), TerminalAuthenticationInfo)) + } + else if((Object.values(Oids.ChipAuthInfo) as string[]).includes(i.protocol)) { + set.push(AsnConvert.parse(AsnConvert.serialize(i), ChipAuthenticationInfo)) + } + else if((Object.values(Oids.ChipAuthPublicKey) as string[]).includes(i.protocol)) { + set.push(AsnConvert.parse(AsnConvert.serialize(i), ChipAuthenticationPublicKeyInfo)) + } + else if((Object.values(Oids.ChipAuthDomainParameters) as string[]).includes(i.protocol)) { + set.push(AsnConvert.parse(AsnConvert.serialize(i), ChipAuthenticationDomainParameterInfo)) + } + else { + set.push(i) + } + } + + return set } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 9d8295d..99a4d6c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,4 +12,6 @@ export { DG15 } from "./dg15" export { SOD } from "./sod" export { PKD } from "./pkd" export * as Enums from "./consts/enums" -export * as Interfaces from "./consts/interfaces" \ No newline at end of file +export * as Interfaces from "./consts/interfaces" +export * as Oids from "./consts/oids" +export * as Schemas from "./asn1/index" \ No newline at end of file diff --git a/src/sod.ts b/src/sod.ts index 94a24dc..04aafe8 100644 --- a/src/sod.ts +++ b/src/sod.ts @@ -1,8 +1,8 @@ import TLV from "node-tlv" -import { Enums } from "./index"; +import { Enums, Interfaces } from "./index"; import { AsnConvert, OctetString } from "@peculiar/asn1-schema"; import { LDSObject } from "./asn1/sod"; -import { ContentInfo, SignedData } from "@peculiar/asn1-cms"; +import { CertificateSet, ContentInfo, SignedData } from "@peculiar/asn1-cms"; /** * Class for working with SOD (Security object) @@ -12,13 +12,17 @@ export class SOD { * Get version, algorithm, data groups hashes * @param data Data of EF.SOD file */ - static load(data: string | Buffer): LDSObject { + static load(data: string | Buffer): Interfaces.DecodedSecurtyObjectOfDocument { let tlv = TLV.parse(data) if(parseInt(tlv.tag, 16) != Enums.TAGS.SOD) throw new Error(`Invalid SOD tag "0x${tlv.tag}", expected 0x${Enums.TAGS.SOD.toString(16)}`); let contentInfo = AsnConvert.parse(tlv.bValue, ContentInfo) let signedData = AsnConvert.parse(contentInfo.content, SignedData) let sod = AsnConvert.parse(signedData.encapContentInfo.eContent?.single as OctetString, LDSObject) - return sod + return { + certificates: signedData.certificates as CertificateSet, + ldsObject: sod, + signatures: signedData.signerInfos + } } } \ No newline at end of file diff --git a/tests/index.test.ts b/tests/index.test.ts index 23d7f95..9e5572b 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "bun:test" import { join } from "path" -import { COM, DG1, DG2, DG3, DG5, DG7, DG11, DG12, SOD, DG15, DG4 } from "../src" +import { COM, DG1, DG2, DG3, DG5, DG7, DG11, DG12, SOD, DG15, DG4, DG14, Schemas } from "../src" const getDGContent = async (name: string): Promise => { return Buffer.from(await Bun.file(join(import.meta.dir, "dgs", name)).bytes()) @@ -79,8 +79,18 @@ test("DG12", async () => { expect(data.personalizationNumber).toBe("123") }) -test.todo("DG14", async () => { +test("DG14", async () => { + let data = DG14.load(await getDGContent("EF_DG14.bin")) + let ta = data.filter(i => i instanceof Schemas.EAC.TerminalAuthenticationInfo)[0] as Schemas.EAC.TerminalAuthenticationInfo + let ca = data.filter(i => i instanceof Schemas.EAC.ChipAuthenticationInfo)[0] as Schemas.EAC.ChipAuthenticationInfo + let caPk = data.filter(i => i instanceof Schemas.EAC.ChipAuthenticationPublicKeyInfo)[0] as Schemas.EAC.ChipAuthenticationPublicKeyInfo + expect(ta.protocol).toBe("0.4.0.127.0.7.2.2.2") + expect(ta.version).toBe(1) + expect(ca.protocol).toBe("0.4.0.127.0.7.2.2.3.2.1") + expect(ca.version).toBe(1) + expect(caPk.protocol).toBe("0.4.0.127.0.7.2.2.1.2") + expect(caPk.chipAuthenticationPublicKey.algorithm.algorithm).toBe("1.2.840.10045.2.1") }) test("DG15", async () => { @@ -91,7 +101,9 @@ test("DG15", async () => { test("SOD", async () => { let data = SOD.load(await getDGContent("EF_SOD.bin")) - expect(data.version).toBe(0) - expect(data.algorithm.algorithm).toBe("1.3.14.3.2.26") - expect(data.hashes.length).toBe(4) + expect(data.certificates.length).toBe(1) + expect(data.signatures.length).toBe(1) + expect(data.ldsObject.version).toBe(0) + expect(data.ldsObject.algorithm.algorithm).toBe("1.3.14.3.2.26") + expect(data.ldsObject.hashes.length).toBe(4) }) \ No newline at end of file