Aller au contenu
Révocation

Ce qu'il révoque, et comment les serveurs l'apprennent

La KRL et la CRL signées et datées qu'authn-bridge publie pour les certificats qu'il a révoqués, et le transmetteur Shared Signals qui atteint ce qu'aucune liste ne peut nommer.

Désactiver une personne ou un établissement (l’API d’administration) met fin à tout ce qui revient vers le fournisseur : jetons de rafraîchissement, /userinfo, nouveaux certificats, mots de passe d’application. Ce qu’il a déjà distribué est vérifié ailleurs, par rapport à des clés, et trois mécanismes l’atteignent :

identifiantrepris par
un certificat SSH signé par cette ACla KRL, GET /ssh/krl, avec sa signature à /ssh/krl.sig
un certificat client X.509la CRL, GET /x509/crl
un jeton d’accès, un certificat opkssh, tout ce qu’un serveur vérifie seulun événement Shared Signals session-revoked

La révocation est définitive. Réactiver une personne ne lui rend pas ses certificats ; elle en reçoit de nouveaux. Une liste dont on pourrait retirer une révocation est une liste que quiconque atteint l’API d’administration pourrait modifier.

Les listes

Les deux sont émises comme le dit le protocole de go-authn/revocation, afin qu’elles puissent transiter par n’importe quoi (un miroir, du HTTP simple, un fichier) et rester vérifiables par quiconque les lit :

KRLCRL
formatOpenSSH PROTOCOL.krl, écrite par go-authn/krl : ce que lisent le RevokedKeys de sshd et ssh-keygen -QDER, RFC 5280
signéepar une SSHSIG détachée avec la clé de l’AC SSH, espace de noms krl@go-authn.github.iopar l’AC qui signe les certificats
à jour jusqu’àson extension expires@go-authn.github.io (que sshd ignore)son nextUpdate
ordonnée parsa krl_version, puis sa date de générationson CRL Number, puis thisUpdate

Chaque liste indique qu’elle cesse d’être à jour une heure après son émission. Une liste est conservée et servie telle quelle jusqu’à mi-chemin de cette échéance, ou jusqu’à ce que quelque chose soit révoqué ; la requête suivante reçoit alors une nouvelle émission. Une émission n’est jamais datée d’avant celle qu’elle remplace, même si l’horloge recule, puisqu’un lecteur refuse une émission antérieure de la même version comme un retour en arrière. Le CRL Number augmente à chaque émission.

La KRL d’un fournisseur de test sans rien de révoqué :

$ curl -s -D - -o var/bridge.krl http://127.0.0.1:28473/ssh/krl
HTTP/1.1 200 OK
Cache-Control: max-age=60
Content-Type: application/octet-stream
Etag: "db5b4e920d2da3911da51a8f70db8c4f"
Date: Sun, 11 Oct 2026 09:48:14 GMT
Content-Length: 130

$ ssh-keygen -Q -l -f var/bridge.krl
# KRL version 0
# Generated at 20261011T114814
# Comment: go-authn/bridge http://127.0.0.1:28473

L’ETag est celui du contenu. Un lecteur interroge avec If-None-Match, et reçoit 304 tant que l’émission qu’il détient est toujours celle servie ; puisque le contenu change au moins toutes les demi-heures, un lecteur qui interroge ne garde jamais une copie au-delà de son expiration :

$ curl -s -o /dev/null -H 'If-None-Match: "db5b4e920d2da3911da51a8f70db8c4f"' -w '%{http_code}\n' http://127.0.0.1:28473/ssh/krl
304

La signature est récupérée avec If-Match sur l’ETag de la liste. Une liste réémise entre-temps répond 412, plutôt qu’une signature qui ne lui correspond pas :

$ curl -s -o var/bridge.krl.sig -H 'If-Match: "db5b4e920d2da3911da51a8f70db8c4f"' -w '%{http_code}\n' http://127.0.0.1:28473/ssh/krl.sig
200
$ curl -s -o /dev/null -H 'If-Match: "stale"' -w '%{http_code}\n' http://127.0.0.1:28473/ssh/krl.sig
412

ssh-keygen vérifie la signature, et la refuse dans tout autre espace de noms :

$ ssh-keygen -Y verify -f allowed -I bridge -n krl@go-authn.github.io -s bridge.krl.sig < bridge.krl
Good "krl@go-authn.github.io" signature for bridge with ED25519 key SHA256:Fpa/Ku2Wv1EDCR+KSsqZ2eECTM1zftiBWj01Lr+NO2Q

où allowed est la ligne bridge namespaces="krl@go-authn.github.io" <the CA key from /ssh/config>.

La CRL, émise quinze secondes plus tard :

$ openssl crl -inform DER -in var/bridge.crl -noout -text
Certificate Revocation List (CRL):
    Version 2 (0x1)
    Signature Algorithm: ecdsa-with-SHA256
    Issuer: CN=go-authn bridge NFS client CA
    Last Update: Oct 11 09:48:29 2026 GMT
    Next Update: Oct 11 10:48:29 2026 GMT
    CRL extensions:
        X509v3 Authority Key Identifier: 
            2A:90:7A:50:FF:65:52:BA:70:4E:E1:F8:70:EE:7A:FC:B1:8B:FE:4E
        X509v3 CRL Number: 
            1791712109993
No Revoked Certificates.
    Signature Algorithm: ecdsa-with-SHA256
    Signature Value:
        30:44:02:20:37:90:14:6f:c1:f2:ea:98:29:04:61:88:a8:17:
        88:e3:f6:84:9d:46:a7:fc:2b:ac:81:26:47:9d:ac:84:23:38:
        02:20:0a:a6:93:cb:7d:2d:81:e4:59:d4:0d:a0:fd:f4:6e:75:
        c9:0d:53:e7:92:49:ac:a1:31:a8:90:09:91:d1:87:b6

Un certificat reste dans sa liste jusqu’à son expiration : un certificat expiré est refusé partout sans être listé.

Qui les lit

  • Un sshd ordinaire, ou nginx et d’autres serveurs TLS : authn-revokd récupère les listes, ne garde que celles qui se vérifient, les écrit là où les serveurs les lisent, et refuse en cas de doute pour sshd quand une liste expire.
  • go-fileshare les récupère et les vérifie lui-même, et refuse les certificats du fournisseur tant qu’il ne détient aucune liste à jour.

Shared Signals

Un jeton d’accès ou un certificat opkssh est vérifié là où il est utilisé, et aucune liste ne peut le nommer. Le fournisseur est donc un transmetteur OpenID Shared Signals : sur DisablePerson, RevokePerson et DisableIdP, il met en file un événement CAEP session-revoked (tout ce qui a été émis pour ce sujet avant l’heure de l’événement est nul) pour chaque récepteur qui l’a demandé.

state { ... }            # required: a queue a restart empties loses revocations
ssf {}                   # event_retention = "192h" by default

client "fileshare-ssf" { # a receiver: client credentials, scope ssf
  secret_file  = "/etc/authn-bridge/fileshare-ssf.secret"
  ssf_receiver = true
  audience     = ["fileshare"]
}
$ curl -s http://127.0.0.1:28473/.well-known/ssf-configuration | jq .
{
  "issuer": "http://127.0.0.1:28473",
  "jwks_uri": "http://127.0.0.1:28473/jwks",
  "delivery_methods_supported": [
    "urn:ietf:rfc:8936"
  ],
  "configuration_endpoint": "http://127.0.0.1:28473/ssf/streams",
  "status_endpoint": "http://127.0.0.1:28473/ssf/streams/status",
  "add_subject_endpoint": "http://127.0.0.1:28473/ssf/streams/subjects:add",
  "remove_subject_endpoint": "http://127.0.0.1:28473/ssf/streams/subjects:remove",
  "verification_endpoint": "http://127.0.0.1:28473/ssf/streams/verify",
  "spec_version": "1_0",
  "authorization_schemes": [
    {
      "spec_urn": "urn:ietf:rfc:6749"
    }
  ]
}
  • Remise par interrogation uniquement (RFC 8936) : le récepteur demande ; rien ici n’appelle vers l’extérieur.
  • Un récepteur s’authentifie avec un jeton d’accès obtenu par l’octroi client credentials, pour un client ssf_receiver doté d’un secret, portée ssf. Il ne voit que ses propres flux, dix au plus.
  • Les événements sont des SET (RFC 8417) signés avec la clé des jetons d’identité, sans exp ni sub. Le sujet est le compte, acct:<username>, par défaut : un sub peut être pairwise, un par client. Un récepteur configuré ssf_subject_format = "iss_sub" reçoit à la place l’émetteur et le sub public.
  • Un établissement désactivé donne un événement par personne dont le fournisseur garde une trace là-bas, et un dont le sujet est le tenant, l’entity ID de l’établissement, avec ses portées, qui couvre les personnes que le fournisseur a oubliées.
  • Les flux et les événements non remis sont conservés dans la base state jusqu’à leur acquittement ou l’écoulement d’event_retention. Un événement qu’un récepteur signale en erreur est redistribué, jusqu’à dix fois.

Le bloc SSF est refusé sans bloc state :

authn-bridge: ssf: needs a state block: a queue a restart empties loses revocations

Créée le par tannevaled

Modifiée le par tannevaled