Skip to content

Instantly share code, notes, and snippets.

@thehowl
Last active July 17, 2023 20:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thehowl/2d57263fc0c700f75c743e14106715e4 to your computer and use it in GitHub Desktop.
Save thehowl/2d57263fc0c700f75c743e14106715e4 to your computer and use it in GitHub Desktop.
The following scripts can aid in the creation of JWKs and server certificates for SMART Health Cards.
#!/usr/bin/env bash
# Small script to create a certificate authority to be used for SMART Health Cards.
# The script accepts an argument, which is the name of your organisation (ie. "ACME Ltd")
# The script will generate a data subdirectory in $PWD, the relevant files for
# the implementation of SMART Health Cards are issuer.key8 and chain.
# SUBJECT_ALT_NAME should be adjusted accordingly.
set -e
set -u
set -o pipefail
# This script is inspired & modified starting from the guide here:
# https://jamielinux.com/docs/openssl-certificate-authority/create-the-root-pair.html
# Keep in mind that for SMART Health Cards we need to use Elliptic Curve keys using the P-256 curve.
mkdir -p data
cd data
mkdir -p certs crl newcerts private csr
chmod 700 private
touch index.txt
echo 1000 > serial
set_openssl_config() {
SUBJECT_ALT_NAME='https://example.com/shc-issuer'
# $1: ca or intermediate, depending on the keys to be used to sign.
# $2: common name to be used
cat <<EOF > openssl.cnf
# Root CA OpenSSL configuration.
# Adapted from https://jamielinux.com/docs/openssl-certificate-authority/_downloads/root-config.txt
[ ca ]
# 'man ca'
default_ca = CA_default
[ CA_default ]
# Directory and file locations.
dir = $PWD
certs = \$dir/certs
crl_dir = \$dir/crl
new_certs_dir = \$dir/newcerts
database = \$dir/index.txt
serial = \$dir/serial
RANDFILE = \$dir/private/.rand
# The root key and root certificate.
private_key = \$dir/private/$1.key.pem
certificate = \$dir/certs/$1.cert.pem
# For certificate revocation lists.
crlnumber = \$dir/crlnumber
crl = \$dir/crl/$1.crl.pem
crl_extensions = crl_ext
default_crl_days = 30
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
name_opt = ca_default
cert_opt = ca_default
default_days = 375
preserve = no
policy = policy_loose
[ policy_strict ]
# The root CA should only sign intermediate certificates that match.
# See the POLICY FORMAT section of 'man ca'.
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the 'ca' man page.
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
# Options for the 'req' tool ('man req').
default_bits = 2048
distinguished_name = req_distinguished_name
string_mask = utf8only
prompt = no
# SHA-1 is deprecated, so use SHA-2 instead.
default_md = sha256
# Extension to add when the -x509 option is used.
x509_extensions = v3_ca
[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
commonName = $2
[ v3_ca ]
# Extensions for a typical CA ('man x509v3_config').
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = CA:true
keyUsage = digitalSignature, cRLSign, keyCertSign
[ v3_intermediate_ca ]
# Extensions for a typical intermediate CA ('man x509v3_config').
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = CA:true, pathlen:0
keyUsage = digitalSignature, cRLSign, keyCertSign
[ usr_cert ]
# Extensions for client certificates ('man x509v3_config').
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection
[ server_cert ]
# Extensions for server certificates ('man x509v3_config').
basicConstraints = CA:FALSE
subjectAltName = URI:$SUBJECT_ALT_NAME
keyUsage = digitalSignature
[ crl_ext ]
# Extension for CRLs ('man x509v3_config').
authorityKeyIdentifier=keyid:always
[ ocsp ]
# Extension for OCSP signing certificates ('man ocsp').
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = digitalSignature
extendedKeyUsage = OCSPSigning
EOF
}
# ROOT
set_openssl_config ca "$1 Root Certificate Authority"
# Create Root CA private key
openssl ecparam -name P-256 -genkey -noout -out private/ca.key.pem
chmod 400 private/ca.key.pem
# X509 cert
openssl req -config openssl.cnf \
-key private/ca.key.pem \
-new -x509 -days 73000 -sha256 -extensions v3_ca \
-out certs/ca.cert.pem
# INTERMEDIATE
set_openssl_config ca "$1 Certificate Authority"
# Create Intermediate CA Private Key
openssl ecparam -name P-256 -genkey -noout -out private/intermediate.key.pem
chmod 400 private/intermediate.key.pem
# Intermediate Certificate Signing Request
openssl req -config openssl.cnf -new \
-key private/intermediate.key.pem \
-sha256 \
-out csr/intermediate.csr.pem
# Intermediate X509 Certificate
openssl ca -config openssl.cnf -extensions v3_intermediate_ca \
-days 73000 -notext -md sha256 \
-in csr/intermediate.csr.pem \
-out certs/intermediate.cert.pem
# ISSUER
set_openssl_config intermediate "$1"
# Create Issuer Private key
openssl ecparam -name P-256 -genkey -noout -out private/issuer.key.pem
chmod 400 private/issuer.key.pem
# Issuer Certificate Signing Request
openssl req -config openssl.cnf -new \
-key private/issuer.key.pem \
-sha256 \
-out csr/issuer.csr.pem
# Issuer X509 Certificate
openssl ca -config openssl.cnf -extensions server_cert \
-days 73000 -notext -md sha256 \
-in csr/issuer.csr.pem \
-out certs/issuer.cert.pem
# In main dir, as issuer.key8 will often be all we care about together with the chain.
openssl pkcs8 -topk8 -in private/issuer.key.pem -nocrypt -out issuer.key8
# Chain
cat certs/issuer.cert.pem certs/intermediate.cert.pem certs/ca.cert.pem > chain
echo "All generated"
// Command jwkgen generates a jwks.json file using the issuer key and the chain file
// created through generate-smart-certificate.sh.
package main
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"os"
"strings"
"github.com/lestrrat-go/jwx/jwk"
)
func main() {
if err := _main(); err != nil {
fmt.Fprintf(os.Stderr, "Fatal: %v\n", err)
os.Exit(1)
}
}
func _main() error {
data, err := os.ReadFile("../data/shc-certs/issuer.key8")
if err != nil {
return fmt.Errorf("error reading priv key: %q", err)
}
bl, _ := pem.Decode(data)
privKey, err := x509.ParsePKCS8PrivateKey(bl.Bytes)
if err != nil {
return fmt.Errorf("error passing priv key: %q", err)
}
k, err := jwk.New(privKey)
if err != nil {
return fmt.Errorf("error creating key: %w", err)
}
if err = jwk.AssignKeyID(k); err != nil {
return err
}
// convert certificate chain into array that will make jwk happy
chain, err := os.ReadFile("../data/shc-certs/chain")
if err != nil {
return fmt.Errorf("error reading csr: %q", err)
}
parts := strings.Split(string(chain), "\n")
isNew := false
for i := 0; i < len(parts); i++ {
part := parts[i]
if strings.HasPrefix(part, "----") || len(part) == 0 {
isNew = true
// remove this because it's pem and we don't care
parts = append(parts[:i], parts[i+1:]...)
i--
continue
}
if isNew {
isNew = false
continue
}
// append to earlier string and remove
parts[i-1] += part
parts = append(parts[:i], parts[i+1:]...)
i--
}
// modifications required by shc
k.Set(jwk.AlgorithmKey, "ES256")
k.Set(jwk.KeyUsageKey, "sig")
k.Set(jwk.X509CertChainKey, parts)
s := jwk.NewSet()
s.Add(k)
res, err := jwk.PublicSetOf(s)
if err != nil {
return err
}
out, err := json.Marshal(res)
if err != nil {
return err
}
os.Stdout.Write(out)
os.Stdout.Write([]byte{'\n'})
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment