Un brin de réseau


Ce blog propose des articles sur les technologies réseaux et leur utilisation, le tout illustré avec des maquettes et des captures.

Python freeradius-api Python diffplus

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 :

  1. A envoie sa clé publique à B
  2. B accepte ou non la clé publique de A. S'il accepte :
  3. A envoie une signature calculée avec sa clé privée sur un message spécifique (connu de B)
  4. B vérifie la signature avec la clé publique de A : 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 :

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 :

rsa-ssh-1

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).

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 :

rsa-ssh-2

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).

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 :

rsa-ssh-3

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 :

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.

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 :

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.