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:TRUEis 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 asSH256withECDSAis 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 || sform.sigdecode_derfollowed bysigencode_stringperforms 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.
| SignServer | EJBCA | |
|---|---|---|
| Role | Signing and time-stamping service | Certificate authority and PKI platform |
| Answers | "Sign this artefact for me" | "Issue and manage this certificate" |
| Key material | Signing keys, used repeatedly | CA keys, plus the certificates it issues |
| Typical client | Build pipeline, document workflow | Devices, 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.