ti, the command-line tool for the gematik Telematikinfrastruktur (TI), in Rust, built
on ti-pki, ti-connector-client and
jwz. Commands are grouped by subsystem: pki, identity and connector.
just install # cargo install into ~/.cargo/bin
just install-fast # a quicker build for testing, same place
ti pki inspect card.pem # what the TI reads from a certificate
ti pki inspect - < card.der
ti pki inspect identity.p12 # PKCS#12; password 00 unless --p12-password
ti pki inspect card.pem > card.md # piped output is Markdown, with the PEM
ti pki inspect roots.pem --short # a row per certificate: subject, expiry, Telematik-ID
ti --format json pki inspect card.pem | jq '.certificates[0].certificate_type'
ti pki profiles list
ti pki profiles describe hci-aut
ti pki profiles describe hci-aut --env ref # and the TSL CAs that may issue its types
ti pki verify card.pem # exit 0 valid, 1 not valid
ti pki verify smcb.p12 # the certificate with its key is the end entity
ti pki pkcs12 convert old.p12 new.p12 # DER, PBES2 AES-256, SHA-256 MAC; mode 0600
ti pki verify card.pem --offline # cached trust material, no OCSP
ti pki verify --connect epa-as-1.ref.epa4all.de # a TLS server's chain, its name as --fqdn
ti pki roots list --env ref # the roots reached from the anchor
ti pki tsl show # the TSL's CAs under the roots that signed them
ti pki tsl show --rejected # the CAs no verified root signed, and why
ti pki tsl show --ca SMCB-CA51 # one CA; Markdown adds its PEM
ti pki tsl verify ECC-RSA_TSL.xml ECC-RSA_TSL.sig # signature, signer, signer OCSP; exit 0 valid
ti pki verify-signature --cert vau.pem --data keys.cbor --signature sig.bin # one ECDSA signature
ti identity inspect --p12 smcb.p12 --p12-password-path pw.txt # the AUT certificate and key
ti identity sign --p12 smcb.p12 --claims - < claims.json # ES256 JWS, x5c the certificate
ti identity sign --card 80276883110000163974 --claims claims.json # the selected Konnektor signs
ti idpd authenticate --env ref --p12 smcb.p12 --auth-url "$AUTHZ_URI" # IDP-Dienst → authorization code
ti directory login owner --env ref --p12 smcb.p12 # FHIR-Directory → owner access token
ti connector config list # the .kon files (~/.config/telematik/connectors)
ti connector use praxis # the configuration later commands use; no name: the list
ti connector config new praxis # step by step on a terminal: address, pin, credentials, context
ti connector config test praxis # tcp, tls, auth, sds, context, cards; exit 1 on a failure
ti connector config show praxis # the file without its secrets
ti connector config pin praxis # pin the Konnektor's new server certificate
ti connector config edit praxis # in $EDITOR; only a valid copy replaces the file
ti connector -c praxis config set workplaceId=W2 # one field without a secret
ti connector get cards
ti connector get certificates 80276883110000163974 # ICCSN, Telematik-ID or handle
ti connector describe certificate 1-SMC-B-Testkarte-883110000129072 C.AUT
ti connector verify pin 80276883110000163974 # at the card terminal; exit 0/1
ti connector sign letter.txt --card 80276883110000163974 # CAdES → letter.txt.p7s
ti connector sign report.pdf --card 80276883110000163974 # PDF/A → report.signed.pdf (PAdES)
ti connector verify signature letter.txt --signature letter.txt.p7s
ti connector encrypt letter.txt --to recipient.pem # → letter.txt.p7m
ti connector encrypt letter.txt --to-card 80276883110000162094 # for a card's C.ENC
ti connector export certificate 80276883110000162094 C.ENC > enc.pem
ti connector decrypt letter.txt.p7m --card 80276883110000163974
ti connector comfort activate 80276883110000163974 # PIN.QES once, then signatures without
ti probe ref # which TI services of ref answer, live (or --env ref)
ti --profile work pki verify card.pem # another profile's cache and state
ti profile list # the profiles and the size of their state and cache
ti cache clear
ti schema pki verify # JSON Schema of a command's output
ti agent # usage guide for scripts and agents
ti versionjust cli-targets builds release binaries into target/dist for Linux x86_64 (static
musl), Windows x86_64 and, on a Mac, macOS on Apple silicon: the foreign ones through
cross in Docker.
Shell completion comes from the binary itself, so it always matches its commands:
ti completions zsh > ~/.zfunc/_ti # zsh (with fpath+=~/.zfunc)
ti completions bash > ~/.local/share/bash-completion/completions/ti
ti completions fish > ~/.config/fish/completions/ti.fish
ti completions powershell >> $PROFILE # also: elvishReleases are on GitHub, tagged
rust/ti-cli/vX.Y.Z, with a binary for macOS on Apple silicon, Linux x86_64 (static)
and Windows x86_64.
-
Homebrew (macOS, Linux):
brew install gematik/tap/ti;brew upgrade tiupdates,brew autoupdatekeeps it current.tiitself never checks for updates. -
Download: take the binary for your platform with
SHA256SUMS, then check it:shasum -a 256 -c SHA256SUMS --ignore-missing chmod +x ti-*-* && mv ti-X.Y.Z-<target> ~/.local/bin/ti
-
From source:
cargo install --locked --git https://github.com/gematik/zero-lab --tag rust/ti-cli/vX.Y.Z ti-cli, orjust installin a checkout.
pki roots list and pki tsl show show what verify works with, for one environment
(--env, default prod; there is nothing to detect from here, so TI_ENV=auto means
prod and --env auto is an error). roots list gives the
roots the A_28419 walk reaches from the embedded anchor. tsl show lists the TSL's CAs
with the verified root that signed each, or in red why none did. Filter with --ca,
--provider, --root and --rejected. The anchor is GEM.RCA7 (TEST-ONLY outside
production), a P-256 root.
pki roots bundle and pki tsl bundle write those certificates as a CA bundle for
other tools: PEM by default (curl --cacert, openssl -CAfile, Go, rustls), or with
--p12 --p12-password PASSWORD a PKCS#12 truststore for Java, each certificate a
trustedCertEntry under its common name in lower case (the bag attributes OpenSSL's
-caname and -jdktrust anyExtendedKeyUsage write). tsl bundle takes tsl show's
--ca, --provider, --root and --issues (each repeatable; --issues TLS-S keeps
every CA the TSL lets issue TLS server certificates) and never bundles a rejected CA;
--connect HOST and --cert FILE narrow it to the CAs a given server or chain needs
(TI servers send only their own certificate), and --with-roots adds the roots they
chain to. pki tsl export
writes the TSL as published, after it verified. All three go to stdout, or to -o FILE
(--force replaces it), and report as JSON with --format json.
The TSL is verified wherever it is loaded (spec/tsl-detached): the detached signature, signer under
the embedded TSL signer CA, NextUpdate, the list seen before (kept in the profile's
state, so an older list stays rejected), and online the signer's OCSP status;
offline or with --at the trust line says the status was not checked. A list that
fails is not used at all. Of each entry the view shows only what the CA certificate says
(name, organization, validity) and which verified root signed it; the TSL's per-CA
metadata (provider names, certificate types per CA) is never shown as fact. The provider
name is in the JSON, for filtering. --offline works from the cache; --at shows the
material as of another time.
pki tsl verify TSL SIG checks a TSL file: its detached signature, a C.TSL.SIG
signer issued by the embedded TSL signer CA (GEM.TSL-CA3 in production, GEM.TSL-CA28
TEST-ONLY elsewhere), NextUpdate with --grace DAYS (default 0), and the signer's OCSP
status unless --offline or --at, or from --signer-status FILE.ocsp. --previous OLD.xml requires another Id and a
greater sequence number, or the same list. --env auto, the default, takes the
environment whose TSL signer CA issued the signer; --env prod accepts a production TSL
only. An invalid list is exit 1 with the result code and the rule of spec/tsl-detached
or spec/tsl-xmldsig that failed; entries that cannot be processed are skipped and
listed.
ti pki verify builds the chain to the TI roots through the CAs of the TSL and
validates it: RFC 5280 path, gemSpec_Krypt key, the requirements of the profile
(--validation-profile auto|none|<name>), and OCSP for the end entity; a CA's standing
is its status in the verified TSL (gemSpec_PKI TUC_PKI_018 steps 5 and 7).
The environment comes from --env (TI_ENV) or, with auto, from the certificates:
production only on evidence from the production roots, otherwise ref. --at validates
at another time. OCSP still answers for now, and its status is judged at that time
(gemSpec_PKI TUC_PKI_006): a certificate revoked since is valid then, with the warning
revoked_after_reference_time.
roots.json and the TSL are downloaded from the environment's URLs and verified by
ti-pki against the embedded anchor; a CA from the TSL counts only if a verified root
signed it. Where they came from does not matter, so the cache is as untrusted as the
network. The revocation line reports the end entity's outcome (not revoked,
revoked, unknown, failed); revocation_mode in JSON names the
policy. --ocsp-responder URL sends the OCSP request to one responder,
e.g. a relay where the TI responders are not reachable, and --ocsp-max-age DURATION
(500ms, 30s, 5m, 1h) accepts responses older than the default 37.5 s, for
responders that answer from a cache; the TSL signer's check keeps the defaults.
--offline makes no request: it uses the cached material, or the embedded roots
without a TSL when nothing usable is cached (then pass the issuing CA with --issuer),
and does not check revocation. The report says so ("revocation_checked": false).
--nist-only (also on pki roots list, pki roots bundle and pki tsl bundle) is for
a client without brainpool, such as a browser, Go's crypto/x509 or rustls with ring. The
trust material is still walked and verified with brainpool, since the newer roots are
reached through brainpool cross certificates and the TSL is signed on brainpool; only
the result is narrowed to the roots and TSL CAs whose own key is not brainpool
(GEM.RCA2, 6, 7, 9 and 10 and their CAs). pki verify --nist-only then validates against
that subset with no brainpool signature allowed in the chain; brainpool chains come out
invalid. The report carries "nist_only": true.
One difference from curl and Java remains: pki verify --nist-only needs a path up to a
root whose own key is not brainpool, while curl and Java accept any certificate of a
bundle as an anchor, including a TSL CA of tsl bundle --nist-only. A chain under a
NIST CA whose root is brainpool (under GEM.RCA8, for example) is therefore invalid in
ti but validates in curl or Java with that bundle.
ti pki verify-signature checks a single ECDSA-SHA256 signature (DER, or r‖s) against
a certificate's key with ti-pki's verifiers, and nothing about the certificate: an ePA
client checks the VAU's signed_pub_keys this way and the certificate with pki verify.
ti identity is the SMC-B or HBA as a client uses it: the AUT certificate
(digitalSignature, no contentCommitment: C.HCI.AUT or C.HP.AUT) with its key, from a PKCS#12 file (--p12, the AUT pair selected
among a card export's certificates), a PEM certificate and key (--cert --key), or a
card at the Konnektor (--card, the Konnektor resolved as the connector commands do,
--connector naming another; signing through ExternalAuthenticate with the PIN verified
before, PIN.SMC of an SMC-B or PIN.CH of an HBA). inspect shows what was selected, the Telematik-ID and the
curve; sign turns a JSON object of claims into a compact ES256 JWS with x5c, the
form ePA's clientAttest and entitlement JWTs and the IDP-Dienst's nested challenge
take. The key lives only in the ti process for the duration of the call; callers
such as epa pass the PKCS#12 password by file (--p12-password-path,
TI_P12_PASSWORD_PATH), never on the command line.
ti idpd authenticate is the Authenticator-Modul of gemSpec_IDP_Frontend, built on
ti-idpd-client: given the authorization URL a relying party made
(for ePA, the Aktensystem's authz_uri) and an identity, it fetches and verifies the IDP's signed
discovery document and keys, signs the challenge with the card (BP256R1, x5c),
encrypts the answer to PuK_IDP_ENC and returns the authorization code from the IDP's
redirect — what epa sends on in send_authcode_sc. The IDP is the environment's
(--env) or --idp-url. Before the card signs, the IDP's certificates (the
discovery document's signer and PuK_IDP_SIG's) are checked against the environment's
TSL with the idpd-sig profile and OCSP, as gemSpec_IDP_Dienst requires;
--idp-unverified skips that for an IDP outside the TI PKI, and the report warns.
ti directory login obtains an access token of the FHIR-Directory (VZD), built on
ti-directory-client, with one subcommand per Auth-Service
endpoint: owner by card through the IDP-Dienst, owner with a TI-Messenger
RegService-OpenID-Token, search for the Search API of an SMC-B or HBA (the owner
token, the only one the directory issues a card identity), and holder, service and
ti-provider with
client credentials at the directory's Keycloak (the secret only from a file). Every
path reports the same document; --token prints only the bearer for curl. The
directory, its IDP and its Keycloak are the environment's (--env) or given by
--url, --idp-url, --auth-url; card redirects must stay between the directory and
its IDP, or the flow stops before the card signs.
ti connector talks to a Konnektor configured by a .kon file (the format the former Go
ti introduced): -c NAME|PATH or TI_CONNECTOR_CONFIG, else the one connector use selected,
else default; names are looked up here and in ~/.config/telematik/connectors/.
${NAME} in a .kon file is expanded only in the credentials, so a file from someone
else cannot send environment variables to a foreign host. Calls time out after
--connector-timeout (10 s), calls that wait for the card terminal after
--card-timeout (300 s); -v prints one line per call, -vv the SOAP bodies. SMC-KT,
KVK and eGK are restricted on the Konnektor's SOAP API; calls about them fail with an
explanation. While a PIN is entered, a terminal shows a spinner on stderr, a busy
tab (OSC 9;4: Ghostty, iTerm2, Windows Terminal, cmux) and one desktop notification (OSC
9). PKCS#12 client certificates must be P-256, P-384 or RSA for now (TLS runs
on ring, which has no brainpool). Comfort signature keeps each session's random user ID
in a private file of the profile's state (Files).
The tool is meant for people and for agents alike:
- Results go to stdout, diagnostics (
-v,-vv) and errors to stderr. --format auto|text|markdown|json(orTI_FORMAT).autogives aligned, colored text on a terminal and Markdown when piped: readable in notes, chats and by agents. The terminal view is sectioned and complete (Subject, Issuer, Validity, …; Result, Chain, Errors). Lists of objects (cards, roots, CAs, profiles, …) are tables with the focus column first, on the terminal and in Markdown; see "CLI output" in docs/development.md for the columns of each. Markdown is otherwise compact, with certificates as fenced PEM blocks, which the terminal view leaves out.jsonwrites one document with a"schema"version; fields are only added within a schema version. Certificates are inpemfields.ti schema [COMMAND]prints the JSON Schema of each command's output (kept inschemas/, checked against real output by the tests), andti agentprints AGENTS.md, the guide for scripts and agents. Errors in JSON mode are{"schema":1,"error":{"kind","message","hint"}}on stderr.- Times in text and Markdown are in the system time zone:
2023-02-09 00:00 CETfor a single instant, the date alone in lists (TZis honoured). JSON keeps RFC 3339 in UTC. - Colors and JSON highlighting only on a terminal;
--color,NO_COLORandCLICOLOR_FORCEoverride. Long lines are never cut; the terminal wraps them. - Never interactive. Every input is a flag, an environment variable, a file or stdin.
- Exit codes:
0success (or: the certificate is valid),1not valid,2to5failures; every error kind with its exit code is in AGENTS.md (ti agent).
ti keeps plain files in two directories, the state and the cache. Without --profile
(or TI_PROFILE) they are the bases below; --profile NAME uses profiles/NAME inside
each, created on first use. A name is 1 to 64 characters of A-Z a-z 0-9 . _ - and does
not start with a dot; no name is reserved, and no profile is remembered between runs.
| Linux, macOS | Windows | |
|---|---|---|
| State base | $XDG_STATE_HOME/telematik/ti, else ~/.local/state/telematik/ti |
%LOCALAPPDATA%\state\telematik\ti |
| Cache base | $XDG_CACHE_HOME/telematik/ti, else ~/.cache/telematik/ti |
%LOCALAPPDATA%\telematik\ti |
| TSL state | STATE/tsl/ID.json |
the same below STATE |
| HBA user IDs, comfort IDs among them | STATE/hba-user/KONNEKTOR/CARD |
the same below STATE |
| Cache entries | CACHE/v1/SHA-256 of the key |
the same below CACHE |
TI_STATE_DIR replaces the state base and TI_CACHE_DIR (or --cache-dir) the cache
base; both must be absolute paths. A profile's directories lie below the replaced bases
as below the defaults. ti profile list shows the profiles with the size of their state
and cache, and ti profile delete NAME --yes removes one.
A throwaway run leaves nothing behind that a later run reads. Either name a profile that
is never used again, and remove it with ti profile delete, or point both bases at
temporary directories:
ti --profile "$(uuidgen)" pki tsl show
TI_STATE_DIR=$(mktemp -d) TI_CACHE_DIR=$(mktemp -d) ti pki tsl showA fresh profile or TI_STATE_DIR has no TSL state, so rollback protection restarts from
the next verified TSL.
The Konnektor configurations (.kon files, ~/.config/telematik/connectors) are outside
the profiles and shared by them. A comfort ID belongs to its profile.
The cache holds downloaded trust material, OCSP responses and Konnektor service
directories with their HTTP validators. An entry is used without asking for an hour,
then revalidated with its ETag; when the network fails it serves for up to a day more.
Material older than 24 hours (production) or 7 days (elsewhere) is not used. Entries
are written whole by renaming a temporary file; a put removes entries not written for 30
days. ti cache clear empties the cache and leaves the state alone.
The state holds the TSL seen last and one user ID per HBA. Every call on an HBA uses one ID:
the one in its file, else the .kon file's userId, else a new one kept in the file.
comfort activate writes a new ID to the file, and comfort deactivate removes the file.
A file left from before a userId was added to the .kon file goes first until then, so
run comfort deactivate once after adding it. The TSL state is kept on
every run without any setup, except with --at, which neither reads nor writes it
(a past instant may need an older list), so an older list stays rejected; it is not secret. A
corrupt TSL state file is an error that names the file (state_file_failed); removing it
restarts from the next verified TSL.
A list older than the kept one is rejected (tsl_id_incorrect, under
trust_material_unavailable). It can be an attack or a mirror lagging behind, so find out
why first: try another network, and check any proxy and -k. Only if the kept list is known
to be wrong, remove tsl/ID.json in the state directory; the check then restarts from the
next verified TSL.
An HBA's user ID lies unencrypted in a file of mode 0600 in a directory of mode 0700 (POSIX).
It lets anyone with the same call context use what its session has unlocked, in comfort
signature to sign without a PIN, so a backup or copy of the state directory exposes it. ti
refuses such a file, or the hba-user/ directory and the one for the Konnektor, that is a
symbolic link or open to group or others, and says what to chmod. These checks stop at
hba-user/: they do not
cover the state directory above it, and a TI_STATE_DIR shared with other users gives no
such protection. comfort deactivate removes the file. No token and no other secret is
written to disk.
Files of the former sealed store (state.db, cache.db, keyring.bin, store.kek) are
not read and may be removed by hand: ~/.local/state/telematik/profiles,
~/.cache/telematik/profiles, ~/.config/telematik/profiles. Comfort is activated
again, and the TSL state restarts from the next verified TSL.
The HTTP options follow curl: -k/--insecure, --cacert, --capath, -x/--proxy,
--noproxy, --connect-timeout (default 10 s), -m/--max-time (default 60 s),
--retry, -A/--user-agent, and the usual environment variables (HTTPS_PROXY,
HTTP_PROXY, ALL_PROXY, NO_PROXY, CURL_CA_BUNDLE, SSL_CERT_FILE,
SSL_CERT_DIR). As in curl, http:// and https:// URLs have their own proxy
variable with ALL_PROXY as fallback, and the operating system's proxy settings are not
read. The client is ureq, blocking and without an async runtime: a command makes a few
sequential requests. TLS is rustls with the ring provider passed explicitly, as in
kartos. Every https server is verified under the web profile of
spec/pki-validation (crate ti-tls): the operating
system's roots, unless --cacert/--capath (or the variables) name others, which then
replace them as in curl; on top, end-entity keys ECDSA P-256/P-384 or RSA ≥ 3000, no SHA-1,
serverAuth, at most 398 days, and a revocation status by OCSP or CRL, or no connection. The
TSL download points, the IDP-Dienst and the FHIR-Directory all present public certificates.
-k affects only downloads, prints a warning and sets insecure_transport in JSON: trust
material is verified cryptographically either way. The IDP-Dienst and the FHIR-Directory are
always verified.
EUPL-1.2. Copyright, licence and gematik's notes: repository README.