Create Your Own Local Root CA With Yubikey Signing
Tags: yubikey, security, tutorial, ssl
Published: 2023-08-09 01:20:51

{%- # TOC start (generated with https://github.com/derlin/bitdowntoc) -%}

- [Major Security Warning](#major-security-warning)
- [Preparation](#preparation)
- [CA Folder Structure](#ca-folder-structure)
- [Root Certificate Generation](#root-certificate-generation)
- [Intermediate Certificate Generation](#intermediate-certificate-generation)
- [Yubikey Setup](#yubikey-setup)
- [Intermediate Signing Yubikey Import](#intermediate-signing-yubikey-import)
- [Root Trust Store](#root-trust-store)
- [OpenSSL pkcs11 Setup](#openssl-pkcs11-setup)
- [Example HTTPS Setup: Gitea](#example-https-setup-gitea)
- [Conclusion](#conclusion)

{%- # TOC end -%}

## Major Security Warning

Setting up your own certificate authority needs to be handled with extreme caution. If either the root or intermediate certificates are leaked, someone could impersonate the HTTPS certificate of any site you visit (like google/gmail/etc.). In the case you're uncertain about things, just accepting self signed certs for your local development servers and taking the padlock warning is probably a better idea.

**If you plan to setup a corporate security authority please seek professional consultation. This setup is too risky for that type of environment.**

## Preparation

The root certificate is best created external to the system. I recommend an encrypted USB drive. The one I ended up using was an Apricorn USB drive. It allows me to have a physical pin protect to keep the root cert safe. I also recommend one more USB key. With the setup I'll be showing having in encrypted isn't as much of an issue. For Yubikey you'll want one that supports PIV. The one I have is a YubiKey 5 NFC FIPS, but Yubikey 4 and 5 are supported. I also assume your Yubikey has touch support which supplements PIN protection. That way you'll need to know the pin if you manage physical access, and you'll need to have physical access if you manage to get the pin.

First I recommend doing a scan of the system using something like [Microsoft Safety Scanner](https://learn.microsoft.com/en-us/microsoft-365/security/intelligence/safety-scanner-download?view=o365-worldwide) for Windows or scanning tool of choice for other systems. Anything related to the root key or any time the USB drive that the root key is on is accessed, I make sure to unplug the Ethernet cable first. You can also setup a basic system with the proper Yubikey and openssl related software and handle most of the process in a more secure and offline environment.

I recommend having a browser tab open with this guide before going offline in a private/incognito window, then close out any other network related apps. Having another device with internet connectivity (other PC, smart phone, tablet, etc.) is good if you need to look something up without re-connecting to the internet.

## CA Folder Structure

This is the time to disconnect your Ethernet cable or turn off your Wifi. Some laptops even come with an Airplane mode to handle this easily. Plugin your encrypted USB key you plan to use for holding the root key. All commands should be run from the encrypted USB drive. 

```
$ mkdir rootCA
$ mkdir intermediateCA
$ mkdir rootCA/certs
$ mkdir intermediateCA/certs
$ mkdir rootCA/crl
$ mkdir intermediateCA/crl
$ mkdir rootCA/newcerts
$ mkdir intermediateCA/newcerts
$ mkdir rootCA/private
$ mkdir intermediateCA/private
$ mkdir rootCA/csr
$ mkdir intermediateCA/csr
```

Next are initial serial numbers used to identify certificates in the CA:

```
$ echo 1000 > rootCA/serial
$ echo 1000 > intermediateCA/serial
$ echo 0100 > rootCA/crlnumber
$ echo 0100 > intermediateCA/crlnumber
```

And now the database itself:

```
$ touch rootCA/index.txt
$ touch intermediateCA/index.txt
```
*Note*: `ni` can be used in place of touch in powershell

Now it's time to make some configuration files. These will store path information for our CAs so we don't have to enter it manually, as well as setup defaults for various certificate properties.

`openssl_root.cnf`
```
[ ca ]                                                   # The default CA section
default_ca = CA_default                                  # The default CA name

[ CA_default ]                                           # Default settings for the CA
dir               = ./rootCA                             # CA directory
certs             = $dir/certs                           # Certificates directory
crl_dir           = $dir/crl                             # CRL directory
new_certs_dir     = $dir/newcerts                        # New certificates directory
database          = $dir/index.txt                       # Certificate index file
serial            = $dir/serial                          # Serial number file
RANDFILE          = $dir/private/.rand                   # Random number file
private_key       = $dir/private/cakey.pem               # Root CA private key
certificate       = $dir/certs/ca.cert.pem               # Root CA certificate
crl               = $dir/crl/ca.crl.pem                  # Root CA CRL
crlnumber         = $dir/crlnumber                       # Root CA CRL number
crl_extensions    = crl_ext                              # CRL extensions
default_crl_days  = 30                                   # Default CRL validity days
default_md        = sha256                               # Default message digest
preserve          = no                                   # Preserve existing extensions
email_in_dn       = no                                   # Exclude email from the DN
name_opt          = ca_default                           # Formatting options for names
cert_opt          = ca_default                           # Certificate output options
policy            = policy_strict                        # Certificate policy
unique_subject    = no                                   # Allow multiple certs with the same DN

[ policy_strict ]                                        # Policy for stricter validation
countryName             = match                          # Must match the issuer's country
stateOrProvinceName     = match                          # Must match the issuer's state
organizationName        = match                          # Must match the issuer's organization
organizationalUnitName  = optional                       # Organizational unit is optional
commonName              = supplied                       # Must provide a common name
emailAddress            = optional                       # Email address is optional

[ req ]                                                  # Request settings
default_bits        = 2048                               # Default key size
distinguished_name  = req_distinguished_name             # Default DN template
string_mask         = utf8only                           # UTF-8 encoding
default_md          = sha256                             # Default message digest
prompt              = no                                 # Non-interactive mode

[ req_distinguished_name ]                               # Template for the DN in the CSR
countryName                     = US
stateOrProvinceName             = Texas
localityName                    = Dallas
0.organizationName              = cwprogram
organizationalUnitName          = cwprogram Cert Authorization
commonName                      = cwprogram Root Cert Authority
emailAddress                    = me@nospam.com

[ v3_ca ]                                           # Root CA certificate extensions
subjectKeyIdentifier = hash                         # Subject key identifier
authorityKeyIdentifier = keyid:always,issuer        # Authority key identifier
basicConstraints = critical, CA:true                # Basic constraints for a CA
keyUsage = critical, keyCertSign, cRLSign           # Key usage for a CA

[ crl_ext ]                                         # CRL extensions
authorityKeyIdentifier = keyid:always,issuer        # Authority key identifier

[ v3_intermediate_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
```

The use of 2048 bits along with sha256 is meant to be a balance between security and performance. Anything more complex and it can require additional processor power and may have compatibility issues. `req_distinguished_name` should be filled out with your appropriate information. 

`openssl_intermediate.cnf`
```
[ ca ]                           # The default CA section
default_ca = CA_default          # The default CA name

[ CA_default ]                                           # Default settings for the intermediate CA
dir               = ./intermediateCA                     # Intermediate CA directory
certs             = $dir/certs                           # Certificates directory
crl_dir           = $dir/crl                             # CRL directory
new_certs_dir     = $dir/newcerts                        # New certificates directory
database          = $dir/index.txt                       # Certificate index file
serial            = $dir/serial                          # Serial number file
RANDFILE          = $dir/private/.rand                   # Random number file
private_key       = $dir/private/intermediate.key.pem    # Intermediate CA private key
certificate       = $dir/certs/intermediate.cert.pem     # Intermediate CA certificate
crl               = $dir/crl/intermediate.crl.pem        # Intermediate CA CRL
crlnumber         = $dir/crlnumber                       # Intermediate CA CRL number
crl_extensions    = crl_ext                              # CRL extensions
default_crl_days  = 30                                   # Default CRL validity days
default_md        = sha256                               # Default message digest
preserve          = no                                   # Preserve existing extensions
email_in_dn       = no                                   # Exclude email from the DN
name_opt          = ca_default                           # Formatting options for names
cert_opt          = ca_default                           # Certificate output options
policy            = policy_loose                         # Certificate policy

[ policy_loose ]                                         # Policy for less strict validation
countryName             = optional                       # Country is optional
stateOrProvinceName     = optional                       # State or province is optional
localityName            = optional                       # Locality is optional
organizationName        = optional                       # Organization is optional
organizationalUnitName  = optional                       # Organizational unit is optional
commonName              = supplied                       # Must provide a common name
emailAddress            = optional                       # Email address is optional

[ req ]                                                  # Request settings
default_bits        = 2048                               # Default key size
distinguished_name  = req_distinguished_name             # Default DN template
string_mask         = utf8only                           # UTF-8 encoding
default_md          = sha256                             # Default message digest
x509_extensions     = v3_intermediate_ca                 # Extensions for intermediate CA certificate

[ req_distinguished_name ]                               # Template for the DN in the CSR
countryName                     = US
stateOrProvinceName             = Texas
localityName                    = Dallas
0.organizationName              = cwprogram
organizationalUnitName          = cwprogram Cert Authorization
commonName                      = cwprogram Intermediate Cert Authority
emailAddress                    = me@nospam.com

[ v3_intermediate_ca ]                                      # Intermediate CA certificate extensions
subjectKeyIdentifier = hash                                 # Subject key identifier
authorityKeyIdentifier = keyid:always,issuer                # Authority key identifier
basicConstraints = critical, CA:true, pathlen:0             # Basic constraints for a CA
keyUsage = critical, digitalSignature, cRLSign, keyCertSign # Key usage for a CA

[ crl_ext ]                                                 # CRL extensions
authorityKeyIdentifier=keyid:always                         # Authority key identifier

[ server_cert ]                                             # Server certificate extensions
basicConstraints = CA:FALSE                                 # Not a CA certificate
nsCertType = server                                         # Server certificate type
keyUsage = critical, digitalSignature, keyEncipherment      # Key usage for a server cert
extendedKeyUsage = serverAuth                               # Extended key usage for server authentication purposes (e.g., TLS/SSL servers).
authorityKeyIdentifier = keyid,issuer                       # Authority key identifier linking the certificate to the issuer's public key.
```

This is for the intermediate certificate authority. It will be used to handle most of the signing to reduce the probability of the root key getting compromised. Note that you want to make sure `commonName` is different for each of your certificates.

## Root Certificate Generation

Now it's time to start making the certificates. First we'll need a private key used for signing purposes as well as generating the root certificate:

```
$ openssl ecparam -genkey -name prime256v1 -out rootCA/private/cakey.pem
$ sudo chmod 400 rootCA/private/cakey.pem
```

If you're in Windows:

```
> icacls.exe cakey.pem /reset
> icacls.exe cakey.pem /grant:r "$($env:username):(r)"
> icacls.exe cakey.pem /inheritance:r
```

Is a reasonable `chmod 400` equivalent. Now it's time sign the certificate:

```
$ openssl req -config openssl_root.cnf -key ./rootCA/private/cakey.pem -new -x509 -days 7300 -sha256 -extensions v3_ca -out ./rootCA/certs/ca.cert.pem -subj "/C=US/ST=Texas/L=Dallas/O=cwprogram/OU=cwprogram Cert Authorization/CN=Root CA"
$ chmod 444 ./rootCA/certs/ca.cert.pem
```

This creates a self-signed certificate where the issuer of the certificate and the subject of the certificate are the same. It's what defines a certificate as root. You'll want a local copy of `./rootCA/certs/ca.cert.pem` somewhere so you can upload it to various trust stores when the time comes.

## Intermediate Certificate Generation

The intermediate certificate will handle the actual signing of certificates. First the key is generated:

```
$ openssl ecparam -genkey -name prime256v1 -out ./intermediateCA/private/intermediate.key.pem
$ sudo chmod 400 ~/myCA/intermediateCA/private/intermediate.key.pem
```

Next the certificate itself is generated similar to the root cert:

```
$ openssl req -config openssl_intermediate.cnf -key ./intermediateCA/private/intermediate.key.pem -new -sha256 -out ./intermediateCA/certs/intermediate.csr.pem -subj "/C=US/ST=Texas/L=Dallas/O=cwprogram/OU=cwprogram Cert Authorization/CN=Intermediate CA"
```

What's different in this case is we'll be using the root certificate to sign the intermediate one. This begins the overall certificate chain of trust:

```
$ openssl ca -config openssl_root.cnf -extensions v3_intermediate_ca -days 3650 -notext -md sha256 -in ./intermediateCA/certs/intermediate.csr.pem -out ./intermediateCA/certs/intermediate.cert.pem
$ openssl verify -CAfile ./rootCA/certs/ca.cert.pem ./intermediateCA/certs/intermediate.cert.pem
./intermediateCA/certs/intermediate.cert.pem: OK
```

This shows that the intermediate is properly linked to the root CA. We'll also want to create a certificate bundle linking the intermediate and root certificate together. While you should be fine in most cases it's good to have this around for compatibility reasons. It also prevents needing to access your root key later should a bundle become necessary. The actual process is simply combining the certs as so:

```
$ cat ./intermediateCA/certs/intermediate.cert.pem ./rootCA/certs/ca.cert.pem > ./intermediateCA/certs/ca-chain.cert.pem
```

`type` can be used in Powershell if you're on Windows:

```
> type ./intermediateCA/certs/intermediate.cert.pem,./rootCA/certs/ca.cert.pem > ./intermediateCA/certs/ca-chain.cert.pem
```

So now that we have the root CA requirement out of the way it's time to finalize putting the intermediate signing key into Yubikey.

## Yubikey Setup

To start out the `yubikey-manger` will need to be installed. An example on Ubuntu:

```
$ sudo apt-get install -y yubikey-manager
```

For Windows you'll need first obtain the [Yubikey Manager GUI](https://www.yubico.com/support/download/yubikey-manager/) which comes with the `ykman` CLI tool necessary. to add to path or create a profile entry [as documented](https://docs.yubico.com/software/yubikey/tools/ykman/Using_the_ykman_CLI.html#windows). Once everything is setup you'll want to setup pins:

```
$ ykman piv access change-pin
Enter the current PIN:
Enter the new PIN:
Repeat for confirmation:
New PIN set.
```

This is for the pin that will be used for general access. The factory default pin is `123456` when it asks for current PIN. Next is the PUK:

```
$ ykman piv access change-puk
Enter the current PUK:
Enter the new PUK:
Repeat for confirmation:
New PUK set.
```

This is used for the case that you forgot your pin as a backup of sorts. When it asks for current PUK the factory default is `12345678`. For the management key, I tend to use one that's derived from the pin like so:

```
$ ykman piv access change-management-key --generate --protect
Enter the current management key [blank to use default key]:
```

Note that you'll want to check the [Yubikey site ](https://developers.yubico.com/yubikey-piv-manager/PIN_and_Management_Key.html) to understand the implications and decide what step you want to take here.

## Intermediate Signing Yubikey Import

Now that the setup of Yubikey is done it's time to import the intermediate certificate and key into Yubikey:

```
$ ykman piv certificates import 9c ./intermediateCA/certs/intermediate.cert.pem
$ ykman piv keys import 9c --pin-policy ALWAYS --touch-policy CACHED  ./intermediateCA/private/intermediate.key.pem
```

The signing key is now protected by both pin and touch. A touch policy of cached means that you won't have to touch the Yubikey again if it's been touched within 15 seconds. Note that pin policy and touch policy can only be set at import. If you fail to do so and decide to change it later you'll have to essentially re-import the key again. Now that the intermediate key is somewhere safe, we can pull the intermediate CA to another device so the encrypted drive with the root key on it doesn't need to be accessed anymore:

1. **move** ./intermediateCA/private/intermediate.key.pem to the root of the encrypted drive. This is to have a backup in case something happens with the Yubikey and it needs to be re-imported.
2. Now put in the other USB drive 
3. Copy the entirety of `intermediateCA` over to it
4. Copy `./rootCA/certs/ca.cert.pem` over to it (this is just the certificate, not the signing key, and will be needed for setting up trust as a root certificate authority)
5. Copy `openssl_intermediate.cnf` over to it
6. Safely remove the encrypted drive with the root CA information on it through umount/eject
7. Store it somewhere safe

Once the encrypted USB drive with root CA information on it has been removed, it's safe to connect to your network again / move the USB drive with the intermediate CA on it to a network connected system. Please note that everything from here on out will be done using the intermediate CA USB drive.

## Root Trust Store

Next up is setting our root certificate as trusted. Without this we'll get warnings from browsers trying to validate certificates. Here are how some operating systems handle this:

- OSX: Root certificates are handled via [Keychain Access](https://support.apple.com/guide/keychain-access/add-certificates-to-a-keychain-kyca2431/mac). You'll also need to [modify the trust settings](https://support.apple.com/guide/keychain-access/change-the-trust-settings-of-a-certificate-kyca11871/mac) to Always Trust.
- Windows: Root certificates are handled via the [Windows Trust Store](https://learn.microsoft.com/en-us/skype-sdk/sdn/articles/installing-the-trusted-root-certificate)
- Linux: Root certificates are handled by some variation of the the [ca-certificates package](https://packages.debian.org/sid/ca-certificates). You'll need to check with your Linux distribution (Ubuntu as [an example](https://ubuntu.com/server/docs/security-trust-store)) for how to setup certs using this package 

Some other places you might have to chang include the Java Runtime with its own store for root certs:

```
$ keytool -import -trustcacerts -cacerts -alias 'cwprogram Root CA' -storepass changeit -file ca.cert.pem
```

For Windows, `git` will need to be changed to use the `schannel` SSL backend so it uses the Windows Trust Store to validate certificates:

```
> git config --global http.sslBackend schannel
```

Firefox will need to either have the certificate added to its own store or use the system's via `security.enterprise_roots.enabled` as [per their documentation](https://support.mozilla.org/en-US/kb/setting-certificate-authorities-firefox).

## OpenSSL pkcs11 Setup

Now we need to make some changes to OpenSSL so it works with Yubikey when doing signing. For this we'll need two packages:

```
$ sudo apt-get install -y opensc libengine-pkcs11-openssl
```

This installs [opensc](https://github.com/OpenSC/OpenSC/), a library for dealing with Smart Card (essentially what a Yubikey is recognized as) access in a programmatic way. It also installs OpenSSL bindings that interact using the [pkcs11 standard](https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=pkcs11). Basically, we won't get very far using a Yubikey for signing without this. The intermediate CA configuration will also need to be updated:

`openssl_intermediate.cnf`
```
openssl_conf = openssl_init

[openssl_init]
engines = engine_section

[engine_section]
pkcs11 = pkcs11_section

[pkcs11_section]
engine_id = pkcs11
init = 0

[ ca ]                           # The default CA section
default_ca = CA_default          # The default CA name

[ CA_default ]                                           # Default settings for the intermediate CA
dir               = ./intermediateCA                     # Intermediate CA directory
certs             = $dir/certs                           # Certificates directory
crl_dir           = $dir/crl                             # CRL directory
new_certs_dir     = $dir/newcerts                        # New certificates directory
database          = $dir/index.txt                       # Certificate index file
serial            = $dir/serial                          # Serial number file
RANDFILE          = $dir/private/.rand                   # Random number file
private_key       = pkcs11:manufacturer=piv_II;id=%02    # Intermediate CA private key
certificate       = $dir/certs/intermediate.cert.pem     # Intermediate CA certificate
crl               = $dir/crl/intermediate.crl.pem        # Intermediate CA CRL
crlnumber         = $dir/crlnumber                       # Intermediate CA CRL number
crl_extensions    = crl_ext                              # CRL extensions
default_crl_days  = 30                                   # Default CRL validity days
default_md        = sha256                               # Default message digest
preserve          = no                                   # Preserve existing extensions
email_in_dn       = no                                   # Exclude email from the DN
name_opt          = ca_default                           # Formatting options for names
cert_opt          = ca_default                           # Certificate output options
policy            = policy_loose                         # Certificate policy

[ policy_loose ]                                         # Policy for less strict validation
countryName             = optional                       # Country is optional
stateOrProvinceName     = optional                       # State or province is optional
localityName            = optional                       # Locality is optional
organizationName        = optional                       # Organization is optional
organizationalUnitName  = optional                       # Organizational unit is optional
commonName              = supplied                       # Must provide a common name
emailAddress            = optional                       # Email address is optional

[ req ]                                                  # Request settings
default_bits        = 2048                               # Default key size
distinguished_name  = req_distinguished_name             # Default DN template
string_mask         = utf8only                           # UTF-8 encoding
default_md          = sha256                             # Default message digest
x509_extensions     = v3_intermediate_ca                 # Extensions for intermediate CA certificate

[ req_distinguished_name ]                               # Template for the DN in the CSR
countryName                     = US
stateOrProvinceName             = Texas
localityName                    = Dallas
0.organizationName              = cwprogram
organizationalUnitName          = cwprogram Cert Authorization
commonName                      = cwprogram Intermediate Cert Authority
emailAddress                    = me@nospam.com

[ v3_intermediate_ca ]                                      # Intermediate CA certificate extensions
subjectKeyIdentifier = hash                                 # Subject key identifier
authorityKeyIdentifier = keyid:always,issuer                # Authority key identifier
basicConstraints = critical, CA:true, pathlen:0             # Basic constraints for a CA
keyUsage = critical, digitalSignature, cRLSign, keyCertSign # Key usage for a CA

[ crl_ext ]                                                 # CRL extensions
authorityKeyIdentifier=keyid:always                         # Authority key identifier

[ server_cert ]                                             # Server certificate extensions
basicConstraints = CA:FALSE                                 # Not a CA certificate
nsCertType = server                                         # Server certificate type
keyUsage = critical, digitalSignature, keyEncipherment      # Key usage for a server cert
extendedKeyUsage = serverAuth                               # Extended key usage for server authentication purposes (e.g., TLS/SSL servers).
authorityKeyIdentifier = keyid,issuer                       # Authority key identifier linking the certificate to the issuer's public key.
```

This will now setup OpenSSL to use the pkcs11 OpenSSL backend. For the `private_key` value it should be fine as is if you're private key is in 9c. Otherwise you'll need to use `pkcs11-tool` to find the proper ID:

```
$ pkcs11-tool --list-objects
Using slot 0 with a present token (0x0)
Public Key Object; EC  EC_POINT 256 bits
  label:      SIGN pubkey
  ID:         02
  Usage:      verify
  Access:     none
```

As mine is in 9c it shows 02 as the ID. If the ID ends up being different just put it after the `pkcs11:manufacturer=piv_II;id=%` part.

## Example HTTPS Setup: Gitea

To test if this works I'll show how to add HTTPS to a [Gitea install](https://dev.to/cwprogram/run-a-local-webui-git-interface-with-gitea-5eh3) I wrote about. The first thing we'll do is create a nice little config for the server certificate:

`gitea.cnf`
```
[req]
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no

[req_distinguished_name]
C = US
ST = Texas
L = Dallas
O = cwprogram
OU = cwprogram Cert Authorization
CN = gitea

[req_ext]
subjectAltName = @alt_names

[alt_names]
IP.1 = 172.18.139.193
DNS.1 = gitea
```

I recommend making a config like this customized for each cert you wish to deploy. The important thing to note is the subjectAltName or SAN. It used to be the common name was used for verifying the host. The modern way now is to use the SAN extension to provide host name info when the certificate is validated.

```
[alt_names]
IP.1 = 172.18.139.193
DNS.1 = gitea
```

For here I'm using `/etc/hosts` to bind a gitea hostname to the IP present. Technically it works fine as-is without the `IP.1` part. I just tend to like being specific as possible to keep things safe. If you're not sure of the IP or there's too many just ignore and use DNS instead. The items under `req_distinguised_name` (especially the CN value) will need to be changed if the hostname is not gitea and to match the values you've been using in the root and intermediate cert setup. Now create the private key for the gitea cert first:

```
$ openssl ecparam -genkey -name prime256v1 -out gitea_private.pem
$ sudo chmod 400 gitea_private.pem
```

Make the certificate request:

```
$ openssl req -config ./gitea.cnf -key gitea_private.pem -new -sha256 -out ./intermediateCA/csr/gitea.csr
```

Then the intermediate will sign it:

```
$ openssl ca -config ./openssl_intermediate.cnf -engine pkcs11 -keyform engine -extensions server_cert -extensions req_ext -extfile gitea.cnf -days 375 -notext -md sha256 -in ./intermediateCA/csr/gitea.csr -out ./gitea.cert.pem

Engine "pkcs11" set.
Using configuration from ./openssl_intermediate.cnf
Enter PKCS#11 token PIN for Intermediate CA:
Check that the request matches the signature
Signature ok
Certificate Details:
        Serial Number: 4098 (0x1002)
        Validity
            Not Before: Aug  6 14:59:52 2023 GMT
            Not After : Aug 15 14:59:52 2024 GMT
        Subject:
            countryName               = US
            stateOrProvinceName       = Texas
            localityName              = Dallas
            organizationName          = cwprogram
            organizationalUnitName    = cwprogram Cert Authorization
            commonName                = gitea
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                IP Address:172.18.139.193, DNS:gitea
Certificate is to be certified until Aug 15 14:59:52 2024 GMT (375 days)
Sign the certificate? [y/n]:y
Enter PKCS#11 key PIN for SIGN key:
```

The PIN is entered twice to validate against the intermediate cert stored, and then do the signing. If you have the touch policy enabled as per instructions you should notice a hang after you enter the PIN a second time to access the key. This is because it's waiting on you to touch authenticate with the Yubikey. Obtaining the private key from the Yubikey is handled by this:

```
-engine pkcs11 -keyform engine
```

This tells OpenSSL the value of `private_key` we set in the config comes from our Yubikey instead of a file. Also of importance:

```
-extensions req_ext -extfile gitea.cnf
```

As-is the signing will not pass over the absolutely essential SAN value. So the above forces it to be included with the values we had setup during the client signing request session. Now we need to have gitea recognize the certificate we have. Before copying things over gitea expects a cert bundle with the actual gitea cert and the intermediate cert included. This can be done via:

```
$ cat gitea.cert.pem ./intermediateCA/certs/intermediate.cert.pem > gitea.cert.bundle.pem
```

I'll make a folder to store the cert info:

```
$ sudo mkdir /home/git/certs
$ sudo cp gitea.cert.bundle.pem /home/git/certs/
$ sudo cp gitea_private.pem /home/git/certs/
$ chown -R git:git /home/git/certs
$ sudo chmod 400 /home/git/certs/gitea.cert.bundle.pem
$ sudo chmod 400 /home/git/certs/gitea_private.pem
```

The git user is what I have setup as gitea's service user (as per recommendation). Now the app configuration needs to be updated:

```
$ sudo su git -
$ vim /etc/gitea/app.ini
```

Now I change the server block to something like this:

```
[server]
SSH_DOMAIN = gitea
DOMAIN = gitea
PROTOCOL = https
CERT_FILE = /home/git/certs/gitea.cert.bundle.pem
KEY_FILE  = /home/git/certs/gitea_private.pem
HTTP_PORT = 3000
ROOT_URL = https://gitea:3000/
DISABLE_SSH = false
SSH_PORT = 22
LFS_START_SERVER = true
LFS_JWT_SECRET = [censored]
OFFLINE_MODE = false
```

Then simply restart gitea:

```
$ sudo systemctl restart gitea.service
$ sudo systemctl status gitea.service
● gitea.service - Gitea (Git with a cup of tea)
     Loaded: loaded (/etc/systemd/system/gitea.service; enabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-08-08 19:54:13 CDT; 5s ago
   Main PID: 33939 (gitea)
      Tasks: 16 (limit: 18997)
     Memory: 117.2M
     CGroup: /system.slice/gitea.service
             └─33939 /usr/local/bin/gitea web --config /etc/gitea/app.ini
```

Then check the have the wonderful secure padlock:

![Firefox showing site secure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fdaykepfycsej2y3fjfm.png)

While this is still somewhat of a warning about the source of the root cert (being from Windows store instead of Mozilla's store) the padlock we want is present. 

## Conclusion

This concludes a look at setting up a root certificate authority locally in a secure manner. Root CA information is stored on an encrypted USB key and the signing key is managed via Yubikey and a separate USB drive. I hope this helps those who really do want to get rid of the annoying cert warnings. Also I hope it means you don't have to deal with piecing together of 10 different sources to try to figure out how this all works!