Setting up seals¶
A seal proves to the recipient of a calibration certificate that the document came from this laboratory and has not changed since its release. This page describes how a laboratory obtains a seal, what distinguishes the four routes, and how to verify the setup.
Management lives under Administration → Seals and requires the seal_edit
operation.
The four routes compared¶
All four end up in the same list. They differ in where the private key lives and what a recipient can conclude from the seal.
| Route | Key created | Proof for the recipient | When |
|---|---|---|---|
| Create it here | Inside the seal service | "Validity unknown" — proves integrity, not origin | Internal use, testing, immediately |
| Have a CA issue it | Inside the seal service, never leaves it | Origin proven, issuer visible | The normal case, including accreditation seals |
Import an existing .p12 |
Outside, travels as a file | Whatever the purchased certificate supports | Certificate already bought |
| Configure remote signing | At the trust service provider | The only setup that can be a qualified seal | Procurement running or complete |
A self-signed seal is not proof of origin
It takes seconds to create and looks like a seal in the list. Every PDF viewer shows "validity unknown" for it, because nobody but the laboratory itself vouches for the certificate. Right for internal use and for trying things out, wrong for a certificate that goes to a customer.
Prerequisites¶
-
The dedicated seal service. The host
.envneedsSEAL_SERVICE_URL=http://calserver-seal:8091. The deprecatedreport-runnercannot manage credentials; without that line, provisioning is refused with exactly this hint.deploy.shsets it on new installations. -
A keystore password.
PDF_SIGNING_PASSWORDprotects the key files. Without it no credential can be created and none can be opened.deploy.shgenerates one when the.envdoes not know the line yet.Belongs in the password manager
Without this password existing seal credentials are lost — from a backup as well. Vaultwarden item "calServer seal keystore" per installation.
-
A writable signing directory — see Signing directory. Check:
Expect
"provisioning":"enabled". On"directory-read-only"theprovisioningDetailfield of the same response names the path and both possible causes. -
Laboratory details filled in. Administration → Reports → Laboratory details holds the laboratory name, its code (accreditation number), address and contact. The setup dialog pre-fills the certificate holder from there, so letterhead, DCC export and seal name the same identity.
Creating a company seal yourself¶
- Administration → Seals → Set up seal
- Pick "Create it here"
- Check the values — they come from the laboratory details
- Set up
The credential appears in the list with the assurance "self-declaration" and
level PAdES-B-B. It can be used immediately.
Accreditation seal via a certificate authority¶
The route on which no key material leaves the installation. It spans several days, because the certificate authority works in between.
Step 1: create the credential and fetch the request¶
- Set up seal → "Have a certificate authority issue it"
- Set the kind to accreditation seal, check the accreditation number (e.g.
D-K-12345-01-00) - Set up — the dialog then shows the next step
- Download the certificate request (
<reference>.csr)
Where the accreditation number ends up
In the certificate as organizationIdentifier (OID 2.5.4.97, EN 319 412-1),
which is where assessment tooling looks for it. The credential's subject line
spells it out (organizationIdentifier=D-K-12345-01-00).
Seals created before seal service 1.3.0 still show 2.5.4.97=#0c0f… there. A
sync brings the credential list up to date; the subject line recorded against
an already sealed document stays as it was — a record is not rewritten after
the fact.
Step 2: submit the request¶
Submit the .csr file to the certificate authority. Ask for a certificate for
digital signatures (digitalSignature, nonRepudiation); the extensions are
part of the request.
Step 3: install the certificate¶
- In the list, choose Install certificate on the credential
- Upload the certificate file (
.cer,.crt,.pem,.p7b) or paste the PEM text - Install certificate
The key stays the same, the assurance changes to "issued by a CA". A certificate belonging to a different key is refused.
Include intermediate certificates
Level PAdES-B-LT needs the chain and revocation data inside the document.
If the CA offers a bundle (certificate plus intermediates), upload that one
rather than the leaf alone.
The previous keystore stays next to the new one as
<reference>.p12.replaced-<epoch>. It is no longer listed as a credential, but
the key is not lost if the wrong certificate was installed.
Importing an existing key file¶
For a certificate that has already been purchased.
- Set up seal → "Import an existing key file"
- Choose the
.p12/.pfx, enter its password - Set up
The seal service re-wraps the keystore with the operating password. The file's own password is used once to open it and stored nowhere.
The only route on which the key travels
The file goes from the browser through the API to the seal service. It is stored there and only there — not in the database, not in a log. To avoid even that, use the certificate request, or keep placing the file by hand (see below).
Configuring remote signing¶
A credential at a trust service provider is the only setup in which a seal can be qualified: the key lives on the provider's qualified signature creation device rather than in a file on the server.
- Set up seal → "Configure remote signing …"
- Provider, protocol, service and token address (
https://only), client ID, client secret, credential ID - Paste the provider's certificate as PEM
- Set up
- Use the network icon in the list to check the provider is reachable
Which providers work
The CSC interface (Cloud Signature Consortium API v2) is supported —
protocol value csc-v2. That is the standard most European providers speak.
A provider with a protocol of its own (Swisscom AIS, GlobalSign DSS) can be
configured, and the list then says no adapter exists for that protocol — the
entry is still worth having, because procurement takes weeks.
The access data lives with the seal service, not in the database.
No real provider has been tested
The adapter is built against the specification and verified against a test
server that plays it. Expect deviations at the first real provider: the
specification settles neither the OAuth grant for unattended sealing nor,
unambiguously, which OID belongs in signAlgo. Those values are therefore
configurable per credential (OAuth grant and signAlgo OID), so a
deviation is configured rather than patched.
Three fields decide the flow:
| Field | Default | When to change it |
|---|---|---|
| OAuth grant | client_credentials |
When the provider requires a different grant for automated sealing |
| Authorize each signature | on | Off only when the provider issues a token already scoped to the credential |
| signAlgo OID | empty (derived) | Only when the provider rejects the derived OID |
The reachability check deliberately says nothing about whether the access data is accepted — establishing that would take an actual signature, and at most providers that costs money. It rules out the failure that actually bites: a container with no outbound network.
When sealing goes wrong¶
A remote seal has an outcome a local key does not: indeterminate. If the provider does not answer, it may still have signed, recorded it and charged for it — only the answer is missing. calServer records that as "not determinable" rather than as a failure.
On 'not determinable', check with the provider first
A blind retry otherwise buys a second signature over the same document — and a second invoice. When the message is unambiguous ("nothing was signed"), retrying immediately is safe.
Still supported: placing the file by hand¶
The route from ADR-042 works unchanged and is the better one for many credentials at once:
# Put the key file into the directory from SEAL_KEYSTORE_PATH
# (default: ./pdf-signing, relative to the docker/ directory).
# The file name without .p12 is the reference: acme-company.p12 -> acme-company
cp acme-company.p12 ./pdf-signing/
# Per-credential password, if it differs from PDF_SIGNING_PASSWORD:
# PDF_SIGNING_PASSWORD_ACME_COMPANY=...
# (reference upper-cased, every non-alphanumeric to _; then recreate the container)
# Then in the UI: Administration -> Seals -> Sync with seal service
A newly placed file takes effect without a restart — the directory is read on
every call. A changed environment variable needs
docker compose up -d calserver-seal.
A hand-copied file needs the right owner
A .p12 copied in as root cannot be opened by the service — it runs as
uid 10001. After copying:
Signing directory¶
This is where the key files live. It is the one piece of operations that setup from the UI depends on — and there are exactly two states in which that setup fails.
The service runs as uid 10001 (not as root, so a break-in to the container does not own the keys). The directory has to belong to it:
# Look at the state (the path inside the container is always /keystores)
docker compose exec calserver-seal wget -qO- http://127.0.0.1:8091/ready
# Correct the ownership if provisioning does not report "enabled"
docker compose exec -u 0 calserver-seal chown -R 10001:10001 /keystores
The ownership change takes effect at once — no restart, no redeployment. The next attempt in the UI succeeds.
provisioning |
Meaning | Remedy |
|---|---|---|
enabled |
All good | — |
directory-read-only |
The directory is there, the service may not write it | Set the ownership (above), or remove the :ro from the keystore mount in docker-compose.v2.yml |
read-only |
The service was started without a signing directory | Check the mount in docker-compose.v2.yml |
Two causes, one state name
directory-read-only covers both: a :ro mount and a directory with the
wrong owner. The provisioningDetail field of the same response names the
path and both possibilities. The second case is the more common one: when
Docker has to create the bind-mount source itself because it does not exist
yet, that directory belongs to root — with no :ro anywhere.
deploy.sh creates the directory and sets the ownership on every run. Doing it
by hand is only necessary when the deployment does not go through deploy.sh,
or when the ownership was changed afterwards.
Producing a seal for testing¶
The quickest complete run on a test installation:
- Fill in the laboratory details (at least the name)
- Set up seal → Create it here → Set up
- Release a calibration certificate and seal it
- Open the document in Adobe Reader: the signature is there, the holder is shown, the validity is unknown — which is exactly right for a self-signed certificate
For a run with real proof, use step 2 of the certificate request route and a CA that signs it; your own test CA is enough.
The seal service does not verify signatures
It creates them. Whether a seal is valid is decided by whoever views the document — so verify with a PDF viewer, not with calServer.
Troubleshooting¶
| Message | Cause | Remedy |
|---|---|---|
| "This installation still seals via the combined report-runner" | SEAL_SERVICE_URL missing |
Add the line to the host .env, recreate the containers |
| "The seal service does not manage credentials" | Directory does not belong to uid 10001, or the mount is :ro |
Signing directory |
| "the service may not write /keystores/…" (uid 10001) | The same thing, named by the service itself | Signing directory |
| "No keystore password configured" | PDF_SIGNING_PASSWORD missing |
Set it and run docker compose up -d calserver-seal |
| "The reference is already taken" | Duplicate name | Choose another reference; credentials are never overwritten |
| "None of the certificates … belongs to the key" | Certificate belongs to another request | Upload it to the matching credential |
| List stays empty after placing a file | File is in the wrong directory, or belongs to root |
Check SEAL_KEYSTORE_PATH — calserver-seal owns it, not the report-runner; then check the ownership |
Service state:
Reports the version, authentication, producible levels and
provisioning: enabled | read-only | directory-read-only.