Ce qu'il révoque, et comment les serveurs l'apprennent
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 :
| identifiant | repris par |
|---|---|
| un certificat SSH signé par cette AC | la KRL, GET /ssh/krl, avec sa signature à /ssh/krl.sig |
| un certificat client X.509 | la CRL, GET /x509/crl |
| un jeton d’accès, un certificat opkssh, tout ce qu’un serveur vérifie seul | un é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 :
| KRL | CRL | |
|---|---|---|
| format | OpenSSH PROTOCOL.krl, écrite par go-authn/krl : ce que lisent le RevokedKeys de sshd et ssh-keygen -Q | DER, RFC 5280 |
| signée | par une SSHSIG détachée avec la clé de l’AC SSH, espace de noms krl@go-authn.github.io | par l’AC qui signe les certificats |
| à jour jusqu’à | son extension expires@go-authn.github.io (que sshd ignore) | son nextUpdate |
| ordonnée par | sa krl_version, puis sa date de génération | son 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:28473L’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
304La 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
412ssh-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+NO2Qoù 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:b6Un certificat reste dans sa liste jusqu’à son expiration : un certificat expiré est refusé partout sans être listé.
Qui les lit
- Un
sshdordinaire, 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_receiverdoté d’un secret, portéessf. 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
expnisub. Le sujet est le compte,acct:<username>, par défaut : unsubpeut être pairwise, un par client. Un récepteur configuréssf_subject_format = "iss_sub"reçoit à la place l’émetteur et lesubpublic. - 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
statejusqu’à 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 revocationsCréée le par tannevaled
Modifiée le par tannevaled