Last active
July 17, 2023 20:41
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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