Spring Boot for JavaCard — annotation-driven applet installation and provisioning.
CINPO (Card INstallation and Provision Orchestrator) is a framework that lets you install and provision JavaCard applets with zero boilerplate. You declare your tasks with annotations, declare your dependencies with @Inject, and the framework handles everything else — GlobalPlatform secure channel authentication, CAP file loading, APDU transport, simulator lifecycle.
@CardTaskDef("provision")
public final class MyProvision implements CardTask {
@Inject private ApduChannel channel;
@Override
public void run() {
channel.transmit(Iso7816Commands.selectDf(MY_AID));
// your provisioning logic
}
}No manual wiring. No scripts. Just annotate, inject, run.
CINPO defines a developer experience for developing Java Card Applets. It provides a complete workflows and frameworks for building, provisioning, testing and deploying in the wild.
It is designed just like "Spring Boot for Java Card".
- We want to make Java Card Applet development easier and more modern.
- We want Spring Boot or Quarkus-like opinionated experience for Java Card Applet development.
- We want out-of-box experience for Java Card Applet development, with batteries included.
- We aim the world where we can achieve applet development with the following simple steps:
- Create a project from the template
- Write Card Applet
- Add annotations to provision code
- Run in one simple gradle command
- Do without entrypoint Main class, without build shellscripts, ad-hoc gradle tasks.
- We want to help developers make secure and robust applets in fast and iterative way.
You can create a starter project without cloning this repository by piping scripts/bootstrap.sh from GitHub Pages with curl:
curl -fsSL https://AokiApp.github.io/cinpo/bootstrap.sh | bash -s -- my-projectThis downloads and runs scripts/bootstrap.sh, which fetches template/ into a fresh project directory. It requires git, awk, and mktemp on your machine.
If you already have a checkout of this repository, you can run the script locally instead:
bash ./scripts/bootstrap.sh my-projectOn Windows, use scripts/bootstrap.ps1. It is a port of the same
four phases and only requires git:
# From a checkout
powershell -ExecutionPolicy Bypass -File .\scripts\bootstrap.ps1 my-project
# Without cloning
irm https://AokiApp.github.io/cinpo/bootstrap.ps1 -OutFile bootstrap.ps1
powershell -ExecutionPolicy Bypass -File .\bootstrap.ps1 my-projectBoth scripts accept --template/-Template and --name/-Name. The generated project is
identical either way: the PowerShell version checks the template out with core.autocrlf=false
and marks gradlew executable in the Git index, so a project created on Windows still builds
on Linux.
This creates a standalone starter project that you can customize manually. If you prefer, you can still copy template/ yourself. The starter is intentionally small: a single Hello applet plus one provision task that issues and writes a leaf certificate, and one test task that reads it back.
my-project/
├── manifest.yaml # What to install (package AID, applets, SDK version)
├── applet/ # JavaCard applet source files
├── host/
│ ├── provision/ # Default task kind for plain `write`
│ ├── test/ # Conventional verification tasks for `--test`
│ └── foobar/ # Any additional annotation-defined task kind
└── profile/
└── jcdksim.yaml # Optional simulator profile override
The copy script automatically sets rootProject.name in the generated project's settings.gradle to the destination directory name. Other sample identifiers remain unchanged for manual customization.
Non-Java files placed under host/ are packaged as runtime resources. Template projects can use this to bundle default fixture data or a read-only database file alongside host-side task code.
Edit manifest.yaml to describe your applet package:
schema: cinpo.applet.v1
basePackage: com.example.myapplet
toolSdkVersion: "26.0"
targetApiVersion: "3.0.4"
package:
name: com.example.myapplet
aid: D276000085010100
version: "1.0"
companionBundles:
- id: globalplatform-1.7
jar: vendor/globalplatform/org.globalplatform-1.7/gpapi-globalplatform.jar
exports: vendor/globalplatform/org.globalplatform-1.7/exports
applets:
- id: main
className: com.example.myapplet.MainApplet
classAid: D27600008501010001
instanceAid: D2760000850101000001
privilege: "00"Place JavaCard applet source files in applet/. Standard JavaCard API — nothing CINPO-specific here.
If your applet depends on vendor-supplied on-card APIs, declare them in companionBundles. Each bundle contributes:
- a stub
jaron thecompileAppletJavaclasspath - an
exportsdirectory in the converter export search path used byCinpoConverterLogic.profileFor()
This lets CINPO compile and convert applets that import companion packages such as GlobalPlatform, provided the vendor supplies both artifacts.
Create a class in host/provision/:
@CardTaskDef("provision")
public final class SetupPins implements CardTask {
@Inject private ApduChannel channel;
@Override
public void run() {
channel.transmit(Iso7816Commands.selectDf(AID));
channel.transmit(Iso7816Commands.changeReferenceData(0x01, "1234".getBytes()));
}
}The annotation processor indexes this class at compile time. At runtime, the framework discovers it, injects dependencies, and calls run().
Any task kind can be declared with @CardTaskDef("<kind>"). provision is simply the default selected kind when no explicit task selector is provided.
Configure GitHub Packages credentials as Gradle properties or environment variables so the standalone template/example projects can resolve CINPO artifacts:
gpr.user=<github-user>
gpr.key=<github-token-with-packages-read>Then run an application project from its own directory:
cd template
../gradlew cinpoRunThat's it. SDK download, CAP compilation, simulator setup, GP authentication, installation, and provisioning — all handled.
To run the conventional test flow after provisioning:
../gradlew cinpoRun --args='--test'To select explicit task kinds or skip installation:
../gradlew cinpoRun --args='--task=foobar'
../gradlew cinpoRun --args='--task=provision --task=test'
../gradlew cinpoRun --args='--skip-install --task=foobar'To pass template-specific runtime arguments to host-side tasks, put them after --:
../gradlew cinpoRun --args='write -- --record sample01'
../gradlew cinpoRun --args='write --profile mycard -- --record sample01 --db ./cards.db'CINPO still parses its own options strictly. Only the arguments after -- are forwarded verbatim to tasks.
To build a self-contained fat JAR with embedded simulator:
../gradlew cinpoJarRun it anywhere:
java -jar build/libs/cinpo-appliance.jar write --profile jcdksim
java -jar build/libs/cinpo-appliance.jar write --profile mycard -- --record customer-42CINPO task kinds are annotation-defined and loaded from META-INF/cinpo/<kind>.
Common conventions are:
| Kind | Annotation | Selected by |
|---|---|---|
| Provision | @CardTaskDef("provision") |
Default write; or --task=provision |
| Test | @CardTaskDef("test") |
--test; or --task=test |
| Custom | @CardTaskDef("foobar") |
--task=foobar |
--task is repeatable and preserves command-line order. --test is shorthand for --task=provision --task=test.
Tasks within the same kind are sorted by order() (ascending), then by class name:
@CardTaskDef(value = "provision", order = 100)
public final class FirstTask implements CardTask { ... }
@CardTaskDef(value = "provision", order = 200)
public final class SecondTask implements CardTask { ... }Fields annotated with @Inject are populated before run() is called:
| Type | What you get |
|---|---|
ApduChannel |
Live channel to the card/simulator |
AppletManifest |
Parsed manifest (AIDs, package info) |
Profile |
Active runtime profile |
SecureChannelProfile |
SCP key material for opening your own secure sessions |
TaskArguments |
Raw template-specific arguments forwarded after -- |
Arguments after -- are not interpreted by CINPO. They are forwarded verbatim to host-side tasks and exposed as TaskArguments.
@CardTaskDef("provision")
public final class LoadSelectedRecord implements CardTask {
@Inject private TaskArguments taskArguments;
@Override
public void run() {
String recordId = taskArguments.firstValue("record").orElse("sample01");
String dbPath = taskArguments.firstValue("db").orElse("host/data/cards.db");
// interpret recordId/dbPath however your template wants
}
}This keeps CINPO generic while letting each template define its own selectors such as record, db, customer, or input.
If you bundle a file-based database under host/, remember that it is packaged as a classpath resource. Some drivers can read directly from a resource stream; others may require copying the resource to a temporary file before opening it.
@CardTaskDef("provision")
public final class FullProvision implements CardTask {
@Inject private ApduChannel channel;
@Inject private AppletManifest manifest;
@Override
public void run() {
for (var applet : manifest.applets()) {
channel.transmit(Iso7816Commands.selectDf(applet.instanceAid()));
// provision each applet
}
}
}When your applet requires authenticated commands (e.g., writing keys or sensitive data), open your own SCP session within the task:
@CardTaskDef(value = "provision", order = 200)
public final class WriteKeys implements CardTask {
@Inject private ApduChannel channel;
@Inject private SecureChannelProfile scpProfile;
@Override
public void run() {
SecureChannelSession session = SecureChannelSession.create(channel, scpProfile);
session.authenticate();
try {
session.transmit(new CommandApdu(0x80, 0xD4, 0x00, 0x00, keyData));
} finally {
session.close();
}
}
}Test tasks run after provisioning and verify that the card state is correct:
@CardTaskDef("test")
public final class VerifyPinWorks implements CardTask {
@Inject private ApduChannel channel;
@Inject private AppletManifest manifest;
@Override
public void run() {
byte[] aid = manifest.applets().get(0).instanceAid();
channel.transmit(Iso7816Commands.selectDf(aid));
ResponseApdu response = channel.transmit(
Iso7816Commands.verify(0x01, "1234".getBytes()));
if (response.sw() != 0x9000) {
throw new RuntimeException("PIN verify failed: SW=%04X".formatted(response.sw()));
}
}
}A profile defines where APDUs go and how the secure channel authenticates.
The default profile targets the Oracle JCDK simulator with SCP03 and well-known test keys. No configuration needed — just ./gradlew cinpoRun.
Create profile/<name>.yaml in the application project directory. Named profiles resolve only from profile/. Path-like values such as --profile ./secrets/card.yaml are resolved relative to the application project directory; absolute paths are used as-is.
schema: cinpo.profile.v1
name: mycard
runtime: pcsc
pcsc:
# Optional. When set, CINPO uses this reader only and does not fall back.
reader: My Smart Card Reader
# Optional. Used only for automatic reader selection.
excludedReaders:
- Built-in Contactless Reader
secureChannel:
protocol: scp03
isdAid: A000000151000000
keyVersionNumber: 16
keyIdentifier: 0
securityLevel: 1
encKey: <your 32-byte hex ENC key>
macKey: <your 32-byte hex MAC key>
dekKey: <your 32-byte hex DEK key>Use it:
./gradlew cinpoRun --args='--profile mycard'
# or
java -jar cinpo-appliance.jar write --profile mycardFramework changes should pass unit tests, artifact publication, and simulator-backed application flows without adding project-specific verification tasks:
./gradlew test publishAllPublicationsToStagingRepository
(cd template && ../gradlew cinpoRun --args='--test')The application flows consume the published CINPO artifacts, generate CAP resources, start the managed Oracle JCDK simulator, install the applets, run provisioning, and execute --test tasks. This catches schema drift, missing generated resources, profile resolution mistakes, and simulator/runtime classpath regressions that unit tests cannot see.
runtime value |
Transport |
|---|---|
jcresim |
Oracle JCDK simulator (auto-managed) |
pcsc |
PC/SC card reader (javax.smartcardio) |
protocol |
Key size | Notes |
|---|---|---|
scp03 |
16 or 32 bytes | Recommended. AES-based. |
scp02 |
16 bytes | Legacy 3DES-based. C-MAC only. |
CINPO is source-available, not open source. It is provided under the AokiApp Normative Application License - Tight. Review that license before using, modifying, or distributing the software. Third-party components remain subject to their own terms; see Third-Party Notices.