Skip to content

DSS Signing Server

The {{ product_name }} DSS signing server (alpine-dss-server) is an eIDAS remote-signing service. It performs hash-only document signing: callers send a digest (never the document), and the server signs it with a private key held in an HSM partition pool through the {{ product_name }} HSM abstraction (alpine-hsm / alpine-pools). Signing always runs on the tenant's Securosys Primus partition: the partition type is fixed in the code that resolves it, so there is no software-key signing path and no setting that could enable one.

It is a single Spring Boot process exposing a JSON/REST API. In the signing chain it is the central, shared service that the on-prem DSS Proxy calls with only document hashes.

How the document stays private

The server signs a digest, not a document. The document bytes never leave the caller (for the proxy, they never leave the tenant network). This is what makes the proxy → server split privacy-preserving.

Endpoints

Method & path Purpose Authentication
POST /api/server-signing/sign-ska Sign with a tenant's Securosys Primus SKA key, tenant/module-scoped Self-authenticating DSS-proxy JWT
GET / Branded service index (no signing surface) Public
GET /actuator/health Liveness/health probe Public

/sign-ska is the whole signing surface. Signing is always SKA-authorized: the private key stays in the tenant's Securosys Primus partition and every operation carries a hardware-enforced authorization from the key profile's authorization key.

Deployment

Prerequisites

  • Java 25 only.
  • A Securosys Primus HSM partition reachable through the {{ product_name }} HSM abstraction. Its host, user and PIN are read from the control database per request — never from this service's configuration.
  • PostgreSQL 18 control database — required to sign: /sign-ska resolves the caller's registered public key, licence and permission from it. The tenant must have a provisioned HSM partition (provision hsm), the dss feature license, and the dss.sign permission.
  • Optionally a JWT key source — a JWKS URI, a public-key file, or an OIDC issuer — for the default-deny filter in front of every path that is not public and does not authenticate itself. Signing does not need one.

Build and run

# Build this module and its dependencies
mvn -pl 07_dss/alpine-dss-server -am package

# A control database is still required to sign: /sign-ska reads the caller's key,
# licence and permission from it.
java -jar 07_dss/alpine-dss-server/target/alpine-dss-server-<version>.jar \
  --alpine.dss.control-db.url=jdbc:postgresql://localhost:5432/alpine_control \
  --alpine.dss.control-db.username=dss_ro \
  --alpine.dss.control-db.password="$ALPINE_DSS_CONTROL_DB_PASSWORD"
java -jar alpine-dss-server-<version>.jar \
  --alpine.dss.control-db.url=jdbc:postgresql://db-host:5432/alpine_control \
  --alpine.dss.control-db.username=dss_ro \
  --alpine.dss.control-db.password="$ALPINE_DSS_CONTROL_DB_PASSWORD"

Add a JWT key source (--spring.security.oauth2.resourceserver.jwt.jwk-set-uri=…) only if something other than /sign-ska is to be reachable; signing itself does not consult it.

The server listens on port 8080 by default (--server.port=<port> to change it). There is no bootstrap step: the server is ready as soon as a partition and the control database are configured. If /sign-ska is called without a control database, it returns 503 Service Unavailable.

Configuration

Configuration is in 07_dss/alpine-dss-server/src/main/resources/application.yml and can be overridden with command-line arguments or environment variables (Spring relaxed binding).

Key Env override Default Description
server.port SERVER_PORT 8080 HTTP listening port.
alpine.dss.security.permit-all ALPINE_DSS_SECURITY_PERMIT_ALL false Dev only — permit every request at the resource-server layer. /sign-ska still verifies its own DSS JWT.
alpine.dss.security.max-request-bytes ALPINE_DSS_SECURITY_MAX_REQUEST_BYTES 16777216 (16 MiB) Largest request body accepted under /api/**. An over-sized Content-Length is refused with 413; a chunked body is aborted once it streams past the cap. Zero or less falls back to the 16 MiB default.
alpine.dss.signing.timeout-seconds ALPINE_DSS_SIGNING_TIMEOUT_SECONDS 30 Max wait for a signing operation on the partition-pool worker.
alpine.dss.control-db.url ALPINE_DSS_CONTROL_DB_URL (unset) JDBC URL of the read-only control DB. Set enables SKA signing; unset → /sign-ska returns 503.
alpine.dss.control-db.username ALPINE_DSS_CONTROL_DB_USERNAME (unset) Control-DB user (a read-only role is recommended).
alpine.dss.control-db.password ALPINE_DSS_CONTROL_DB_PASSWORD (unset) Control-DB password (supply via env / secrets manager).
alpine.dss.control-db.schema ALPINE_DSS_CONTROL_DB_SCHEMA (unset) Optional schema; may be blank.
alpine.dss.control-db.maximum-pool-size ALPINE_DSS_CONTROL_DB_MAXIMUM_POOL_SIZE 4 HikariCP maximum pool size.
spring.security.oauth2.resourceserver.jwt.jwk-set-uri (relaxed binding) (unset) JWKS endpoint (RSA/EC, rotatable via kid).
spring.security.oauth2.resourceserver.jwt.public-key-location (relaxed binding) (unset) Single RSA public-key file (classpath: or path).
spring.security.oauth2.resourceserver.jwt.issuer-uri (relaxed binding) (unset) OIDC issuer (auto-discovers the JWKS URI).

Table: the DSS signing server's operator-facing configuration keys. There is no partition setting here — the partition is resolved per caller from the control database. Supply a JWT key source only if a path other than /sign-ska has to be reachable.

permit-all is a development escape hatch

alpine.dss.security.permit-all=true makes every path unauthenticated at the resource-server layer and logs a prominent warning at startup. Never enable it in production. It does not weaken /sign-ska, which verifies the dss-proxy's DSS JWT itself.

HSM PINs are never in this configuration

The server does not hold HSM connection secrets. Partition host, PIN and credentials are managed in the HSM partition registry (see provision hsm) and resolved by the alpine-hsm driver at runtime — never printed in this service's config.

Authentication

The server has two authentication layers: the signing endpoint authenticates itself, and a resource-server filter denies everything that is neither public nor self-authenticating.

/sign-ska self-authenticates the dss-proxy's client-signed DSS JWT via DssProxyAuthenticator instead of the resource-server filter. It verifies the token's issuer, audience, kid and tenant claim, then looks up the caller's public key scoped to the tenant's DSS module instance ((tenantId, kid)) from the control DB — a key registered for a different module/tenant is rejected. The request's in-band tenantId and moduleUuid must match the authenticated identity (fail-closed). The signing call is then gated by the dss feature license (@LicenseRequireFeature) and the dss.sign permission (@RequiresAuthorization), both resolved from the control DB.

The index page, its static assets, the styled error view and GET /actuator/health are public; /sign-ska is permitted at this layer because it verifies its own token. Every other path requires a bearer token, which Spring Security's OAuth2 resource server validates as a client-signed JWT (RSA or EC) against the configured key source (jwk-set-uri / public-key-location / issuer-uri), time claims (exp, nbf, iat) included. With no key source configured and permit-all=false, those paths are denied outright and the server logs a warning at startup — which costs nothing today, because no endpoint of this service sits behind that filter.

Error responses

HTTP status When
401 Unauthorized Missing/malformed Authorization header; invalid signature; expired/future token; bad claims.
403 Forbidden Tenant/module mismatch; dss feature not licensed; dss.sign permission absent.
400 Bad Request Invalid request body; a configuration error raised while resolving the signing context.
413 Content Too Large A request under /api/** whose declared Content-Length exceeds alpine.dss.security.max-request-bytes. A body with no declared length is not rejected up-front: it is aborted mid-read once it passes the cap.
502 Bad Gateway Control-DB or HSM error.
503 Service Unavailable /sign-ska called but no control-DB connection is configured.

FAQ

Does the signing server ever see the document?

No. It signs a digest only. The caller (for on-prem use, the DSS Proxy) hashes the document locally and sends just the hash, so the document bytes never reach the server.

Do I need a database to run the signing server?

Yes, to sign. /sign-ska is the only signing endpoint and it resolves the caller's key, licence and permission from the control DB. When alpine.dss.control-db.url is unset the server still starts and serves the index and the health probe, and /sign-ska returns 503 Service Unavailable.

What key source should I use for authentication?

Supply exactly one of spring.security.oauth2.resourceserver.jwt.jwk-set-uri (JWKS, supports key rotation), …public-key-location (a single RSA key), or …issuer-uri (OIDC discovery). It is the key source for the default-deny filter, not for /sign-ska, which verifies the dss-proxy's DSS JWT against a key held in the control DB. A deployment that only signs therefore needs no key source at all.

How is qualified (eIDAS) signing enforced?

By construction rather than by configuration. /sign-ska resolves a Securosys Primus partition and signs with an SKA-protected key held in it, so there is no software-key path to switch off. The partition is registered and reachable through the HSM registry — see provision hsm.