Info
Ce billet est écrit dans le contexte défini dans le premier post de ma série sur l’auto-hébergement d’un PKI. Voir la liste plus bas.

Introduction Link to heading

Dans ce post j’initialiserai localement des CA racine et intermédiaire en utilisant cfssl ainsi que les bases de données nécessaires à la sauvegarde de l’état des PKI, qui seront aussi utilisées plus tard par les APIs.

La clef privée de la CA racine devrait rester hors ligne alors que nous utiliserons la CA intermédiaire pour les signatures classiques de certificats. Ainsi si la CA intermédiaire était compromise, il serait possible de la révoquer et d’en réinitialiser une autre rapidement. Ce montage permettrait aussi de créer différentes CA intermédiaires qui pourraient être distribuées entre différentes entités d’une organisation afin de permettre à chacune de celles-ci de signer à leur tour des certificats. Si l’une d’entre elles venait à être compromise, la révoquer n’empêcherait pas les autres CA intermédiaires de fonctionner.

cfssl, le toolkit que j’ai utilisé, est un outil de gestion de PKI créé par CloudFlare et écrit en Go. J’ai trouvé peu pratique son approche minimaliste de la documentation, mais heureusement, le code source de l’application était suffisamment clair pour qu’en gardant le nez dedans, l’utilisation de l’outil reste facile.

Au moment où j’écris cet article, la dernière version de cfssl est la v1.6.4. Je n’écrirai pas ici à propos de l’installation de cfssl que vous retrouverez probablement dans les dépôts de votre distribution linux ou que vous pourrez compiler depuis ses sources facilement.

Configuration Link to heading

Base de données Link to heading

Comme dit plus haut, cfssl peut sauvegarder son état dans une base de données. Trois types de bases sont supportés : mysql, pgsql et SQLite. J’utiliserai dans cet article des bases SQLite par facilité, mais l’une des deux autres devrait plutôt être utilisée en environnement de production.

L’utilisation d’une base de donnée permet l’utilisation des commandes liées à OCSP (Online Certificate Status Protocol) et tous les certificats signés par la CA seront aussi stockés en base.

Le fichier cloudflare/cfssl/certdb/README.md informe que l’initialisation des bases de données se fait avec un outil appelé goose. Ce projet n’a pas été mis à jour depuis 2015 et plusieurs forks en ont été faits. Le fork principal, sur GitHub, n’est pas compatible avec les scripts fournis dans le dépôt de cfssl.

Vous devrez donc, pour initialiser la base, soit continuer avec liamstask’s goose ( PKGBUILD) ou le lâcher complètement. Pour cet article, j’ai choisi de ne pas l’utiliser après pourtant l’avoir utilisé lors de mes premiers tests avec cfssl.

La configuration utilisée pour initialiser la base peut être trouvée dans cloudflare/cfssl/certdb/sqlite.

J’ai arrangé les deux scripts SQL en un seul appelé ici db/definition.sql:

CREATE TABLE certificates (
  serial_number            blob NOT NULL,
  authority_key_identifier blob NOT NULL,
  ca_label                 blob,
  status                   blob NOT NULL,
  reason                   int,
  expiry                   timestamp,
  revoked_at               timestamp,
  pem                      blob NOT NULL,
  issued_at                timestamp,
  not_before               timestamp,
  metadata                 text,
  sans                     text,
  common_name              text,
  PRIMARY KEY(serial_number, authority_key_identifier)
);

CREATE TABLE ocsp_responses (
  serial_number            blob NOT NULL,
  authority_key_identifier blob NOT NULL,
  body                     blob NOT NULL,
  expiry                   timestamp,
  PRIMARY KEY(serial_number, authority_key_identifier),
  FOREIGN KEY(serial_number, authority_key_identifier) REFERENCES certificates(serial_number, authority_key_identifier)
);

Créer les bases nécessaires pour chaque CA est l’affaire de deux petites commandes :

sqlite3 db/root-certstore.db < db/definition.sql
sqlite3 db/intermediate-certstore.db < db/definition.sql

cfssl Link to heading

Accès à la base de données Link to heading

cfssl a besoin d’un peut de configuration pour fonctionner.

Pour commencer, je dois créer les fichiers de configuration lui indiquant comment accéder aux bases. Il s’agit de deux fichiers JSON très simples :

mkdir -p {root,intermediate}/config
cat <<EOF > root/config/db.json
{
    "driver": "sqlite3",
    "data_source": "db/root-certstore.db"
}
EOF
cat <<EOF > intermediate/config/db.json
{
    "driver": "sqlite3",
    "data_source": "db/intermediate-certstore.db"
}
EOF

Profils de signature Link to heading

Viennent ensuite les profils que mes CA vont utiliser pour signer des certificats. Il y a quelques paramètres que je dois définir maintenant même s’ils ne seront vraiment utiles que lorsque les CA seront finalement hébergées :

  • À quelle adresse je pourrai récupérer mes CA ;
  • Où leurs CRL seront distribuées ;
  • Quelles seront les adresses auxquelles les répondeurs OCSP seront servis.

J’ai choisi de service le PKI sous le domaine pki.valhall.local. Les URI que j’utiliserai seront :

  • http://pki.valhall.local/root/ca
  • http://pki.valhall.local/root/crl
  • http://pki.valhall.local/root/ocsp
  • http://pki.valhall.local/intermediate/ca
  • http://pki.valhall.local/intermediate/crl
  • http://pki.valhall.local/intermediate/ocsp

Ces informations seront plus tard disponibles comme extensions dans les certificats. Par exemple, si on prend le certificat de google.com :

$ echo | \
  openssl s_client \
    -showcerts \
    -servername google.com \
    -connect google.com:443 2>/dev/null | \
  openssl x509 \
    -inform pem \
    -noout -text

# Certificate:
#     Data:
#         ...
#         X509v3 extensions:
#             ...
#             Authority Information Access:
#                 OCSP - URI:http://ocsp.pki.goog/gts1c3
#                 CA Issuers - URI:http://pki.goog/repo/certs/gts1c3.der
#             X509v3 CRL Distribution Points:
#                 Full Name:
#                   URI:http://crls.pki.goog/gts1c3/moVDfISia2k.crl

Ces extensions font parties du système de vérification de la validité des certificats.

Les profils de signature sont donc définis dans un fichier JSON. Ils permettent de renseigner des paramètres par défaut pour certains types de certificats. Par exemple, la CA racine qui ne signera que des certificats intermédiaires utilisera la configuration suivante :

{
  "signing": {
    "default": {
      "crl_url": "http://pki.valhall.local/root/crl",
      "ocsp_url": "http://pki.valhall.local/root/ocsp",
      "issuer_urls": [
        "http://pki.valhall.local/root/ca"
      ],
      "expiry": "8760h"
    },
    "profiles": {
      "intermediate": {
        "usages": [
          "signing",
          "digital signature",
          "key encipherment",
          "cert sign",
          "crl sign",
          "server auth",
          "client auth"
        ],
        "ca_constraint": {
          "is_ca": true,
          "max_path_len": 0,
          "max_path_len_zero": true
        },
        "expiry": "87600h"
      },
      "ocsp": {
        "usages": [
          "digital signature",
          "ocsp signing"
        ],
        "expiry": "26280h"
      }
    }
  }
}

Dans la configuration ci-dessus, j’ai défini deux profils : intermediate qui sera utilisé pour signer d’autres CA et ocsp qui sera utilisé pour signer le certificat utilisé pour signer l’OCSP. Le bloc .signing.default est utilisé pour renseigner des paramètres partagés entre les profils.

La CA intermédiaire sera utilisée principalement pour signer des certificats à destination de serveurs ou pour de l’authentification client. Étant donné que cette CA sera plus tard utilisée dans un mécanisme de renouvellement automatique, à l’image des CA de Let’s Encrypt, les certificats serveurs pourraient avec une période de validité courte :

{
  "signing": {
    "default": {
      "crl_url": "http://pki.valhall.local/intermediate/crl",
      "ocsp_url": "http://pki.valhall.local/intermediate/ocsp",
      "issuer_urls": [
        "http://pki.valhall.local/intermediate/ca"
      ],
      "expiry": "8760h"
    },
    "profiles": {
      "client": {
        "usages": [
          "signing",
          "digital signing",
          "key encipherment",
          "client auth"
        ],
        "expiry": "8760h"
      },
      "server": {
        "usages": [
          "signing",
          "digital signing",
          "key encipherment",
          "server auth"
        ],
        "expiry": "2190h"
      },
      "ocsp": {
        "usages": [
          "digital signature",
          "ocsp signing"
        ],
        "expiry": "26280h"
      }
    }
  }
}

Ces profils seront sauvegardés respectivement sous root/config/profiles.json et intermediate/config/profiles.json.

Définition des CA Link to heading

La structure de l’objet Certificat attendu par cfssl est définie dans cloudflare/cfssl/csr/csr.go#L138.

Les deux définitions que j’utiliserai, respectivement root/config/init.json et intermediate/config/init.json sont :

{
  "CN": "Valhall Root CA Certificate",
  "CA": {
    "expiry": "87600h"
  },
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [{
    "C":  "FR",
    "ST": "Pays de la Loire",
    "L":  "Nantes",
    "O":  "Valhall"
  }]
}
{
  "CN": "Valhall Intermediate CA Certificate",
  "CA": {
    "expiry": "87600h"
  },
  "key": {
    "algo": "rsa",
    "size": 4096
  },
  "names": [{
    "C":  "FR",
    "ST": "Pays de la Loire",
    "L":  "Nantes",
    "O":  "Valhall"
  }]
}

La création des clefs privées et des certificats est ensuite plutôt facile. La commande genkey créée une clef privée, un CSR puis l’auto-signe.

cfssl genkey -initca root/config/init.json | cfssljson -bare root/ca

cfssl genkey retourne un JSON comprenant trois paramètres : cert, csr et key. cfssljson, un autre exécutable faisant partie du toolkit pourra créer les fichiers automatiquement depuis ce JSON.

La création de la CA intermédiaire suit le même processus. Il faudra par contre supprimer le certificat généré et réutiliser le CSR pour la faire signer par la CA racine :

cfssl genkey -initca intermediate/config/init.json | cfssljson -bare intermediate/ca
rm intermediate/ca.pem
cfssl sign \
    -ca root/ca.pem \
    -ca-key root/ca-key.pem \
    -config root/config/profiles.json \
    -profile intermediate \
    -db-config root/config/db.json \
    intermediate/ca.csr | cfssljson -bare intermediate/ca

La structure du dossier courant devrait maintenant ressembler à ça :

tree

# .
# ├── db
# │   ├── definition.sql
# │   ├── intermediate-certstore.db
# │   └── root-certstore.db
# ├── intermediate
# │   ├── ca.csr
# │   ├── ca-key.pem
# │   ├── ca.pem
# │   └── config
# │       ├── db.json
# │       ├── init.json
# │       └── profiles.json
# └── root
#     ├── ca.csr
#     ├── ca-key.pem
#     ├── ca.pem
#     └── config
#         ├── db.json
#         ├── init.json
#         └── profiles.json
#
# 6 directories, 15 files

Et une requête SQL dans la base utilisée par la CA racine permet de voir que le certificat de la CA intermédiaire y a bien été stocké.

$ sqlite3 db/root-certstore.db "SELECT common_name FROM certificates;"
Valhall Intermediate CA Certificate

Clef et certificat pour OCSP Link to heading

Il ne reste plus qu’à générer les certificats et clefs pour les répondeurs OCSP de chaque CA et à générer les CRL.

cat <<EOF > root/config/ocsp.json
{
  "CN": "Valhall Root OCSP Certificate",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [{
    "C":  "FR",
    "ST": "Pays de la Loire",
    "L":  "Nantes",
    "O":  "Valhall"
  }]
}
EOF
cfssl gencert \
    -ca root/ca.pem \
    -ca-key root/ca-key.pem \
    -config root/config/profiles.json \
    -profile ocsp \
    -db-config root/config/db.json \
    root/config/ocsp.json | cfssljson -bare root/ocsp
cat <<EOF > intermediate/config/ocsp.json
{
  "CN": "Valhall Intermediate OCSP Certificate",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [{
    "C":  "FR",
    "ST": "Pays de la Loire",
    "L":  "Nantes",
    "O":  "Valhall"
  }]
}
EOF
cfssl gencert \
    -ca intermediate/ca.pem \
    -ca-key intermediate/ca-key.pem \
    -config intermediate/config/profiles.json \
    -profile ocsp \
    -db-config intermediate/config/db.json \
    intermediate/config/ocsp.json | cfssljson -bare intermediate/ocsp

CRL Link to heading

Les CRL (Certificate Revocation Lists) sont des fichiers qui devraient (probablement ?) être mis à jour régulièrement. Je ne suis pas certain de savoir comment les clients SSL implémente le mécanisme de cache pour cette fonctionnalité étant donné que les CRL comportent deux dates : la date de la dernière mise à jour et la date de la prochaine mise à jour. Ce qui laisse supposer que si le client décide de cacher le CRL jusqu’à la prochaine mise à jour, ils pourraient manquer pendant cette période des mises à jour concernant la révocation de certificats, sujet critique. Les CRL sont aussi signées avec la clef privée de la CA, ce qui peut être gênant quand on souhaite garder la clef privée de la CA racine hors ligne. J’ai pû rencontrer différentes approches à ce sujet : CRL avec une date d’expiration similaire à celle de la CA et mise à jour seulement si une CA intermédiaire est compromise ou encore CRL hebdomadaires, ce qui est d’ailleurs le choix par défaut pour cfssl :

$ cfssl crl -h

# ...
#   -expiry=168h0m0s: time from now after which the CRL will expire (default: one week)

J’ai choisi de correspondre à ce deuxième cas et comme je ne peux pas imaginer générer des CRL toutes les semaines sans automatiser le processus, la clef privée de ma CA racine sera stockée dans un coffre-fort et je m’arrangerai pour que la tâche de mise à jour aille la récupérer, ce qui est un risque acceptable pour mon home-lab.

cfssl crl retourne un CRL au format PEM sans header/footer ni retours à la ligne, il faudra donc les ajouter :

echo "-----BEGIN X509 CRL-----" > root/crl.pem
cfssl crl \
    -ca root/ca.pem \
    -ca-key root/ca-key.pem \
    -db-config root/config/db.json | fold -w 64 >> root/crl.pem
echo "-----END X509 CRL-----" >> root/crl.pem
echo "-----BEGIN X509 CRL-----" > intermediate/crl.pem
cfssl crl \
    -ca intermediate/ca.pem \
    -ca-key intermediate/ca-key.pem \
    -db-config intermediate/config/db.json | fold -w 64 >> intermediate/crl.pem
echo "-----END X509 CRL-----" >> intermediate/crl.pem

La CRL peut être lue avec openssl crl :

openssl crl -inform PEM -text -noout -in root/crl.pem
# Certificate Revocation List (CRL):
#         Version 2 (0x1)
#         Signature Algorithm: sha256WithRSAEncryption
#         Issuer: C = FR, ST = Ile de France, L = Nantes, O = Valhall, CN = Valhall Root CA Certificate
#         Last Update: Jun 17 08:39:31 2023 GMT
#         Next Update: Jun 24 08:39:31 2023 GMT
#         CRL extensions:
#             X509v3 Authority Key Identifier:
#                 D0:33:EF:44:95:BD:B2:0B:61:6D:B8:E0:19:95:6D:80:90:AA:3F:A6
# No Revoked Certificates.
#     Signature Algorithm: sha256WithRSAEncryption
#     Signature Value:
#         7b:91:89:00:41:d4:80:72:0b:af:db:7d:e5:19:cd:d0:29:3b:
#         ...

Validation Link to heading

Tous les prérequis pour pouvoir signer, révoquer, etc. des certificats devraient être là. Il n’y a plus qu’à démarrer l’api pour tester.

cfssl serve \
      -ca=intermediate/ca.pem \
      -ca-key=intermediate/ca-key.pem \
      -responder=intermediate/ocsp.pem \
      -responder-key=intermediate/ocsp-key.pem \
      -db-config=intermediate/config/db.json \
      -config=intermediate/config/profiles.json
# 2023/06/17 10:55:12 [INFO] Initializing signer
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/newcert' is enabled
# 2023/06/17 10:55:12 [INFO] setting up key / CSR generator
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/newkey' is enabled
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/ocspsign' is enabled
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/info' is enabled
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/gencrl' is enabled
# 2023/06/17 10:55:12 [INFO] bundler API ready
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/bundle' is enabled
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/scan' is enabled
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/revoke' is enabled
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/certadd' is enabled
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/sign' is enabled
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/init_ca' is enabled
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/scaninfo' is enabled
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/certinfo' is enabled
# 2023/06/17 10:55:12 [INFO] endpoint '/' is enabled
# 2023/06/17 10:55:12 [WARNING] endpoint 'authsign' is disabled: {"code":5200,"message":"Invalid or unknown policy"}
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/crl' is enabled
# 2023/06/17 10:55:12 [INFO] endpoint '/api/v1/cfssl/health' is enabled
# 2023/06/17 10:55:12 [INFO] Handler set up complete.
# 2023/06/17 10:55:12 [INFO] Now listening on 127.0.0.1:8888

Vous remarquerez un avertissement à propos de l’API authsign qui est désactivée : j’aborderai ce sujet dans l’article sur l’hébergement de l’API dans kubernetes. Pour faire simple, c’est désactivé parce que le fichier de configuration renseignant les profils ne contient pas d’information sur des méthodes d’authentification. Si vous souhaitez servir l’API en l’état, faites bien attention à qui pourrait y avoir accès. Parmi Les autres routes de l’API, certaines ne serviront pas et nous verrons aussi comment les désactiver pour éviter de les rendre accessibles.

Dans un deuxième terminal, je vais utiliser cfssl pour créer une clef privée, un CSR et faire signer ce dernier via l’API servie depuis le premier terminal.

cfssl gencert \
      -remote="localhost:8888" \
      -config=intermediate/config/profiles.json \
      -profile server \
      <(echo '
{
  "CN": "Test",
  "hosts": [
    "test.valhall.local"
  ],
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [{
    "C":  "FR",
    "ST": "Pays de la Loire",
    "L":  "Nantes",
    "O":  "Valhall"
  }]
}') | \
    cfssljson -bare test
# 2023/06/17 11:17:28 [INFO] generate received request
# 2023/06/17 11:17:28 [INFO] received CSR
# 2023/06/17 11:17:28 [INFO] generating key: rsa-2048
# 2023/06/17 11:17:28 [INFO] encoded CSR

Les logs de l’api montrent bien que le certificat a été signé :

# 2023/06/17 11:17:28 [INFO] signature request received
# 2023/06/17 11:17:28 [INFO] signed certificate with serial number 485014236354530765875959101436276396320072239922
# 2023/06/17 11:17:28 [INFO] wrote response
# 2023/06/17 11:17:28 [INFO] 127.0.0.1:46402 - "POST /api/v1/cfssl/sign" 200

J’aurais aussi pu générer une clef privée et un CSR avec openssl req -newkey rsa:2048 -nodes -keyout test-key.pem -out test.csr puis d’utiliser cfssl sign avec la même option -remote.

openssl x509 permet de consulter les informations du certificat :

openssl x509 -text -in test.pem
# Certificate:
#     Data:
#         Version: 3 (0x2)
#         Serial Number:
#             54:f4:ca:61:42:01:e9:0d:8a:ae:93:65:42:b1:66:37:5d:91:8b:32
#         Signature Algorithm: sha512WithRSAEncryption
#         Issuer: C = FR, ST = Ile de France, L = Nantes, O = Valhall, CN = Valhall Intermediate CA Certificate
#         Validity
#             Not Before: Jun 17 09:12:00 2023 GMT
#             Not After : Sep 16 15:12:00 2023 GMT
#         Subject: C = FR, ST = Pays de la Loire, L = Nantes, O = Valhall, CN = Test
#         Subject Public Key Info:
#             Public Key Algorithm: rsaEncryption
#                 Public-Key: (2048 bit)
#                 Modulus:
#                     00:c3:6e:f3:0a:21:ff:fa:be:10:11:48:63:60:1a:
#                     ...
#                     cf:f1
#                 Exponent: 65537 (0x10001)
#         X509v3 extensions:
#             X509v3 Key Usage: critical
#                 Digital Signature, Key Encipherment
#             X509v3 Extended Key Usage:
#                 TLS Web Server Authentication
#             X509v3 Basic Constraints: critical
#                 CA:FALSE
#             X509v3 Subject Key Identifier:
#                 33:29:48:E7:3A:B6:4E:90:92:1E:F7:F2:33:E6:1F:99:F2:42:5E:EF
#             X509v3 Authority Key Identifier:
#                 C8:80:42:BF:8B:0D:C9:9F:55:78:DD:56:E2:8B:1A:AE:57:49:37:1B
#             Authority Information Access:
#                 OCSP - URI:http://pki.valhall.local/intermediate/ocsp
#                 CA Issuers - URI:http://pki.valhall.local/intermediate/ca
#             X509v3 Subject Alternative Name:
#                 DNS:test.valhall.local
#             X509v3 CRL Distribution Points:
#                 Full Name:
#                   URI:http://pki.valhall.local/intermediate/crl
#     Signature Algorithm: sha512WithRSAEncryption
#     Signature Value:
#         a2:e2:9a:dd:83:57:ff:4e:3c:92:b3:cc:78:1b:4c:0e:f0:da:
#         ...
#         0e:3c:54:81:ef:04:9b:af
# -----BEGIN CERTIFICATE-----
# MIIFsDCCA5igAwIBAgIUVPTKYUIB6Q2KrpNlQrFmN12RizIwDQYJKoZIhvcNAQEN
# ...
# qVBNU7XEsx7X+n4rDjxUge8Em68=
# -----END CERTIFICATE-----

Le profil server a bien été utilisé : la période de validité est bien de 3 mois et les URIs concernant l’OCSP, les CA et CRL sont tels que configurés.

Pour tester la suite, le répondeur OCSP devra être lancé :

cfssl ocspserve -db-config intermediate/config/db.json -port 8889
# 2023/06/18 10:23:06 [INFO] Registering OCSP responder handler
# 2023/06/18 10:23:06 [INFO] Now listening on 127.0.0.1:8889

Le certificat test.pem pourra être verifié auprès du répondeur avec la commande openssl suivante :

openssl ocsp \
    -issuer <(cat intermediate/ca.pem) \
    -CAfile <(cat root/ca.pem) \
    -cert test.pem \
    -url http://localhost:8889

Si vous exécutez cette commande maintenant, vous devriez recevoir une erreur unauthorized. C’est parce que cfssl utilise des réponses OCSP cachées en base et pré-signées, ce qui veut dire qu’à chaque fois que vous révoquerez ou signerez un nouveau certificat il faudra la mettre à jour.

# Responder Error: unauthorized (6)

Après avoir rafraîchi la réponse OCSP, openssl ocsp retourne :

cfssl ocsprefresh \
    -ca intermediate/ca.pem \
    -responder intermediate/ocsp.pem \
    -responder-key intermediate/ocsp-key.pem \
    -db-config intermediate/config/db.json
# WARNING: no nonce in response
# Response verify OK
# test.pem: good
#   This Update: Jun 18 08:00:00 2023 GMT
#   Next Update: Jun 22 08:00:00 2023 GMT

Vous pouvez désactiver l’alerte concernant le nonce manquant avec le paramètre -no_nonce. Comme dit précédemment, cfssl utilise des réponses OCSP pré-signées, ce qui ne permet pas l’envoi de nonces (comportement compatible avec la RFC 5019, section 4). Le projet cfssl ne prévoie d’ailleurs pas de gérer l’envoi de nonce dans le futur, cf. cloudflare/cfssl/ocsp/responder.go#L336.

Il nous reste à valider le comportement à la révocation d’un certificat. L’API de révocation attend trois paramètres :

  • serial: l’identifiant unique du certificat, (étrangement) en décimal ;
  • authority_key_id: l’identifiant de la CA, en minuscules et en hexadécimal, sans séparateurs ;
  • reason: la raison de la révocation.

Les raisons possibles lors d’une révocation sont listées dans la RFC 5280, section 6.3.2. Leurs syntaxes pour l’api de cfssl sont écrites dans cloudflare/cfssl/ocsp/ocsp.go#L26 :

// revocationReasonCodes is a map between string reason codes
// to integers as defined in RFC 5280
var revocationReasonCodes = map[string]int{
    "unspecified":          ocsp.Unspecified,
    "keycompromise":        ocsp.KeyCompromise,
    "cacompromise":         ocsp.CACompromise,
    "affiliationchanged":   ocsp.AffiliationChanged,
    "superseded":           ocsp.Superseded,
    "cessationofoperation": ocsp.CessationOfOperation,
    "certificatehold":      ocsp.CertificateHold,
    "removefromcrl":        ocsp.RemoveFromCRL,
    "privilegewithdrawn":   ocsp.PrivilegeWithdrawn,
    "aacompromise":         ocsp.AACompromise,
}

Les valeurs des deux autres paramètres peuvent être déduites de la description du certificat de test avec openssl x509 affichée plus haut.

  • Le numéro de série 54:f4:ca:61:42:01:e9:0d:8a:ae:93:65:42:b1:66:37:5d:91:8b:32 devient 485014236354530765875959101436276396320072239922;
  • L’identifiant de la CA C8:80:42:BF:8B:0D:C9:9F:55:78:DD:56:E2:8B:1A:AE:57:49:37:1B devient c88042bf8b0dc99f5578dd56e28b1aae5749371b.
curl -d '{
  "serial": "485014236354530765875959101436276396320072239922",
  "authority_key_id": "c88042bf8b0dc99f5578dd56e28b1aae5749371b",
  "reason": "cessationofoperation"
}' http://localhost:8888/api/v1/cfssl/revoke
# {"success":true,"result":{},"errors":[],"messages":[]}

Comme vous pouvez le voir, il n’y a pas d’authentification disponible pour cette API, au contraire de l’api sign qui est aussi disponible avec au moins de l’authentification HMAC derrière la route authsign. Ce n’est donc pas une bonne idée d’exposer la route de révocation en l’état.

Après avoir rafraîchi la réponse OCSP, la commande openssl ocsp utilisée précédemment répond avec :

# Response verify OK
# test.pem: revoked
#   This Update: Jun 19 08:00:00 2023 GMT
#   Next Update: Jun 23 08:00:00 2023 GMT
#   Reason: cessationOfOperation
#   Revocation Time: Jun 19 08:25:56 2023 GMT

Et après avoir régénéré le fichier CRL, openssl crl retourne :

# Certificate Revocation List (CRL):
#         Version 2 (0x1)
#         Signature Algorithm: sha256WithRSAEncryption
#         Issuer: C = FR, ST = Ile de France, L = Nantes, O = Valhall, CN = Valhall Intermediate CA Certificate
#         Last Update: Jun 19 08:46:10 2023 GMT
#         Next Update: Jun 26 08:46:10 2023 GMT
#         CRL extensions:
#             X509v3 Authority Key Identifier:
#                 C8:80:42:BF:8B:0D:C9:9F:55:78:DD:56:E2:8B:1A:AE:57:49:37:1B
# Revoked Certificates:
#     Serial Number: 54F4CA614201E90D8AAE936542B166375D918B32
#         Revocation Date: Jun 19 08:25:56 2023 GMT
#     Signature Algorithm: sha256WithRSAEncryption
#     Signature Value:
#         d6:3d:18:16:6c:6a:db:07:99:41:02:76:aa:4b:16:b5:da:bd:
#         ...
#         42:df:ef:c2:6f:17:6b:3c

Nous savons donc depuis ces deux méthodes que le certificat est bien révoqué.

Conclusion Link to heading

Dans ce billet, j’ai créé une CA racine et une CA intermédiaire. J’ai été capable de signer puis de révoquer un certificat et de le tester dans ces différentes situations avec le répondeur OCSP et contre la CRL. Tout semble en ordre pour la suite.

Les futurs articles de cette série aborderont l’hébergement dans kubernetes (dans lequel j’automatiserai le rafraîchissement des réponses OCSP et des CRL) et l’utilisation de ce service pour fournir automatiquement des certificats pour d’autres services auto-hébergés.