Vdstools
A library to parse and generate Visible Digital Seals (VDS)
Install / Use
/learn @tsenger/VdstoolsREADME
VdsTools - Kotlin multiplatform library to work with Visible Digital Seals
This a Kotlin multiplatform (JVM and iOS) library to decode/verify and encode/sign Visible Digital Seals (VDS) as specified in
- BSI TR-03137 Part 1
- ICAO Doc 9303 Part 13: Visible Digital Seals
- ICAO TR "VDS for Non-Electronic Documents"
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.
<a href='https://play.google.com/store/apps/details?id=de.tsenger.sealver&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png' width='155' height='60'/></a>
<a href="https://apps.apple.com/de/app/sealva-vds-validator/id6756822727?itscg=30200&itsct=apps_box_badge&mttnsubad=6756822727" style="display: inline-block;"><img src="https://toolbox.marketingtools.apple.com/api/v2/badges/download-on-the-app-store/black/en-us?releaseDate=1767571200" alt="Download on the App Store" style="width: 155px; height: 45px; vertical-align: middle; object-fit: contain;" /></a>
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.value}")
}
// Access message data by name - value.toString() returns the decoded value
val mrz: String? = seal.getMessageByName("MRZ_TD2")?.toString()
// Or use type-safe access via sealed class
val messageValue = seal.getMessageByName("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()
Metadata messages (administrative documents)
Some seal types use a two-stage lookup: the header's documentRef points to a base type (e.g.
ADMINISTRATIVE_DOCUMENTS), and the first tags of the message zone carry administrative metadata
(e.g. the document profile UUID at tag 0, validity dates at tag 1) rather than user-visible content.
These tags are declared as metadataTagList in VdsDocumentTypes.json and are automatically separated
from the regular messageList during parsing. They are only accessible via metadataMessageList.
val seal: Seal = Seal.fromString(rawString)
// Regular user-visible messages (metadata tags are excluded)
val messageList = seal.messageList
// Metadata messages (e.g. DOC_PROFILE_NUMBER, VALIDITY_DATES)
val metadataList = seal.metadataMessageList
for (message in metadataList) {
println("${message.name} (tag ${message.tag}) -> ${message.value}")
}
// Check if the seal has any metadata at all
if (seal.metadataMessageList.isNotEmpty()) {
// access a specific metadata message by name
val validity = seal.metadataMessageList.firstOrNull { it.name == "VALIDITY_DATES" }
val v = validity?.value as? MessageValue.ValidityDatesValue
println("valid from ${v?.validFrom} to ${v?.validTo}")
}
To check at the type level which tags are configured as metadata (without a concrete seal instance):
DataEncoder.vdsDocumentTypes.getMetadataTags("ADMINISTRATIVE_DOCUMENTS") // → {0, 1, 2, 3}
DataEncoder.vdsDocumentTypes.getMetadataTags("RESIDENT_PERMIT") // → {} (no metadata tags)
Regular document types have no metadataTagList configured, so their metadataMessageList is
always empty and all tags appear in messageList as usual.
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
Related Skills
node-connect
338.0kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
83.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
338.0kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
commit-push-pr
83.4kCommit, push, and open a PR
