Skip to main content

Security

Keyfactor SignServer

SignServer performs digital signing and time stamping as a network service. The private key stays on the server (or in an HSM behind it), so build machines and developer workstations never hold signing material.

Install

sudo apt install -y openjdk-21-jdk

For a database-less deployment, in conf/signserver_deploy.properties:

database.name=nodb
database.nodb.location=/opt/signserver/nodb

nodb keeps all state in memory and on disk at that path. It is fine for evaluation and for a build-time signer; use a real database for anything whose audit log has to survive a restart.

PKCS11CryptoToken does not work on Java 17 unless the application server's Java process gets:

JAVA_OPTS="--add-exports=jdk.crypto.cryptoki/sun.security.pkcs11.wrapper=ALL-UNNAMED"

The SunPKCS11 wrapper package was encapsulated by the module system, so without the export the token fails to initialise with an IllegalAccessError rather than an obviously key-related error. This affects hardware tokens and HSMs only — a soft key store needs nothing extra.

Run in Docker

Build a certificate chain for the TLS endpoint first:

# Root CA
openssl genrsa -out rootCA.key 2048
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.pem

# Intermediate CA — needs CA:TRUE, which -x509 would add but a plain CSR does not
openssl genrsa -out intermediateCA.key 2048
openssl req -new -key intermediateCA.key -out intermediateCA.csr
openssl x509 -req -in intermediateCA.csr \
-CA rootCA.pem -CAkey rootCA.key -CAcreateserial \
-extfile <(printf "basicConstraints=critical,CA:TRUE,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign\n") \
-out intermediateCA.crt -days 500 -sha256

# Server certificate, signed by the intermediate
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr
openssl x509 -req -in server.csr \
-CA intermediateCA.crt -CAkey intermediateCA.key -CAcreateserial \
-extfile <(printf "basicConstraints=CA:FALSE\nsubjectAltName=DNS:localhost\n") \
-out server.crt -days 500 -sha256

# Bundle key and chain for import
openssl pkcs12 -export -out server.p12 -inkey server.key -in server.crt \
-certfile intermediateCA.crt -certfile rootCA.pem

Two things the shortest version of this recipe gets wrong:

  • An intermediate signed without basicConstraints=CA:TRUE is an end-entity certificate. Anything it signs fails chain validation.
  • Modern TLS clients ignore the certificate's common name entirely. Without a subjectAltName, the connection is rejected as a hostname mismatch.
sudo docker run -it --rm --name signserver \
-p 80:8080 -p 443:8443 \
-v /home/user/Downloads/rootCA.pem:/mnt/external/secrets/tls/cas/rootCA.crt \
-h localhost \
keyfactor/signserver-ce

Sign an ESP32 application binary

Sends an unsigned image to a SignServer worker, verifies the returned signature locally, then appends it in the layout the bootloader expects.

import hashlib
import struct

import ecdsa
import requests

SIGNSERVER_URL = 'http://localhost/signserver/process'
UNSIGNED = 'OTABasicUnsigned.bin'
SIGNATURE = 'OTABasicSignaturServer.bin'
SIGNED = 'OTABasicSigned.bin'
PUBLIC_KEY = 'ESPSignerPub.pem'


def gen_image_signserver():
with open(UNSIGNED, 'rb') as raw_file:
binary_content = raw_file.read()

# Send the image to SignServer and keep the raw response
with open(UNSIGNED, 'rb') as upload:
files = {
'workerName': (None, 'PlainSigner'),
'file': upload,
'REQUEST_METADATA.SIGNATUREALGORITHM': (None, 'SHA256withECDSA'),
'REQUEST_METADATA.RESPONSE_ENCODED': (None, 'true'),
}
response = requests.post(SIGNSERVER_URL, files=files, timeout=30)
response.raise_for_status()

with open(SIGNATURE, 'wb') as sig_file:
sig_file.write(response.content)

# Verify before shipping: a bad signature must fail here, not on the device
with open(PUBLIC_KEY, 'r') as key_file:
vk = ecdsa.VerifyingKey.from_pem(key_file.read())

r, s = ecdsa.util.sigdecode_der(response.content, vk.pubkey.order)
sig_string = ecdsa.util.sigencode_string(r, s, vk.pubkey.order)
vk.verify(sig_string, binary_content, hashlib.sha256)

# Application image, 4-byte marker, then the raw (r, s) signature
with open(SIGNED, 'wb') as bin_file:
bin_file.write(binary_content)
bin_file.write(struct.pack('I', 0))
bin_file.write(sig_string)


if __name__ == '__main__':
gen_image_signserver()

Notes on the details that are easy to get wrong:

  • The algorithm name is SHA256withECDSA. A misspelling such as SH256withECDSA is rejected by the server, and the error can look like a worker configuration problem rather than a typo.
  • SignServer returns a DER-encoded signature; the ESP32 verification code expects the raw fixed-width r || s form. sigdecode_der followed by sigencode_string performs that conversion.
  • The trailing layout — image, four zero bytes, signature — has to match what the bootloader parses. Confirm it against your bootloader before flashing a device you cannot recover over UART.
  • struct.pack('I', 0) uses native size and alignment. Use '<I' to pin it to little-endian and exactly four bytes, independent of the build host.

SignServer and EJBCA

Both are open-source Keyfactor (formerly PrimeKey) products. They solve different halves of the same problem and are often deployed together.

SignServerEJBCA
RoleSigning and time-stamping serviceCertificate authority and PKI platform
Answers"Sign this artefact for me""Issue and manage this certificate"
Key materialSigning keys, used repeatedlyCA keys, plus the certificates it issues
Typical clientBuild pipeline, document workflowDevices, servers, users enrolling

SignServer

Purpose:

  • Code signing — sign software releases, firmware images and drivers so recipients can verify integrity and origin.
  • Document signing — PDF, XML and similar formats, proving the content has not been altered.
  • Time stamping — RFC 3161 tokens attesting that data existed at a given time. This is what keeps a signature verifiable after the signing certificate expires.

Characteristics:

  • Centralised control of signing keys and their certificates, with an audit log of every signing operation.
  • Multiple algorithms and output formats: PKCS#1, PKCS#7/CMS, XML signatures, PDF signatures.
  • REST and SOAP APIs, so it drops into existing build and document pipelines.

EJBCA

Purpose:

  • Certificate authority — issue and manage X.509 certificates.
  • Lifecycle management — enrolment, renewal, revocation, CRL and OCSP publication.
  • Authentication — certificates for TLS client authentication, S/MIME, device identity.

Characteristics:

  • Covers the full path from request through issuance to revocation.
  • Supports the enrolment protocols devices actually speak: SCEP, EST, CMP, ACME.
  • Scales to large certificate populations, and supports HSMs and detailed audit logging.

Using them together

The natural split is EJBCA issuing the signing certificate that SignServer then uses. EJBCA answers "is this key trusted, and is it still valid"; SignServer answers "apply that key to this artefact". Either works standalone — choose based on whether the problem is issuing identities or applying signatures.