Pages

Wednesday, 8 May 2024

Create your own certificate authority

 

This is an adaptation of my previous articles How to create Peer TLS Certificates and How to create and test a CSR for HTTPS.

For super quick testing with https, you may be interested in the localhost.daplie.com certificates or serve-https.

WARNING: Again, don't ever try to use a truly self-signed certificate (Root CA) directly, even in testing. Always use a certificate which has been signed by your self-created Root CA.

Why? Because Self-Signed Certificates are a LIE!!

A "self-signed" certificate is otherwise known as a "Root Certificate Authority". Many browsers and tools will reject these not just with warnings, but with actual errors.

A proper "self-signed" certificate would be one that you sign with a Root CA that you create - that's what we'll be doing here.

WARNING: Some clients have different underlying behaviors when instructed to use invalid certificates. For example curl on OS X does not send SNI when using the --insecure (-k) option. But if you have your own Root CA and supply it, curl behaves.

Scope

We'll discuss how to create certificates for example.com domains.

First you'll need sudo vim /etc/hosts to add a few domains.

/etc/hosts:

127.0.0.1 example.com

127.0.0.1 foo.example.com
127.0.0.1 bar.example.com
127.0.0.1 baz.example.com

127.0.0.1 ssh.example.com
127.0.0.1 vpn.example.com

127.0.0.1 localhost.example.com

Create a Certificate Authority

You need to create an authority for yourself. This is a true "self-signed certificate" or, in other words, a Root Certificate Authority (Root CA).

(Yes, I know I'm repeating myself about the Root CA vs "self-signed" bit. Hopefully it'll get through to you.)

create-root-ca.sh:

#!/bin/bash

# make directories to work from
mkdir -p certs/ca

# Create your very own Root Certificate Authority
openssl genrsa \
  -out certs/ca/my-root-ca.key.pem \
  2048

# Self-sign your Root Certificate Authority
# Since this is private, the details can be as bogus as you like
openssl req \
  -x509 \
  -new \
  -nodes \
  -key certs/ca/my-root-ca.key.pem \
  -days 9131 \
  -out certs/ca/my-root-ca.crt.pem \
  -subj "/C=US/ST=Utah/L=Provo/O=ACME Signing Authority Inc/CN=example.net"

Create *.example.com Private Keys and CSRs

In this step we're going to generate the private key, which will be tied to the server's identity by way of the certificate.

create-key-and-csr.sh:

#!/bin/bash

# Change to be whatever
FQDN="$1"

# make directories to work from
mkdir -p certs/{servers,tmp}

# Create Certificate for this domain,
mkdir -p "certs/servers/${FQDN}"
openssl genrsa \
  -out "certs/servers/${FQDN}/privkey.pem" \
  2048

# Create the CSR
openssl req -new \
  -key "certs/servers/${FQDN}/privkey.pem" \
  -out "certs/tmp/${FQDN}.csr.pem" \
  -subj "/C=US/ST=Utah/L=Provo/O=ACME Service/CN=${FQDN}"

You would use this script like so:

bash create-key-and-csr.sh foo.example.com

Sign the Certificate

Since we're doing development / testing stuff, I won't blame you if you do cheat a little but, technically, you should be creating each private key on the server which uses it, transferring just the CSR over to the server which is signing it, and then transfer the server certificate as well as the signing certificates (not keys) back with it.

sign-certificate-request.sh:

#!/bin/bash

FQDN="$1"

# Sign the request from Server with your Root CA
openssl x509 \
  -req -in certs/tmp/${FQDN}.csr.pem \
  -CA certs/ca/my-root-ca.crt.pem \
  -CAkey certs/ca/my-root-ca.key.pem \
  -CAcreateserial \
  -out certs/servers/${FQDN}/cert.pem \
  -days 9131

# If you already have a serial file, you would use that (in place of CAcreateserial)
# -CAserial certs/ca/my-root-ca.srl

You would use this script like so:

bash sign-certificate-request.sh foo.example.com

Create Bundles

Some web servers are happy to have your private key separate from your certificate and the chain responsible for it. Others like them to be bundled in a certain way.

create-bundles.sh:

#!/bin/bash

FQDN="$1"

echo ""

echo "PRIVATE server bundle: certs/servers/${FQDN}/server.pem"
echo "(keep it secret, keep it safe - just like privkey.pem)"
echo ""
echo ""
cat \
  "certs/servers/${FQDN}/privkey.pem" \
  "certs/servers/${FQDN}/cert.pem" \
  > "certs/servers/${FQDN}/server.pem"


echo "chain: certs/servers/${FQDN}/chain.pem"
echo "(contains Intermediates and Root CA in least-authoritative first manner)"
echo ""
echo ""
# if there were an intermediate, it would be concatonated before the Root CA
cat \
  "certs/ca/my-root-ca.crt.pem" \
  > "certs/servers/${FQDN}/chain.pem"


# TODO
#
# The Convention for Full Chain is one of these:
#   root + intermediates + cert
#   root + intermediates
#   intermediates + cert
#
# ... but I don't remember which
# I may be wrong about chain as well...

echo "fullchain: certs/servers/${FQDN}/fullchain.pem"
echo "(contains Server CERT, Intermediates and Root CA)"
echo ""
echo ""
cat \
  "certs/servers/${FQDN}/cert.pem" \
  "certs/ca/my-root-ca.crt.pem" \
  > "certs/servers/${FQDN}/fullchain.pem"

echo "All Done"

You would use this script like so:

bash create-bundles.sh foo.example.com

How to use with servers

haproxy

I don't think it matters how you concatonate them, but haproxy needs your intermeditate certs, server cert, and server key.

It should NOT contain the Root CA (it'll get confused by it)

frontend foo_ft
    mode http
    bind 0.0.0.0:80
    bind 0.0.0.0:443 ssl crt chain.pem crt server.pem

    use_backend foo_bk

backend foo_bk
    mode http

    server 127.0.0.1:8080

node.js

See the example below in the client section; the server takes the same options as the client.

How to use with clients

cURL

Instead of using --insecure (-k) you can now specify --cacert or --capath.

curl https://foo.example.com/api/endpoint --cacert certs/servers/foo.example.com/chain.pem

You cannot use a Root CA as a self-signed cert with curl's --cecert option. If you try it'll throw errors because there will be no chain to follow. It won't be able to validate the leaf node.

If you were instead to use --capath, it would need to point to a directory that had files such as

  • intermediate.crt.pem
  • my-root-ca.crt.pem
curl https://foo.example.com/api/endpoint --capath certs/ca

node.js

'use strict';

var request = require('request');

var agentOptions = {
  host: 'foo.example.com'
, port: '443'
, path: '/'
, rejectUnauthorized: true
  // note, you cannot use chain.pem, you must specify files individually
, ca: [
    fs.readFileSync('certs/ca/my-root-ca.pem')
  //, fs.readFileSync('certs/ca/intermediate.pem')
  ]
};

var agent = new https.Agent(agentOptions);

request({
  url: "https://foo.example.com/api/endpoint"
, method: 'GET'
, agent: agent
}, function (err, resp, body) {
  // ...
});

What about intermediates?

When you purchase a certificate for $10 at name.com you can't turn around and use it to sign other certificates for yourself... but why not?

They're doing it, right? They are issuing from an intermediate, not a root cert.

Obviously these intermediates have been signed in such a way that they are allowed to sign again themselves. I don't really understand that process, but you can learn more about it from someone who does:

See https://jamielinux.com/docs/openssl-certificate-authority/create-the-intermediate-pair.html

from https://web.archive.org/web/20160603021144/https://coolaj86.com/articles/create-your-own-certificate-authority-for-testing/

No comments:

Post a Comment