Table of Contents
- Generate CA + intermediate + cert via cfssl
- One self signed certificate per user/device
- Single CA that signs certificates
- root CA
- intermediate CA
- config with profiles
- sign intermediate CA with root CA
- generate CA bundle
- generate certificate to sign OCSP
- generate client certificate (script wrapper)
- generate client certificate (manually)
- OCSP CRL
- Usage in caddy web server
Generate CA + intermediate + cert via cfssl
Alternative software, less complex: https://github.com/FiloSottile/mkcert
One self signed certificate per user/device
#!/usr/bin/env bash
set -euo pipefail
_user=$1
_device=$2
crtname="${_user}_${_device}"
if [[ -e "${crtname}.crt" ]]; then echo "${crtname}.crt already exists, existing" ; exit 1 ; fi
#step-cli certificate create "${crtname}@local" "${crtname}.crt" "${crtname}.key" --profile self-signed --subtle --insecure --no-password
# two years
step-cli certificate create "${crtname}@local" "${crtname}.crt" "${crtname}.key" --not-after=17520h --template selfsigned.tmpl.json --subtle --insecure --no-password
openssl pkcs12 -export -in "${crtname}.crt" -inkey "${crtname}.key" -out "${crtname}.p12"
# seems iphones need the -legacy switch... thanks to <https://github.com/paulgessinger/swift-paperless/issues/70#issuecomment-2308163983>
openssl pkcs12 -legacy -export -in "${crtname}.crt" -inkey "${crtname}.key" -out "${crtname}-for-iphone.p12"
exit 0
selfsigned.tmpl.json
:
{
"subject": {{ toJson .Subject }},
"sans": {{ toJson .SANs }},
"issuer": {{ toJson .Subject }},
"keyUsage": ["digitalSignature","keyEncipherment"],
"extKeyUsage": ["clientAuth"],
"basicConstraints": {
"isCA": false
}
}
Usage: ./gen-user-cert.sh useridentifier deviceidentifier
, e.g. ./gen-user-cert.sh sophie laptop
The trusted_ca_cert_file
directive then has to be added for each certificate (see the caddy config at the end).
Revocation works by removing the line.
Single CA that signs certificates
This does not allow revocation of certificates, if you want to revoke access to individual certificates, you need to follow above steps or work out how to validate certificates via e.g. https://caddyserver.com/docs/caddyfile/directives/tls#verifier, but it seems that feature is broken: https://github.com/caddyserver/caddy/issues/4518
Beware: This does not support OCSP / CRLs. Seems you need some database for this...
mkdir -p {root,intermediate}-ca
mkdir -p {ocsp,client}
root CA
root-ca/root-ca.json
:
{
"CN": "xxx root CA",
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "DE",
"L": "Ort",
"O": "xxx",
"OU": "root ca"
}
],
"ca": {
"expiry": "87600h"
}
}
cfssl gencert -initca root-ca/root-ca.json | cfssljson -bare root-ca/root-ca
intermediate CA
mkdir -p intermediate-ca
touch intermediate-ca/intermediate-ca.json
{
"CN": "xxx intermediate CA",
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "DE",
"L": "Ort",
"O": "xxx",
"OU": "intermediate ca"
}
],
"ca": {
"expiry": "87600h"
}
}
cfssl gencert -initca intermediate-ca/intermediate-ca.json | cfssljson -bare intermediate-ca/intermediate-ca
config with profiles
config.json
:
- intermediate
- valid for 1 year
- intermediate signed for 8 years
- intermediate may create + revoke certs
- is a CA
- may not issue child CAs
- client
- for client authentication
- valid for 1 year
{
"signing": {
"default": {
"expiry": "8760h"
},
"profiles": {
"intermediate": {
"usages": ["cert sign", "crl sign"],
"expiry": "70080h",
"ca_constraint": {
"is_ca": true,
"max_path_len": 1
}
},
"client": {
"usages": [
"signing",
"digital signature",
"key encipherment",
"client auth"
],
"expiry": "8760h"
},
"ocsp": {
"usages": ["digital signature", "ocsp signing"],
"expiry": "70080h"
}
}
}
}
sign intermediate CA with root CA
cfssl sign -ca root-ca/root-ca.pem -ca-key root-ca/root-ca-key.pem -config config.json -profile intermediate intermediate-ca/intermediate-ca.csr | cfssljson -bare intermediate-ca/intermediate-ca
generate CA bundle
generates cert-bundle.crt
:
cfssl-mkbundle root-ca/root-ca.pem intermediate-ca/intermediate-ca.pem
generate certificate to sign OCSP
NOT REQUIRED, requires e.g. an sqlite DB and also e.g. LetsEncrypt do not provide this anymore: https://letsencrypt.org/2024/07/23/replacing-ocsp-with-crls/
ocsp/ocsp.json
:
{
"CN": "ocsp.example.internal",
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "DE",
"ST": "NRW",
"L": "City",
"O": "Ahmed Example",
"OU": "ocsp cfssl demo"
}
]
}
cfssl gencert -ca intermediate-ca/intermediate-ca.pem -ca-key intermediate-ca/intermediate-ca-key.pem -config config.json -profile ocsp ocsp/ocsp.json | cfssljson -bare ocsp/ocsp
generate client certificate (script wrapper)
gen-client-cert.sh
:
#!/usr/bin/env bash
# example usage:
# gen-client-cert.sh khalila laptop
set -euo pipefail
_user=$1
_device=$2
_jsonfile="clients/${_user}-${_device}.json"
if [[ -e "${_jsonfile}" ]]; then echo "${_jsonfile} already exists, existing" ; exit 1 ; fi
sed "s,DEVICE,${_device}," clients/template.json > "${_jsonfile}"
sed -i "s,USER,${_user}," "${_jsonfile}"
cfssl gencert -ca intermediate-ca/intermediate-ca.pem -ca-key intermediate-ca/intermediate-ca-key.pem -config config.json -profile client "${_jsonfile}" | cfssljson -bare "clients/${_user}-${_device}"
openssl pkcs12 -export -in "clients/${_user}-${_device}.pem" -inkey "clients/${_user}-${_device}-key.pem" -out "clients/${_user}-${_device}.p12"
# seems iphones need the -legacy switch... thanks to <https://github.com/paulgessinger/swift-paperless/issues/70#issuecomment-2308163983>
openssl pkcs12 -legacy -export -in "clients/${_user}-${_device}.pem" -inkey "clients/${_user}-${_device}-key.pem" -out "clients/${_user}-${_device}-for-iphone.p12"
echo "created: $(ls clients/${_user}-${_device}*)"
Installation on iOS (iPhone) needs to happen via safari, you can not install a certificate via file browser. Also you need to install the CA as trusted (AFAIR) to be able to install the client certificate. You can spawn a http server e.g. via python3 -m http.server 9000
.
clients/template.json
:
{
"CN": "DEVICE@USER",
"hosts": ["DEVICE@USER"],
"key": {
"algo": "rsa",
"size": 4096
},
"names": [
{
"C": "DE",
"ST": "AYY",
"L": "City",
"O": "USER",
"OU": "DEVICE"
}
]
}
generate client certificate (manually)
clients/example.json
:
{
"CN": "client.example.internal",
"hosts": "client@exameple.internal",
"key": {
"algo": "rsa",
"size": 4096
},
"names": [
{
"C": "DE",
"ST": "NRW",
"L": "City",
"O": "Ahmed Example",
"OU": "client cfssl demo"
}
]
}
cfssl gencert -ca intermediate-ca/intermediate-ca.pem -ca-key intermediate-ca/intermediate-ca-key.pem -config config.json -profile client clients/example.json | cfssljson -bare clients/example
OCSP CRL
#cfssl bundle -cert intermediate-ca/intermediate-ca.pem -key intermediate-ca/intermediate-ca-key.pem -ca-bundle root-ca/root-ca.pem -int-bundle intermediate-ca/intermediate-ca.pem
Usage in caddy web server
# re-usable snippet
(mtls_conf) {
tls your@mailaddress.com.example {
on_demand
client_auth {
mode require_and_verify
trusted_ca_cert_file /etc/caddy/root-ca.pem
trusted_ca_cert_file /etc/caddy/intermediate-ca.pem
}
}
}
# usage with e.g. reverse proxy
domain.example {
import mtls_conf
reverse_proxy 192.168.1.1:80
}