Authentification RSA dans SSH
Cet article applique au protocole SSH la signature-based authentication introduite dans Chiffrement et signature RSA, et qui avait été illustrée en Python sur un exemple simple.
Principe de l'authentification par signature
Voici les étapes classiques lorsqu'un hôte A s'authentifie par signature (on dit aussi par clé publique) auprès d'un hôte B :
Aenvoie sa clé publique àBBaccepte ou non la clé publique deA. S'il accepte :Aenvoie une signature calculée avec sa clé privée sur un message spécifique (connu deB)Bvérifie la signature avec la clé publique deA: si elle est valide, l'authentification est réussie
Je n'ai pas trouvé de source pouvant corroborer ce pattern. Dommage car, exprimé tel quel, il donne à mon sens une bonne vue d'ensemble. En réalité, il ressort bien dans les standards des protocoles, comme SSH et TLS, mais décrit de façon plus technique et moins directe.
De plus, les protocoles ne le déroulent pas forcément étape par étape. Dans SSH par exemple, le serveur envoie dans un même paquet sa clé publique et la signature calculée (étapes 1 et 3). À la réception, le client, s'il accepte la clé publique, vérifie la signature (étapes 2 et 4).
Les étapes 3 et 4 sont indispensables.
Présenter une clé publique ou un certificat, même signé par une CA, ne suffit pas :
A doit prouver à B qu'il possède la clé privée associée à la clé publique.
La signature, si elle est vérifiée, en apporte la preuve—nous parlons de proof of possession.
Enfin, la signature porte en général sur tout ou partie des données du handshake du protocole. Pourquoi signer cela au lieu d'un message aléatoire décorrélé de la session ? Sans doute pour renforcer la sécurité et faire d'une pierre deux coups : authentification et intégrité (une autre application de la signature). La RFC 8446 de TLS 1.3 dit en effet à ce sujet :
This message is used to provide explicit proof that an endpoint
possesses the private key corresponding to its certificate. The
CertificateVerify message also provides integrity for the handshake
up to this point. Servers MUST send this message when authenticating
via a certificate.
Application à SSH
Le protocole SSH constitue, selon moi, un cas d'école de la signature-based authentication. Il peut aussi constituer un chemin naturel vers les certificats, pour en comprendre l'intérêt et dans quelle mesure ils remplacent l'acceptation manuelle des clés publiques.
D'ailleurs, les extraits des standards parlent souvent à la fois de clés publiques et de certificats, comme s'il s'agissait de la même chose.
La route vers les certificats
Le problème d'acceptation des clés publiques
Comme nous le verrons concrètement avec OpenSSH, l'étape 2 du pattern consiste en une acceptation humaine et manuelle de la clé publique présentée par le serveur :
The authenticity of host '192.168.122.254 (192.168.122.254)' can't be established.
RSA key fingerprint is SHA256:y5NfcIqOn3D7YLrIg2+1EwX8rD6dv1BP5gfK4jK9Ijw.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Mais comment avoir confiance en la clé publique présentée et être sûr que je m'adresse au bon serveur (et non pas un malicieux) ? Les utilisateurs acceptent souvent à l'aveugle les clés publiques, après avoir lancé une connexion vers le hostname ou l'adresse IP du serveur.
Nous pourrions imaginer que l'équipe en charge des serveurs communique, par mail ou papier, une liste qui associe, pour chaque serveur, sa clé publique. Cette liste, émanant de l'équipe qui a autorité sur les serveurs, apporte la certification que telle clé publique appartient à tel serveur. Ainsi cela rejoint, dans l'idée, les notions de certificat et de CA (Certificate Authority).
Les notions de certificat et de CA
Un certificat peut se définir comme une structure de données normalisée—souvent au format X.509 mais pas toujours—qui associe un sujet (un serveur SSH dans l'exemple) à une clé publique, et qui est signé par une CA afin d'attester que cette association est correcte. La RFC 5280 dit :
Users of a public key require confidence that the associated private
key is owned by the correct remote subject (person or system) with
which an encryption or digital signature mechanism will be used.
This confidence is obtained through the use of public key
certificates, which are data structures that bind public key values
to subjects. The binding is asserted by having a trusted CA
digitally sign each certificate.
La CA apposant une signature sur le certificat, cela peut complexifier la compréhension de notre propos car le client vérifie alors deux signatures :
- celle du certificat (apposée par la CA) présenté par le serveur—pour accepter sa clé publique
- celle envoyée par le serveur—pour s'assurer qu'il a la clé privée associée (et l'authentifier)
Une fois encore, se contenter de présenter une clé publique ou un certificat, même signé par une CA, ne suffit pas : tout le monde peut (en théorie) le faire. Je peux configurer le certificat de Twitch, public et téléchargeable, sur mon serveur Web et le présenter aux clients qui s'y connectent. Sauf qu'il faudra prouver que je possède la clé privée associée, ce qui échouera.
Pour le premier point—la vérification de la signature apposée par la CA—le client doit avoir connaissance de la clé publique (plus précisément, le certificat) de la CA en question, ajoutée au préalable par un moyen hors bande et car considérée de confiance. Cela rejoint la notion de chain of trust, hors périmètre de cet article.
L'acceptation des certificats
Utiliser des certificats signés par une CA permet d'automatiser l'acceptation de la clé publique du serveur, le client effectuant avant des vérifications sur le certificat, qui dépendent du protocole.
Le draft-ietf-sshm-cert
spécifie un format de certificats propre à OpenSSH, plus simple que le format standardisé X.509 de TLS, et plus adapté à son besoin,
par exemple ssh-rsa-cert :
RSA certificates have type "ssh-rsa-cert". These certify RSA, as
defined in Section 6.6 of [RFC4253]. This format is equivalent to
the vendor extension "ssh-rsa-cert-v01@openssh.com".
string "ssh-rsa-cert"
string nonce
mpint e
mpint n
uint64 serial number
uint32 certificate role
string identifier
string principals
uint64 valid after
uint64 valid before
string critical options
string extensions
string reserved
string signature key
string signature
"n" is the public composite modulus and "e" is the public exponent.
These values are defined by Section 5.1 of [FIPS.186-4].
Dans la section accepting certificates, il donne les vérifications à effectuer par le client pour considérer le certificat du serveur acceptable—autrement dit, pour accepter la clé publique présentée par le serveur—à commencer par la signature apposée par la CA :
Implementations that accept a certificate MUST verify the CA
signature over the certificate contents prior to making any
authentication or authorisation decisions. Implementations MAY elect
to verify the certificate signature as soon as the certificate is
received.
Avec ensuite les autres étapes, que je reprends ici à titre informatif :
* MUST check that the certificate is well-formed
* MUST ensure that no unsupported critical options are present
* MUST check that the "certificate role" matches the intended decision type (user or host authentication)
* MUST check the time against the certificate validity interval
* MUST verify that at least one of the listed certificate principals is accepted
* MUST perform any additional authorisation operations required by critical options.
Noter que le protocole SSH fonctionne donc à la fois avec des clés publiques « brutes » et des certificats, même si moins répandu.
Cas pratique avec OpenSSH
Je vais commenter pas-à-pas un échange OpenSSH avec le mode verbeux activé, sachant que je ne reprends que le debug pertinent pour l'article.
Début du handshake
Le client initie la connexion vers le serveur
Nous considérons qu'il s'agit de sa première connexion vers ce serveur.
$ ssh brindereseau@192.168.122.254 -vvv
[…]
debug1: Local version string SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.17
debug1: Remote protocol version 2.0, remote software version ROSSSH
Après avoir établi la connexion TCP sur le port 22 habituel,
les parties échangent leur version du protocole via un identifier de la forme SSH-protoversion-softwareversion :
Commence ensuite la phase KEX comme spécifié par la RFC 4253 :
Key exchange will begin immediately after sending this identifier.
Par conséquent, nous verrons dans la suite des paquets comme
SSH_MSG_KEXINIT ou encore SSH_MSG_KEX_DH_GEX_REPLY, pour ne citer qu'eux.
Notion de KEX (Key EXchange)
Il s'agit de la phase d'échange de clés—de type Diffie-Hellman—qui permet d'aboutir sur un secret partagé entre les parties, utilisé ensuite pour le chiffrement du trafic (avec AES par exemple) :
The Diffie-Hellman (DH) key exchange provides a shared secret that
cannot be determined by either party alone. The key exchange is
combined with a signature with the host key to provide host
authentication. This key exchange method provides explicit server
authentication as defined in Section 7.
De plus, comme dit dans l'extrait, elle permet au serveur de présenter sa clé publique au client avec une signature afin de s'authentifier auprès de ce dernier (étapes 1 et 3 de notre pattern).
Un point important, l'authentification dans SSH est bidirectionnelle :
- le client s'authentifie auprès du serveur (par mot de passe ou clé publique)
- mais avant tout, le serveur s'authentifie auprès du client (par clé publique)
Une capture montre en clair les paquets KEX, qui ne véhiculent rien de sensible. Au contraire, l'authentification du client auprès du serveur qui suit se veut, elle, chiffrée. Et à raison, surtout quand le client se connecte via mot de passe, pour ne pas qu'il transite en clair.
Authentification du serveur auprès du client
Négociation de l'algorithme d'authentification
Durant la phase KEX, chaque partie envoie à l'autre un paquet SSH_MSG_KEXINIT :
$ ssh brindereseau@192.168.122.254 -vvv
[…]
debug1: SSH2_MSG_KEXINIT sent
debug1: SSH2_MSG_KEXINIT received
Ces paquets contiennent les algorithmes cryptographiques que chaque partie supporte,
en particulier le champ server_host_key_algorithms qui nous intéresse ici :
byte SSH_MSG_KEXINIT
byte[16] cookie (random bytes)
name-list kex_algorithms
name-list server_host_key_algorithms
name-list encryption_algorithms_client_to_server
name-list encryption_algorithms_server_to_client
name-list mac_algorithms_client_to_server
name-list mac_algorithms_server_to_client
name-list compression_algorithms_client_to_server
name-list compression_algorithms_server_to_client
[…]
Les parties s'entendent sur ces algorithmes et le choix final apparaît plus bas :
debug1: kex: algorithm: diffie-hellman-group-exchange-sha256
debug1: kex: host key algorithm: rsa-sha2-256
debug1: kex: server->client cipher: aes192-ctr MAC: hmac-sha2-256 compression: none
debug1: kex: client->server cipher: aes192-ctr MAC: hmac-sha2-256 compression: none
Le serveur s'authentifiera auprès du client via l'algorithme rsa-sha2-256 que définit la
RFC 8332 :
Signing and verifying using these algorithms is performed according
to the RSASSA-PKCS1-v1_5 scheme in [RFC8017] using SHA-2 [SHS] as
hash.
For the algorithm "rsa-sha2-256", the hash used is SHA-256.
For the algorithm "rsa-sha2-512", the hash used is SHA-512.
Elle mentionne de plus le schéma de signature \(\text{RSASSA-PKCS1-v1\_5}\) abordé dans cet article.
Le serveur présente sa clé publique au client
Plus loin, le serveur envoie sa clé publique au client dans le paquet SSH_MSG_KEX_DH_GEX_REPLY :
debug1: SSH2_MSG_KEX_DH_GEX_REQUEST(2048<8192<8192) sent
debug1: SSH2_MSG_KEX_DH_GEX_GROUP received
debug1: SSH2_MSG_KEX_DH_GEX_INIT sent
debug1: SSH2_MSG_KEX_DH_GEX_REPLY received
La capture associée :
Ce paquet combine plusieurs informations. Le champ KEX host key nous intéresse ici :
il contient la clé publique de type ssh-rsa du serveur avec l'exposant \(e = 65537\) ou 0x10001 en hexadécimal
et le module \(n\) sur 256 octets (soit une clé de 2048 bits).
mpint, abréviation de multi precision integer, qui permet la représentation d'entiers signés.
Pour un entier positif, si son premier bit ne vaut pas 0 (cas de \(n\)),
il faut ajouter l'octet 0x00 au début, d'où
la taille affichée de 257 octets bien qu'il s'agisse d'une clé de 256 octets.
Voir l'article OpenSSH : clé publique et fingerprint.
Le client accepte (ou non) la clé publique du serveur
Côté client, arrive alors une étape bien connue des utilisateurs d'OpenSSH : la clé publique du serveur doit être acceptée explicitement (ou refusée).
debug3: hostkeys_foreach: reading file "/home/brindereseau/.ssh/known_hosts"
[…]
The authenticity of host '192.168.122.254 (192.168.122.254)' can't be established.
RSA key fingerprint is SHA256:y5NfcIqOn3D7YLrIg2+1EwX8rD6dv1BP5gfK4jK9Ijw.
The authenticity of host '192.168.122.254 (192.168.122.254)' can't be established.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])?
OpenSSH présente pour cela au client une version courte de la clé publique du serveur appelée empreinte ou fingerprint.
Elle correspond au base64 d'un hash de la clé publique encodée selon un certain format.
L'option VisualHostKey=yes permet d'afficher un random art de ce même hash.
La RFC 4716 explique l'intérêt d'une telle empreinte :
The security of the SSH protocols relies on the verification of
public host keys. Since public keys tend to be very large, it is
difficult for a human to verify an entire host key. Even with a
Public Key Infrastructure (PKI) in place, it is useful to have a
standard for exchanging short fingerprints of public keys.
En d'autres termes, elle aide le client, visuellement parlant, à s'assurer qu'il communique avec le bon serveur—dans l'hypothèse où le client connaît à l'avance l'empreinte de la clé publique du serveur pour comparer avec celle présentée.
Si le client accepte la clé publique, OpenSSH l'ajoute à son fichier known_hosts :
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.122.254' (RSA) to the list of known hosts.
$ tail -1 .ssh/known_hosts
192.168.122.254 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDY3j1nubqGI2gt9odUh9GnRFf8G5xHZ6p/ljtaUmAXqFANYIypddA4BZ4O8A60QFZbmWfHomfqWzx+IAVDhkr8LvUyyPyiK0h7Wt89GpuGR50DWSN2jZ1M0AFKMCzEuwZWfJ53mIn2QD2Pe9dnoYAjZ+KGf1Q18squ4yUuQwqmmTS3Sw7N+omvgC4+4RjZeOdj9SoaTmdRECNHwhqLMFzVUw4wHRaqGlnpUalCfEX5VzrOpvy0LQQT5Bj3FSk9HtyxY0S3ZVkQan0DbRBbLUl9Un/o+kc7H8SI3id6Emes1tlHyeLxAzeXXhPbbdEPe/KdjV8997KMB+J5ZTk/tcjj
L'approche TOFU (Trust On First Use)
À partir de là, le client considère de confiance la clé publique du serveur. Il n'aura pas à l'accepter de nouveau aux prochaines connexions. Cette approche s'appelle trust on first use ou TOFU. Le récent draft-ietf-sshm-hostkey de 2026 l'explique bien :
Excluding certified keys such as "pgp-sign-rsa" (Section 6.6 of
[RFC4253]) or OpenSSH certificates ([I-D.miller-ssh-cert]), the SSH
protocol does not specify any way for a client to learn the host keys
of a server. Public keys may be shared out-of-band but are more
commonly learned for a given server the first time a client connects
to it and trusted thereafter. This is the origin of the Trust on
First Use (TOFU) pattern.
OpenSSH vérifiera à chaque nouvelle connexion que la clé publique présentée par le serveur ne diffère pas par rapport à celle dans known_hosts,
sinon le client obtiendra le fameux message :
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
SHA256:C2NQ+SQZ7S3w//3Uv3m+PJLoT9xG4NiT+ILv1SX5vMQ.
Please contact your system administrator.
Add correct host key in /home/brindereseau/.ssh/known_hosts to get rid of this message.
Offending RSA key in /home/brindereseau/.ssh/known_hosts:1136
remove with:
ssh-keygen -f "/home/brindereseau/.ssh/known_hosts" -R "192.168.122.254"
Host key for 192.168.122.254 has changed and you have requested strict checking.
Host key verification failed.
Cela peut provenir d'un serveur malicieux qui se fait passer pour celui ciblé. Plus légitimement, comme souvent, la raison provient de la re-génération de la keypair sur le serveur : le client doit accepter la nouvelle clé publique présentée.
Ce problème de changement de clé constitue d'ailleurs la motivation même du draft :
There is no facility in the SSH transport protocol that allows a
server to gracefully rotate its host keys. Unless coordinated out-
of-band, a server changing host keys in this model is a hard break of
connection trust, as any client that had learned the previous host
key would now be met with an unexpected and untrusted key attempting
to authenticate the final key exchange. This situation is
effectively indistinguishable from an on-path adversary hijacking the
connection.
Qui propose, bien que hors périmètre de cet article, un mécanisme de rotation plus automatisé :
This document describes a simple extension to the SSH protocol that
provides a mechanism for a server to advertise its full set of host
keys to a client, and to prove possession of the requisite private
key material for each of them.
Le serveur envoie une signature calculée avec sa clé privée
À ce stade, l'authentification du serveur auprès du client n'est pas encore réussie. Le client a accepté la clé publique mais, comme expliqué en début d'article avec notre pattern, le serveur doit maintenant prouver qu'il possède la clé privée associée à la clé publique.
La signature RSA intervient précisément ici :
le serveur S calcule la signature s d'un hash H avec sa clé privée,
et le client C vérifiera cette signature avec la clé publique K_S du serveur,
préalablement acceptée.
La RFC 4419,
qui s'applique ici en raison de la méthode KEX négociée diffie-hellman-group-exchange-sha256 entre les parties,
le dit plus techniquement ainsi :
4. S generates a random number y, where 0 < y < (p-1)/2, and
computes f = g^y mod p. S receives "e". It computes K = e^y mod
p, H = hash(V_C || V_S || I_C || I_S || K_S || min || n || max ||
p || g || e || f || K) (these elements are encoded according to
their types; see below), and signature s on H with its private
host key. S sends "K_S || f || s" to C. The signing operation
may involve a second hashing operation.
Le paquet SSH_MSG_KEX_DH_GEX_REPLY affiché précédemment comportait en fait déjà la signature :
Il véhicule en effet les trois valeurs suivantes :
byte SSH_MSG_KEX_DH_GEX_REPLY
string server public host key and certificates (K_S)
mpint f
string signature of H
Autrement dit, dans l'ordre :
- la clé publique RSA ou le certificat du serveur—comme vu plus haut
- la clé publique Diffie-Hellman \(f\) du serveur—hors périmètre de cet article
- la signature calculée par le serveur avec sa clé privée
Comme souvent dans les protocoles, le calcul de la signature porte sur une combinaison d'informations liées à la session en cours :
The hash H is computed as the HASH hash of the concatenation of the
following:
string V_C, the client's version string (CR and NL excluded)
string V_S, the server's version string (CR and NL excluded)
string I_C, the payload of the client's SSH_MSG_KEXINIT
string I_S, the payload of the server's SSH_MSG_KEXINIT
string K_S, the host key
uint32 min, minimal size in bits of an acceptable group
uint32 n, preferred size in bits of the group the server will send
uint32 max, maximal size in bits of an acceptable group
mpint p, safe prime
mpint g, generator for subgroup
mpint e, exchange value sent by the client
mpint f, exchange value sent by the server
mpint K, the shared secret
Chaque partie construit cette concaténation, à l'identique, sans besoin de l'échanger sur le réseau.
Il vaut mieux d'ailleurs, puisque le secret partagé Diffie-Hellman y figure.
Puis elles en font le hash, SHA-256 en raison de la méthode diffie-hellman-group-exchange-sha256 négociée.
Le serveur applique ensuite l'opération SIGN du schéma de signature \(\text{RSASSA-PKCS1-v1\_5}\)
avec le hash en paramètre d'entrée, qui résulte en la signature envoyée au client.
Le client vérifie la signature calculée par le serveur
Pour terminer, le client vérifie la signature fournie par le serveur depuis la clé publique K_S du serveur, préalablement acceptée.
Si la vérification passe, l'authentification du serveur auprès du client est (enfin) réussie.
De nouveau, la RFC 4419 le dit techniquement ainsi :
5. C verifies that K_S really is the host key for S (e.g., using
certificates or a local database to obtain the public key). C is
also allowed to accept the key without verification; however,
doing so will render the protocol insecure against active attacks
(but may be desirable for practical reasons in the short term in
many environments). C then computes K = f^x mod p, H = hash(V_C
|| V_S || I_C || I_S || K_S || min || n || max || p || g || e ||
f || K), and verifies the signature s on H.
Notons qu'elle évoque la nécessité, pour le client, de s'assurer de la légitimité de la clé publique présentée par le serveur,
la local database s'apparentant au fichier known_hosts dans OpenSSH.
StrictHostKeyChecking=no dans OpenSSH.
Pour vérifier la signature reçue, le client doit auparavant construire la même combinaison d'informations (décrite plus haut) que le serveur, sur lequel il applique la même fonction hash.
Il applique ensuite l'opération VERIFY du schéma de signature \(\text{RSASSA-PKCS1-v1\_5}\),
dont le retour booléen indique si la signature est valide ou non.
Pour l'exercice, si nous déroulons les premières étapes de cette opération, en appliquant la primitive cryptographique \(\text{RSAVP1}\), c'est-à-dire la formule \(s^e \pmod n\), nous obtenons :
def RSAVP1(n: int, e: int, s: int) -> int:
return pow(s, e, n) # s^e mod n
def I2OSP(i: int) -> bytes:
return int.to_bytes(i, byteorder="big", length=(i.bit_length() + 7) // 8)
n = 0xd8de3d67b9ba8623682df6875487d1a74457fc1b9c4767aa7f963b5a526017a8500d608ca975d038059e0ef00eb440565b9967c7a267ea5b3c7e200543864afc2ef532c8fca22b487b5adf3d1a9b86479d035923768d9d4cd0014a302cc4bb06567c9e779889f6403d8f7bd767a1802367e2867f5435f2caaee3252e430aa69934b74b0ecdfa89af802e3ee118d978e763f52a1a4e6751102347c21a8b305cd5530e301d16aa1a59e951a9427c45f9573acea6fcb42d0413e418f715293d1edcb16344b76559106a7d036d105b2d497d527fe8fa473b1fc488de277a1267acd6d947c9e2f10337975e13db6dd10f7bf29d8d5f3df7b28c07e27965393fb5c8e3
e = 0x010001
s = 0x9643c22e2cf5de43ccd20e9ab0f75f3c4d3370b1c5f2558868ea684d16e15f3a69cb44057b299923135fe9a008034f1c27cbbf69a5b86475ce993f7712b9e54a1d09c91ef22f335b359604587bb37f42e53f66aa25b0d305d4eee8837544c9eb17758a044d5b44f41358510567f696ee3aadbb35c5723e9ecaa4024604f474cf7d32676d621a3c61e053356b12ce6092a2c94cf3c21c650fe64581d9c614e3d04bf6387e8e9f7862c3a41e8d6172065efe11fdf946b5e24117ef926ec68f48d57e499b6328b3cc58244cf8a165368910f8c758c89258684747cc0f40899893d81a90bb30927452009c7233f8a1791f622a6d2e5d11f0c421df869da228e1367b
print(I2OSP(RSAVP1(n, e, s)).hex())
# 01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003031300d06096086480165030402010500042084b6bbefb1091156fc638e26e91658a2c75266738792ca4f49654d7abec536e6
La répétition du motif 0xff dans le résultat obtenu est très révélatrice du schéma de signature \(\text{RSASSA-PKCS1-v1\_5}\) sous-jacent.
Pour rappel, en Python, nous pouvons écrire un entier sous sa forme décimale ou hexadécimale :
e = 0x010001
print(e)
# 65537
e = 65537
print(e)
# 65537
Sachant que j'ai utilisé dans le petit script précédent les notations hexadécimales directement issues de la capture (de Wireshark).
Nous noterons une particularité pour la valeur de la signature :
- elle commence par
0x000001009643c22e2cf5de43ccf6… - or j'ai retiré les 4 octets
0x00000100
La raison, un peu pénible à expliquer à l'écrit mais simple à comprendre,
provient du type string du champ rsa_signature_blob
(RFC 8332) :
The resulting signature is encoded as follows:
string "rsa-sha2-256" / "rsa-sha2-512"
string rsa_signature_blob
The value for 'rsa_signature_blob' is encoded as a string that
contains an octet string S (which is the output of RSASSA-PKCS1-v1_5)
and that has the same length (in octets) as the RSA modulus.
La RFC 4251
précisant, elle, l'encodage du type :
la valeur est précédée de sa longueur codée sur 4 octets.
Il faut donc retirer 0x00000100 qui indique que 256 octets vont suivre (la taille de la signature).
Authentification du client auprès du serveur
Une fois le serveur authentifié, vient le tour du client. La RFC 4252 décrit plusieurs méthodes d'authentification :
"publickey" REQUIRED
"password" OPTIONAL
"hostbased" OPTIONAL
"none" NOT RECOMMENDED
Nous nous concentrons sur l'authentification par clé publique, ou publickey, très répandue :
With this method, the possession of a private key serves as
authentication. This method works by sending a signature created
with a private key of the user. The server MUST check that the key
is a valid authenticator for the user, and MUST check that the
signature is valid. If both hold, the authentication request MUST be
accepted; otherwise, it MUST be rejected.
Cet extrait résume à lui seul notre pattern du début d'article. Et nous retrouvons en fait le même schéma que l'authentification du serveur auprès du client, mais dans l'autre sens. Par conséquent, je serai plus sommaire dans cette section.
Le client présente sa clé publique et une signature comme preuve de possession de la clé privée.
De façon analogue au fichier known_hosts, le serveur consulte le fichier authorized_keys :
s'il contient la clé publique du client—préalablement ajoutée par un moyen hors bande—il
procède à la vérification de la signature.
Si elle passe, il considère réussie l'authentification du client.
Le client présente sa clé publique au serveur (et la signature)
Ici aussi, le client envoie en un seul paquet sa clé publique et la signature :
byte SSH_MSG_USERAUTH_REQUEST
string user name
string service name
string "publickey"
boolean TRUE
string public key algorithm name
string public key to be used for authentication
string signature
La signature portant, de nouveau, sur une combinaison d'informations liées à la session :
The value of 'signature' is a signature by the corresponding private
key over the following data, in the following order:
string session identifier
byte SSH_MSG_USERAUTH_REQUEST
string user name
string service name
string "publickey"
boolean TRUE
string public key algorithm name
string public key to be used for authentication
Comme l'indique l'extrait ci-dessous, le serveur, à la réception, accepte ou non la clé publique du client—selon
sa présence dans le fichier authorized_keys. S'il accepte, il vérifie la signature :
When the server receives this message, it MUST check whether the
supplied key is acceptable for authentication, and if so, it MUST
check whether the signature is correct.
Pour cette vérification, le serveur doit auparavant construire, lui aussi, de son côté, la combinaison d'informations donnée plus haut.
L'implémentation OpenSSH de la partie serveur le montre bien dans auth2-pubkey.c#L203 :
r = sshbuf_put_stringb(b, ssh->kex->session_id)
// …
/* reconstruct packet */
if ((r = sshbuf_put_u8(b, SSH2_MSG_USERAUTH_REQUEST)) != 0 ||
(r = sshbuf_put_cstring(b, userstyle)) != 0 ||
(r = sshbuf_put_cstring(b, authctxt->service)) != 0 ||
(r = sshbuf_put_cstring(b, method)) != 0 ||
(r = sshbuf_put_u8(b, have_sig)) != 0 ||
(r = sshbuf_put_cstring(b, pkalg)) != 0 ||
(r = sshbuf_put_string(b, pkblob, blen)) != 0)
fatal_fr(r, "reconstruct %s packet", method);
Plus loin, le code effectue la vérification proprement dite de la signature :
/* test for correct signature */
authenticated = 0;
if (mm_user_key_allowed(ssh, pw, key, 1, &authopts) &&
mm_sshkey_verify(key, sig, slen, sshbuf_ptr(b), sshbuf_len(b), /*…*/) {
authenticated = 1;
}
Concernant le debug OpenSSH côté client cela donne :
debug1: Authentications that can continue: publickey,password
debug1: Offering public key: /home/brindereseau/ssh/id_rsa RSA SHA256:gJYhpR37dnWvR3ajkx9e4nKQxtw13YsnUQXX3yLRHzE explicit
debug2: we sent a publickey packet, wait for reply
debug1: Server accepts key: /home/brindereseau/id_rsa RSA SHA256:gJYhpR37dnWvR3ajkx9e4nKQxtw13YsnUQXX3yLRHzE explicit
debug3: sign_and_send_pubkey: using publickey with RSA SHA256:gJYhpR37dnWvR3ajkx9e4nKQxtw13YsnUQXX3yLRHzE
debug3: sign_and_send_pubkey: signing using rsa-sha2-256 SHA256:gJYhpR37dnWvR3ajkx9e4nKQxtw13YsnUQXX3yLRHzE
Authenticated to 192.168.122.254 ([192.168.122.254]:22) using "publickey".
Parce que le paquet SSH_MSG_USERAUTH_REQUEST est chiffré sur le réseau,
nous ne pouvons pas le voir sur le capture, ni faire aussi facilement le calcul du petit exercice précédent.
Je garde cela pour un prochain article où nous jouerons avec Paramiko, une implémentation Python d'un client SSH. Modifier son code source permettra d'afficher les informations chiffrées, en particulier la signature (qui ne constitue pas une information sensible, rappelons-le).
Complément
Authentification par signature ou par clé publique ?
Ces deux noms désignent aujourd'hui, à ma connaissance, la même chose, c'est-à-dire la même méthode d'authentification. Le deuxième nom, public key authentication, est sans doute plus connu que le premier, signature-based authentication qui, pourtant, se veut plus correct car il précise le mécanisme employé pour l'authentification : la signature et non le chiffrement.
À proprement parler, nous devrions comparer signature- et encryption-based authentication, tous deux candidats pour réaliser l'authentification par clé publique.
Cela dit, la signature prévaut aujourd'hui sur le chiffrement, comme en témoigne par exemple l'évolution de SSHv1 vers SSHv2 :
Protocol 1 and protocol 2 keys are separated because of the differing
cryptographic usage: protocol 1 private RSA keys are used to decrypt
challenges that were encrypted with the corresponding public key,
whereas protocol 2 RSA private keys are used to sign challenges with
a private key for verification with the corresponding public key. It
is considered unsound practice to use the same key for signing and
encryption.
Cet extrait tiré de libopenssh, l'ancêtre d'OpenSSH, illustre bien le propos à mon sens.
Dans SSHv1, le serveur chiffrait un challenge avec la clé publique du client, préalablement ajoutée à son fichier authorized_keys.
Le client devait parvenir à le déchiffrer avec sa clé privée, puis le signifier auprès du serveur, afin que son authentification (auprès du serveur) soit réussie.
Cela se voit dans le fichier ssh-agent.c#L258 :
/* ssh1 only */
static void process_authentication_challenge1(SocketEntry *e) {
/* … */
/* Decrypt the challenge using the private key. */
if ((r = rsa_private_decrypt(challenge, challenge, private->rsa) != 0)) {
/* … */
/* The response is MD5 of decrypted challenge plus session id */
/* Send the response. */
if ((r = sshbuf_put_u8(msg, SSH_AGENT_RSA_RESPONSE)) != 0 ||
(r = sshbuf_put(msg, mdbuf, sizeof(mdbuf))) != 0)
Dans SSHv2, le client envoie une signature calculée avec sa clé privée sur un certain message. Cela se voit dans ssh-agent.c#L336 :
/* ssh2 only */
static void process_sign_request2(SocketEntry *e) {
/* … */
if ((ok = sshkey_sign(id->key, &signature, &slen, data, dlen, compat)) != 0)
/* … */
if ((r = sshbuf_put_u8(msg, SSH2_AGENT_SIGN_RESPONSE)) != 0 ||
(r = sshbuf_put_string(msg, signature, slen)) != 0)
Le serveur vérifie la signature avec la clé publique du client préalablement ajoutée à son fichier authorized_keys.
S'il parvient à vérifier la signature, l'authentification du client est réussie.
pour toute question (ou erreur) sur un article.