Skip to content

A library to parse and generate Visible Digital Seals (VDS)

License

Notifications You must be signed in to change notification settings

tsenger/vdstools

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

265 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VdsTools - Kotlin multiplatform library to work with Visible Digital Seals

License Maven Central Version GitHub Actions Workflow Status

This a Kotlin multiplatform (JVM and iOS) library to decode/verify and encode/sign Visible Digital Seals (VDS) as specified in

It also fully supports encoding and decoding Seals defined in the new draft of ICAO Datastructure for Barcode. VDS and ICD barcodes can be parsed by a generic interface. An example is given in the following chapter

VDS can be created with the help of this library or, if you want to try it out quickly, via the web Sealgen tool. There is also the Sealva mobile app which scans, verifies and displays all VDS profiles defined in the above specifications.

Get it on Google Play

Download on the App Store

Parse and verify a VDS / IDB

Here is a quick overview how to use the generic parser and verifier. The generic interface handles VDS and IDB barcode via common function calls. When you received the raw string from your favorite datamatrix decoder use VdsTools like this:

import de.tsenger.vdstools.Verifier
import de.tsenger.vdstools.generic.Seal
import de.tsenger.vdstools.generic.Message
import de.tsenger.vdstools.generic.SignatureInfo
import de.tsenger.vdstools.generic.MessageValue

// example raw string coming as result from your used barcode scanner library
val rawString =
    "RDB1BNK6ADJL2PECXOABAHIMWDAQEE6ATAXHCCNJVXVGK5TEHZJGMV22BGPATHQJTYEZ4CM6D73Z2FE4O4Q7RLE6RVZJNXMTHKH7GJN6BGPATNOAIEA7EAAAAADDKKAQCADIKQ4FAAAAACRTHI6LQNJYDEIAAAAAAA2TQGIQAAAAAI5VHAMTIAAAAAFTJNBSHEAAAACAQAAAAMQAACBYHAAAAAAAAB5RW63DSAEAAAAAAAAIQAAAADJZGK4ZAAAAAAETSMVZWGAJMAD7ACLAA7YCAIAAAAAAGU4BSMP7U772RAAUQAAAAAAAGIAAAACAQAAAAAAAAAAAAAAAAAZAAAAAICAAAAAAAAAAAAAAACBYBAH7VYAAXIJTTKZ2MM5GGOZCGGZDDMRVMHYED4CB5UP7VEAAMAAAAAAIAAMCAIAAA75SAADQAAFGFIX2KKAZF6MRSG37ZAAAKAAAAAAADB4AAD74TY7FX6HL6CBIU4OROHXUXWYFSZTVXFI47EE2NADZRJWKVIGHF7BZDEJUHKDBB2LVVJBUG6MCWD66UJDQPTNHAIKTKEB4THMTRBKM6ORXIEW5WWVQDEYMMIFHA43M5OEHWK62OQHQQKTBLBNONJTM3INJTFMRPXM6NUTBYIQWXPHK6EMENBL25ZRIW5FXG2PZO3CLJC6WCXCLFGNZKYPSKOQ7EULA7BVUAKBQ44Q6HCLT5RDUZM4D3TT55GA7H57NQ7G7LXSG4W4NNAT344KM5LE7EMSDFOE5OFQDYF6PQYZRXR3RQSBCDGV34YNJG3VUWGUJ3DL7TJAYWW7YVI5GVGPX4IKM25DFVEAGB6OM2VFHQAGMFNJFT56I7V5XIRMFOIFJDG2SRS5GFCKY6UUYUVPBL3TG2ULE6ULYNIICKTLUJK6ALUA2SNNU7TSPBXVQVRPEJ7R7UHJWBWI6XGNKWRRBXEFB27VLV3OEVKMVQBCLRFUWXFYXFVOWMF7I763YAUQXDNTP7E42DG26XKBB4HYG4UPR3NQONHCMQKS5FOZOP6JDW75G5EPKRLSGBURBIMMLAW72X4TTXLFH5ATDGB4VCA6US4G57CFEPCE6SB6JUZJGDLWTD2L7YLDXCTYPXZYSMPITJV5ABIDRACHAYDBJZXQXWIPCWWX6TVPXTIA6MQQJNAVSETK7IYNK6NXA66V2A6UELOCCMQDDEKQYNOCLDZ2NGWSIRQFODRERJXLTKFZ2PIUHF34VJDGYKAAFN67I2WUVWD2UY3FOBWKVU5YXMYV5FRRN6DWNJWA76JYR2ONQ72CZHBHYBTN7XNRGVNVRMMT7DZLUXZPOA2HA46H6ADOTMVJTNWAG6SENPRKH4SJQZWGSFXXFGP4P6Q3J5NSRFZZBLFGHVFGLQIYB5VGSHBBE2YEXIPMP4XGND45NCNSKOJIN6LIT4ZQO2QXRWLOX6BY7QNMEHHI4A5VUX54LSUSASKQRNZUREAU2IATUK5OYDB3XGQTZBHZC3SHGSQJPCRSBJVFG72WXX6RN2R34JFPYXVPUKMEM55ZTGXLR76AJR2TPT7HKGO6RTEIDE6RBFNRKJRR4NPILHJ5XLUEMZKB4A65NSF6T2YN3Y3T4EXDIQCQ3XQ2N7ERQQKZYWTTVPVCNFAJMMNE4JXNFCY4FRH27KA5P3LWZGCF4TIPXSWR2QZESM4AOSH74XRORBXWWCTS3VO4CW6Q773GBQQWPJEA4DG43NESDACDL7IABCBTZ3ZUTZWNPRAW35KVFV3NSTBLXPY7FHG3HEUY3XDT6KR37OK3J3LEJSGIENHILIYX5D2OZKBGMU7FCU7ZIXAPJORJA7MBTGAQEN"

val seal: Seal = Seal.fromString(rawString)

//Get list with all messages in seal
val messageList = seal.messageList
for (message in messageList) {
    println("${message.name} (${message.coding}) -> ${message.value}")
}

// Access message data by name - value.toString() returns the decoded value
val mrz: String? = seal.getMessage("MRZ_TD2")?.toString()

// Or use type-safe access via sealed class
val messageValue = seal.getMessage("MRZ_TD2")?.value
if (messageValue is MessageValue.MrzValue) {
    println("MRZ: ${messageValue.mrz}")
} else {
    // rawBytes is always an option
    println(messageValue?.rawBytes?.toHexString())
}

// SignatureInfo contains all signature relevant data
val signatureInfo: SignatureInfo? = seal.signatureInfo
println("Signing date: ${signatureInfo?.signingDate}")

// Get the signer certificate reference
val signerCertRef: String? = signatureInfo?.signerCertificateReference
println("Signer certificate reference: $signerCertRef")

// Since X509 certificate handling is strongly platform-dependent,
// the Verifier is given the plain publicKey (r|s) and the curve name.
val publicKeyBytes: ByteArray = ... // load from your certificate store
val verifier = Verifier(
    seal.signedBytes,
    signatureInfo!!.plainSignatureBytes,
    publicKeyBytes,
    "brainpoolP224r1"
)
val result: Verifier.Result = verifier.verify()

Build a barcode

Here is an example on how to use the DataEncoder and Signer classes to build a VDS barcode:

import de.tsenger.vdstools.Signer
import de.tsenger.vdstools.vds.VdsHeader
import de.tsenger.vdstools.vds.VdsMessageGroup
import de.tsenger.vdstools.vds.VdsSeal
import kotlinx.datetime.LocalDate

val keystore: KeyStore = ...

// In this JVM example we use a BouncyCastle keystore to get the certificate (for the header information)
// and the private key for signing the seal's data
val cert: X509Certificate = keystore.getCertificate(keyAlias)
val ecKey: ECPrivateKey = keystore.getKey(certAlias, keyStorePassword.toCharArray())

// Initialize the Signer with the private key bytes and curve name
val signer = Signer(ecKey.encoded, curveName)

// 1. Build a VdsHeader
val header = VdsHeader.Builder("ARRIVAL_ATTESTATION")
    .setIssuingCountry("D<<")
    .setSignerIdentifier("DETS")
    .setCertificateReference("32")
    .setIssuingDate(LocalDate.parse("2024-09-27"))
    .setSigDate(LocalDate.parse("2024-09-27"))
    .build()

// 2. Build a VdsMessageGroup with messages
val mrz = "MED<<MANNSENS<<MANNY<<<<<<<<<<<<<<<<6525845096USA7008038M2201018<<<<<<06"
val azr = "ABC123456DEF"
val messageGroup = VdsMessageGroup.Builder(header.vdsType)
    .addMessage("MRZ", mrz)
    .addMessage("AZR", azr)
    .build()

// 3. Build a signed VdsSeal
val vdsSeal = VdsSeal(header, messageGroup, signer)

// The encoded bytes can now be used to build a DataMatrix (or other) code
val encodedSealBytes = vdsSeal.encoded

Here is an example on how to use the DataEncoder and Signer classes to build an IDB barcode:

import de.tsenger.vdstools.DataEncoder
import de.tsenger.vdstools.Signer
import de.tsenger.vdstools.idb.*

val keystore: KeyStore = ...

// In this JVM example we use a BouncyCastle keystore to get the certificate (for the header information)
// and the private key for signing the seal's data
val cert: X509Certificate = keystore.getCertificate(keyAlias)
val ecKey: ECPrivateKey = keystore.getKey(certAlias, keyStorePassword.toCharArray())

// Initialize the Signer with the private key bytes and curve name
val signer: Signer = Signer(ecKey.encoded, curveName)

// 1. Build an IdbHeader
val header = IdbHeader(
    countryIdentifier = "D<<",
    signatureAlgorithm = IdbSignatureAlgorithm.SHA256_WITH_ECDSA,
    certificateReference = DataEncoder.buildCertificateReference(cert.encoded),
    signatureCreationDate = "2025-02-11"
)

// 2. Build an IdbMessageGroup with messages
val messageGroup = IdbMessageGroup.Builder()
    .addMessage(0x80, faceImageBytes)           // FACE_IMAGE
    .addMessage(0x81, mrzTd2String)             // MRZ_TD2
    .addMessage(0x84, "2026-04-23")             // EXPIRY_DATE
    .addMessage(0x86, 0x01)                     // NATIONAL_DOCUMENT_IDENTIFIER
    .build()

// 3. Sign the header + messageGroup
val dataToSign = header.encoded + messageGroup.encoded
val signatureBytes = signer.sign(dataToSign)
val signature = IdbSignature(signatureBytes)

// 4. Build the IdbSeal
val payload = IdbPayload(header, messageGroup, null, signature)
val idbSeal = IdbSeal(isSigned = true, isZipped = false, barcodePayload = payload)

// The raw string can now be used to build a DataMatrix (or other) code
val encodedRawString = idbSeal.rawString

Also have a look at the testcases for more usage inspiration. You will also find an example on how to generate a datamatrix image with the Zxing library in the jvmTests.

Custom seal codings

VdsTools uses a JSON-based configuration to define seal document types and their message encodings which follows the encoding based on BSI TR-03137. Profile definitions according to BSI TR-03171 are currently not fully supported, as the profile definitions are not publicly available at this time. You can load your own custom codings at runtime to support additional document types beyond the standard ones.

Loading Custom Codings

// Option 1: Load from JSON string
val customJson = """[
  {
    "documentType": "MY_CUSTOM_DOCUMENT",
    "documentRef": "ab01",
    "version": 1,
    "messages": [
      {
        "name": "OWNER_NAME",
        "tag": 1,
        "coding": "C40",
        "decodedLength": 30,
        "required": true,
        "minLength": 1,
        "maxLength": 20
      },
      {
        "name": "ISSUE_DATE",
        "tag": 2,
        "coding": "DATE",
        "required": true,
        "minLength": 3,
        "maxLength": 3
      }
    ]
  }
]"""

DataEncoder.loadCustomSealCodings(customJson)

// Option 2: Load from file (JVM only)
DataEncoder.loadCustomSealCodingsFromFile("path/to/MyCodings.json")

Using Custom Document Types

Once loaded, custom document types work seamlessly with the existing API:

val messageGroup = VdsMessageGroup.Builder("MY_CUSTOM_DOCUMENT")
    .addMessage("OWNER_NAME", "MAX MUSTERMANN")
    .addMessage("ISSUE_DATE", "2024-06-15")
    .build()

val header = VdsHeader.Builder("MY_CUSTOM_DOCUMENT")
    .setIssuingCountry("D<<")
    .setSignerIdentifier("TEST")
    .setCertificateReference("32")
    .setIssuingDate(LocalDate.now())
    .setSigDate(LocalDate.now())
    .build()

val vdsSeal = VdsSeal(header, messageGroup, signer)

Available Coding Types

Coding Description
C40 Compressed text encoding (uppercase, digits, special chars)
MRZ Machine Readable Zone (special C40 variant)
UTF8_STRING Direct UTF-8 text
BYTE Single byte value
BYTES Variable-length byte array (images, binary data)
DATE 3-byte date (ICAO format, e.g. "2024-06-15")
MASKED_DATE 4-byte date with uncertainty masks

See src/commonMain/resources/SealCodings.json for the complete structure of the default codings.

Documentation

javadoc

Online JavaDoc can be found here: https://javadoc.io/doc/de.tsenger/vdstools

How to include

The vdstools library is available on the Maven Central Repository and GitHub Packages to be easy to integrate in your projects.

Gradle

To include this library to your Gradle build add this dependency:

dependencies {
    implementation 'de.tsenger:vdstools:0.11.0'
}

Maven

To include this library to your Maven build add this dependency:

<dependency>
    <groupId>de.tsenger</groupId>
    <artifactId>vdstools</artifactId>
    <version>0.11.0</version>
</dependency>

About

A library to parse and generate Visible Digital Seals (VDS)

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors 4

  •  
  •  
  •  
  •  

Languages