Skip to content

gerege-token-kit — FEITIAN USB token SPM package

gerege-token-kit is a Swift Package Manager package for interacting locally (offline, directly plugged in) with FEITIAN USB tokens (smart-card-form crypto devices). It provides the ability to generate keys on the token, log in with a PIN, produce ECDSA/RSA signatures, read/write certificates, and generate a PKCS#10 CSR.

Dependency-free. It uses no third-party packages at all — it relies only on Apple's system frameworks (Foundation, CryptoTokenKit, CommonCrypto, os). Source: desktop/gerege-token-kit/Package.swift.

Local-only. This package interacts only with a plugged-in USB token directly — it makes no RP-API or any network call. Delivering the CSR/signature created on the token to the server is the responsibility of the calling app (e.g. the macOS desktop).

1. What it does

The package offers two levels of access:

Level Type Method Token
Standard (PKCS#11) PKCS11Module Loads the FEITIAN Castle middleware (libcastle*.dylib) via dlopen and calls the C_* functions FEITIAN Castle FTSmartToken
Low-level (APDU) BioPassDriver Sends ISO 7816-4 APDUs directly and, after mutual-auth, uses Secure Messaging (3DES) BioPass2003 / ePass2003 (EnterSafe-FIPS applet)

Additional supporting components:

  • TokenManager — reader detection and session management based on CryptoTokenKit (TKSmartCard).
  • APDUCommand / APDUResponse / APDUTransceiver — the APDU build/transmit engine.
  • SecureMessaging — the ePass2003 3DES secure channel (mutual auth, APDU wrap/unwrap).
  • CSR — PKCS#10 CSR (EC P-256 + ECDSA-SHA256) builder.
  • TokenError — an enum with error text in Mongolian.

2. Installation

Add it via a local path (the package ships in the repo at the same time):

// Package.swift
dependencies: [
    .package(path: "../gerege-token-kit")
]

Then link it to your target:

.target(name: "MyApp", dependencies: [
    .product(name: "GeregeTokenKit", targets: ["GeregeTokenKit"])
])

Import the package:

import GeregeTokenKit

Package information (Package.swift):

  • Name / product: GeregeTokenKit
  • Platform: macOS 14+, iOS 17+ (since the PKCS#11 dlopen path requires real USB middleware, in practice it works on macOS — see §5 below).
  • swift-tools-version: 5.9

3. Public API

PKCS11Module (standard path)

Source: Sources/GeregeTokenKit/PKCS11Module.swift.

Default middleware paths (defaultLibraryPaths): /usr/local/lib/libcastle.1.0.0.dylib, /usr/local/lib/libcastle.dylib, /Library/OpenSC/lib/opensc-pkcs11.so.

Low-level functions:

Function Role
static open(libraryPath:) Loads the .dylib/.so via dlopen and finds the C_* symbols
initialize() / finalize() C_Initialize / C_Finalize
getSlotList(tokenPresent:) IDs of slots with a token plugged in
openSession(slotId:readWrite:) / closeSession(_:) Open/close a session
login(session:pin:userType:) / logout(_:) C_Login (CKU_USER/CKU_SO) / C_Logout
findPrivateKey(session:label:) / findPublicKey(session:label:) Find a key by label
signECDSA(session:privateKey:hash:) ECDSA signature — returns: r‖s
readECPoint(session:publicKey:) Read the raw EC point from CKA_EC_POINT
initToken(slotId:soPIN:label:) C_InitToken — factory reset + SO PIN
initPIN(session:userPIN:) / setPIN(session:oldPIN:newPIN:) Set/change the user PIN
generateECKeyPair(session:label:keyID:) Generate an EC P-256 keypair on the token
writeCertificate(session:certificateDER:label:keyID:subjectDER:) Store an X.509 certificate via C_CreateObject
listObjects(session:) List all objects on the token as TokenObjectInfo
destroyObject(session:handle:) Delete an object (C_DestroyObject)

High-level helper functions (they select the slot, log in, and log out automatically, and the session closes itself):

Function Role
signECDSA(pin:keyLabel:hash:) Log in with the PIN, find the key by label, and sign
generateCSR(pin:keyLabel:subject:) Generate a PKCS#10 CSR (DER+PEM) with the key on the token
fullProvision(soPIN:userPIN:label:keyLabel:keyID:) Factory reset + SO/User PIN + EC keypair (⚠ all previous keys are erased)
listObjects(pin:) Log in and list all objects
importCertificate(pin:label:certificateDER:keyID:) Import a certificate
deleteObject(pin:idHex:kind:) Delete an object by idHex+kind
generateSigningKey(pin:label:keyID:) Generate an EC P-256 signing keypair
changeUserPIN(oldPIN:newPIN:) / changeSOPIN(oldPIN:newPIN:) Change a PIN
unlockUserPIN(soPIN:newUserPIN:) Unlock a locked User PIN with the SO PIN

BioPassDriver (low-level/APDU path)

Source: Sources/GeregeTokenKit/BioPassDriver.swift. Works directly with TKSmartCard (a Swift port of OpenSC's card-epass2003.c). Main functions:

Function Role
selectApplet(card:) Select the EnterSafe-FIPS applet
establishSecureSession(card:...) Mutual auth → establish a Secure Messaging session
verifyPIN(_:reference:card:) Verify the User/SO PIN (if wrong, pinVerifyFailed(retriesLeft:))
getTokenInfo(card:) ATR, label, whether FIPS (TokenInfo)
signECDSA(hash:card:) / signRSA(data:card:) Signing
generateECKeyPair(keyID:card:) / generateRSAKeyPair(keyID:keySize:card:) Generate a key (requires SM)
readCertificate(fileID:card:) / writeCertificate(data:fileID:card:) Read/write a certificate
initializePIN(pin:puk:card:) Fully initialize the token (erase + transport key + PKCS#15 file system + PIN)

TokenManager (reader/session)

Source: Sources/GeregeTokenKit/TokenManager.swift.

  • TokenManager.shared — singleton
  • getReaderNames() -> [String], hasToken() -> Bool
  • withSession(readerIndex:_:) — opens a session, runs the operation, and closes automatically
  • getATR(readerIndex:)

CSR

Source: Sources/GeregeTokenKit/CSR.swift.

  • CSR.Subject(commonName:organization:country:email:)
  • CSR.buildP256(subject:publicKeyPoint:signer:) — returns a PKCS#10 CSR (DER + PEM) from a 65-byte uncompressed EC point + a signing closure.

4. Usage examples

Check whether a reader is present

import GeregeTokenKit

let mgr = TokenManager.shared
if mgr.hasToken() {
    print("Reader-ууд:", mgr.getReaderNames())
    let atr = try await mgr.getATR()
    print("ATR:", bytesToHex(atr))
}

Signing with PKCS#11 (high level)

let hash = /* SHA-256 digest, 32 bytes */
let p11 = try PKCS11Module.open()               // loads libcastle*.dylib
let signature = try p11.signECDSA(              // returns r‖s
    pin: "12345678",
    keyLabel: "gerege",
    hash: hash
)

Generate a CSR with the key on the token

let p11 = try PKCS11Module.open()
let subject = CSR.Subject(commonName: "Бат-Эрдэнэ", country: "MN")
let (der, pem) = try await p11.generateCSR(
    pin: "12345678",
    keyLabel: "gerege",
    subject: subject
)
// send the pem to the CA to obtain a certificate (outside this package)

At the APDU level (BioPassDriver)

let driver = BioPassDriver()
try await TokenManager.shared.withSession { card in
    _ = try await driver.selectApplet(card: card)
    try await driver.establishSecureSession(card: card)   // 3DES SM
    try await driver.verifyPIN("12345678", card: card)
    let sig = try await driver.signECDSA(hash: hash, card: card)
    return sig
}

5. Limitations

  • Local only. It sends nothing to the network — it interacts only with a plugged-in token.
  • Platform. PKCS11Module loads native .dylib middleware via dlopen, so in practice it is used on macOS. Although Package.swift also declares iOS 17, the PKCS#11 path will not work on iOS since there is no real USB token
  • middleware.
  • Supported tokens:
  • PKCS#11: FEITIAN Castle FTSmartToken (libcastle.1.0.0.dylib); OpenSC opensc-pkcs11.so is also included in the default paths.
  • APDU: BioPass2003 / ePass2003 (EnterSafe-FIPS applet).
  • Crypto: EC P-256 (ECDSA-SHA256) is the primary path; RSA is supported by BioPassDriver/PKCS11Module, but the CSR builder is P-256 only.
  • Provisioning is destructive. fullProvision / initToken / initializePIN erase all keys and certificates on the token.

6. Relationship to the macOS desktop app

This package is used by the macOS desktop app (the eID Mongolia macOS RP client): that app wraps GeregeTokenKit.TokenManager.shared and BioPassDriver in its own Core/Token/TokenManager.swift and runs an experimental flow of signing a PDF with a USB token.