If you need a certificate for development purposes, you can generate one by yourself. This post shows how to do it step by step.
The following assumes Windows and openssl (which should come with Git Bash). The Wikipedia page on public key certificate contains valuable information about what we’ll be doing next so it’s definitely worth reading first. We’ll be creating a root certificate and an end-entity certificate, with the intention of using it for a website (HTTPS).
Root certificate (Certificate Authority)
First we need to generate the root certificate. We’ll use it soon to sign our
end-entity certificate. This is the relevant openssl req
command:
MSYS_NO_PATHCONV=1 openssl req -new -x509 -nodes -out rootCA.crt \
-newkey rsa:2048 -keyout rootCA.key -days 100 \
-subj "/C=NL/L=Amsterdam/emailAddress=nikolaos@acme-issuer.dev/CN=acme-issuer.dev/"
The MSYS_NO_PATHCONV=1
is needed only for Git Bash. Without it, the subj
argument /C=NL
is mistaken for a path and it gets prefixed with
C:\Program Files\Git
(see also Windows and Docker
paths).
Here are the parameters of the openssl req
command explained one by one:
-new
: New request-x509
: Output a x509 structure instead of a certificate request. A certificate request is a step in between that we don’t need, we just need the certificate here.-nodes
: Don’t encrypt the output key. If we don’t specify this, we’ll need to provide a passphrase for the key. Since this is just for development, we can skip it.-out rootCA.crt
: The filename where the certificate will be stored (rootCA.crt
)-newkey rsa:2048
the type of the new key and the bits-keyout rootCA.key
: The filename where the private key will be stored (rootCA.key
)-days 100
: How many days the certificate is valid for-subj value
: the subject of the certificate
The subject is specified in this format:
/key1=value1/key2=value2/.../keyN=valueN
. The keys that are used above are C
for country, L
for city, emailAddress
for, well, email address and finally
CN
for canonical name. The canonical name is important, especially for the
end-entity certificate we’ll be making soon. From
Wikipedia:
A client connecting to that server will perform the certification path validation algorithm:
- The subject of the certificate matches the hostname (i.e. domain name) to which the client is trying to connect;
- The certificate is signed by a trusted certificate authority.
The primary hostname (domain name of the website) is listed as the Common Name in the Subject field of the certificate.
We can import this in Windows with:
certutil -addstore -user "Root" rootCA.crt
This will install it only for the current user (-user
) in the trusted root
certification authorities section. This is our way of telling Windows to trust
any certificate signed by our root certificate.
To delete it, we can run:
certutil -delstore -user "Root" acme-issuer.dev
Notice that we imported the certificate by its filename but we delete it by its canonical name.
End-entity certificate
First, we create the certificate request:
MSYS_NO_PATHCONV=1 openssl req -new -nodes -out acme.csr \
-newkey rsa:2048 -keyout acme.key \
-subj "/C=NL/L=Amsterdam/emailAddress=nikolaos@acme.dev/CN=acme.dev/"
This command is the same as the one we run for the certificate authority, except
that it misses the -x509
argument. This will generate a certificate request
instead of a certificate. In the next command, we will use the certificate
request to generate a certificate signed by our certificate authority:
openssl x509 -req -in acme.csr \
-CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
-out acme.crt -days 100 -extfile v3.ext
The arguments of the openssl x509
command:
-req
: Input is a certificate request, sign and output-in acme.csr
: The input file-CA rootCA.crt
: The certificate of the authority-CAkey rootCA.key
: The key of the authority-CAcreateserial
: Create a serial file for the authority if it is missing-out acme.crt
: The certificate output file-days
: How many days the certificate is valid for-extfile file
: File with X509V3 extensions to add (more on this later)
We can import this in Windows with:
certutil -addstore -user "My" acme.crt
This will install it again only for the current user, but under the “personal” section.
We can delete it later with:
certutil -delstore -user "My" acme.dev
Extfile (X509V3 extensions)
In theory, we have a certificate that is signed by an authority that we told Windows to trust, so we should be good. If we try however to use this certificate in a web server, Chrome will complain that the certificate is not good enough:
This server could not prove that it is acme.dev; its security certificate does not specify Subject Alternative Names. This may be caused by a misconfiguration or an attacker intercepting your connection.
This is where the -extfile
parameter comes in. We need to provide an extra
configuration file that specifies the Subject Alternative Names that Chrome
complains about:
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = acme.dev
This reference documentation might be interesting.
With this in place, the certificate is good enough for Chrome and Edge. Firefox still complains, probably because we’re lacking an intermediate certificate, but that’s an exercise for another time.
One script to rule them all
This script combines all the steps together. Save it somewhere as
self-sign-cert.sh
and you can run it as:
self-sign-cert.sh www.hello.dev
: it will create and install the root and end certificate forwww.hello.dev
self-sign-cert.sh www.hello.dev delete
: it will uninstall the root and end certificate it installed before
#!/bin/bash
set -e
DOMAIN=$1
if [[ -z "$DOMAIN" ]]; then
echo "Please give the domain as the first parameter"
exit 1
fi
if [[ "$2" == "delete" ]]; then
certutil -delstore -user "My" $DOMAIN
certutil -delstore -user "Root" issuer-$DOMAIN
exit 0
fi
export MSYS_NO_PATHCONV=1
ROOT_CRT="$DOMAIN-root.crt"
ROOT_KEY="$DOMAIN-root.key"
openssl req -x509 -new -nodes -out $ROOT_CRT \
-newkey rsa:2048 -keyout $ROOT_KEY \
-days 100 \
-subj "/C=NL/L=Amsterdam/emailAddress=nikolaos@issuer-$DOMAIN/CN=issuer-$DOMAIN/"
certutil -addstore -user "Root" $ROOT_CRT
ACME_CSR="$DOMAIN.csr"
ACME_KEY="$DOMAIN.key"
ACME_CRT="$DOMAIN.crt"
ACME_V3_EXT="$DOMAIN.v3.ext"
tee $ACME_V3_EXT <<-HERE >/dev/null
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = $DOMAIN
HERE
openssl req -new -nodes -out $ACME_CSR \
-newkey rsa:2048 -keyout $ACME_KEY \
-subj "/C=NL/L=Amsterdam/emailAddress=nikolaos@$DOMAIN/CN=$DOMAIN/"
openssl x509 -req -in $ACME_CSR \
-CA $ROOT_CRT -CAkey $ROOT_KEY -CAcreateserial \
-out $ACME_CRT -days 100 -extfile $ACME_V3_EXT
certutil -addstore -user "My" $ACME_CRT
As always, it works on my machine.