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

Calcul manuel des signatures dans SSH

Cet article complète la signature-based authentication appliquée à SSH.

Nous vérifions, par le calcul, les signatures envoyées par le serveur et le client, utilisées pour l'authentification par clé publique entre eux.

L'exercice se voulant bas niveau, il ne faut donc pas craindre ici la manipulation d'octets.

Lab en place

Côté client

Je m'appuie sur Paramiko, une implémentation Python d'un client SSH qui rend plus pratique la compréhension et la modification du code, comparé à une implémentation C comme OpenSSH qui demanderait une recompilation.

Ce bout de code permet au client de se connecter par clé publique (par signature) au serveur :


    from paramiko import SSHClient
    from paramiko.util import log_to_file

    log_to_file("paramiko.log")
    client = SSHClient()
    client.load_system_host_keys()
    client.connect(
        "192.168.122.254",
        key_filename="id_rsa",
        allow_agent=False,
        username="brindereseau",
    )
    client.close()
    

J'ai auparavant généré une paire de clés RSA de 2048 bits avec l'utilitaire d'OpenSSH :


    $ ssh-keygen -t rsa -b 2048 -C "lab@brindereseau.fr" -f ./id_rsa
    Generating public/private rsa key pair.
    Enter passphrase (empty for no passphrase):
    Enter same passphrase again:
    Your identification has been saved in ./id_rsa
    Your public key has been saved in ./id_rsa.pub
    The key fingerprint is:
    SHA256:KadaOn9/GemfoiZg6xYZARI5uMLF++NXIR+tS8+XHjQ lab@brindereseau.fr
    The key's randomart image is:
    +---[RSA 2048]----+
    | .+o..           |
    |. o+  .          |
    |.....  .  .      |
    |o. .  .. + .     |
    |.   . .oS +  E   |
    |     o=+ =  + .  |
    |    ..++o +. +.  |
    |    .=o.o..o=o.. |
    |    o=+. +ooo+o  |
    +----[SHA256]-----+
    

Cela a engendré la création du fichier id_rsa.pub :


    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDhZQ55ImnypiQ+kz6OXyQp/kPqBSFlP2IionTvRwGsbDtqXqSC1lxdocmsr+IYcmqqfhNbeK/WyWDmWxN6PDw3/88K7xtrLXhEXoVc3A+szj/Y+ZGjkrZmhuLQKfSVqLXgzcZaTTZ2O4RUHsf7zcgl4vdpKPR/tvDMcw7mVhpDJmnD16yNcDjy7lcU140df2tjEsuGb3Rxo2t601nHJuWHEkBRUXidiaqa9vgBzkwGcEUvHbQwu1IrPTd2/sYS4TpaMIYprOgJAQ4RPg10VHG2VJQ8IdHGV8wBCQ7nXz/Goo6Evz+CKyzgn1jGnTvpkF44tkNc+u6V9cvRUjqAnPZ1 lab@brindereseau.fr
    

Ainsi que la création du fichier id_rsa qui contient la clé privée :


    $ cat id_rsa
    -----BEGIN OPENSSH PRIVATE KEY-----
    b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
    NhAAAAAwEAAQAAAQEA4WUOeSJp8qYkPpM+jl8kKf5D6gUhZT9iIqJ070cBrGw7al6kgtZc
    XaHJrK/iGHJqqn4TW3iv1slg5lsTejw8N//PCu8bay14RF6FXNwPrM4/2PmRo5K2Zobi0C
    n0lai14M3GWk02djuEVB7H+83IJeL3aSj0f7bwzHMO5lYaQyZpw9esjXA48u5XFNeNHX9r
    YxLLhm90caNretNZxyblhxJAUVF4nYmqmvb4Ac5MBnBFLx20MLtSKz03dv7GEuE6WjCGKa
    zoCQEOET4NdFRxtlSUPCHRxlfMAQkO518/xqKOhL8/giss4J9Yxp076ZBeOLZDXPrulfXL
    0VI6gJz2dQAAA9C82AF1vNgBdQAAAAdzc2gtcnNhAAABAQDhZQ55ImnypiQ+kz6OXyQp/k
    PqBSFlP2IionTvRwGsbDtqXqSC1lxdocmsr+IYcmqqfhNbeK/WyWDmWxN6PDw3/88K7xtr
    LXhEXoVc3A+szj/Y+ZGjkrZmhuLQKfSVqLXgzcZaTTZ2O4RUHsf7zcgl4vdpKPR/tvDMcw
    7mVhpDJmnD16yNcDjy7lcU140df2tjEsuGb3Rxo2t601nHJuWHEkBRUXidiaqa9vgBzkwG
    cEUvHbQwu1IrPTd2/sYS4TpaMIYprOgJAQ4RPg10VHG2VJQ8IdHGV8wBCQ7nXz/Goo6Evz
    +CKyzgn1jGnTvpkF44tkNc+u6V9cvRUjqAnPZ1AAAAAwEAAQAAAQAg4Lyae2Rgfo8xaIma
    u3KbRIl0EMEFE5iVTETJ5X3vQI9vLfSJ2Ep7Zv7z12kf30rDaWYZ9PITXucpWvYtoa04Dv
    LM2cGSYfzV3kLOX5RSxPgnxonRxjQgowLhUglpLkWvj9yj7fjoiLh+C8popuUP77pexthZ
    a95WuF7fRwaILkNhzzGGhpCZMsdh88o3K+hK5yhV1a6nx8nrJ++E7AGbSV7IxKF3liVhMZ
    f4UTJ7XmZsUYKFnTcxnGjAwyFZl0nVPXWkccWZ1wEG9IdSvD1wky8GBDgkBYV22hzuEPHq
    jIcPHUaEKMZLu13VfGZm963uqSJcKCtX8ps3//+ZXJBJAAAAgQC0WIC1zlRlphz1fRkb30
    39KAtKeMYCncp35FHYTrWpMLhZuCmfpI1pBlus3tbNjeM4QmWb1wGG+fC46ZG6JfH4591Z
    GRYxY2p3As2LSgkCCahonB3AeNA+pRTYgnUuE3ZBchZ5+5bzbXQQJLiEHlT3ItfuZxbvpm
    K1Yc+hZEJAYwAAAIEA5lt7dO2U4nsusWLbRQ8g4leT93yRfPZvIoZf3JaJ20HcCGDme3j4
    oRuK8IqB4wEJgCTlRDq4bwweEjpc3P4dIdE375MssaWyLZ78vaR5QnUDwCr5L0zqnF84Xo
    Ts6xeOz8sPDvhvyBIjqzwij0sxveF/jVYo1hPRGXX1FPwlMqkAAACBAPp8J4+YFqZZWCvW
    kwQBUxu+umzVLB1ZAk9ljk9lIFK4OlxT+I5drSLEmXLU9cW4qbBrxXB/+8F4xlmhx5eWPQ
    06FEK+31R9vHJD8+0ytzWGwA8MiiA6fdzvdyH2b+T2Wc1zML6/1MEv4x3Sgh8wbuWwZHt+
    U9+IdTQBGYAS/pDtAAAAE2xhYkBicmluZGVyZXNlYXUuZnIBAgMEBQYH
    -----END OPENSSH PRIVATE KEY-----
    

Convertir le format ci-dessus, propre à OpenSSH, au format standard PKCS #8 (RFC 7468) permet de le parser avec OpenSSL :


    $ cp id_rsa id_rsa.bak
    $ ssh-keygen -p -m PKCS8 -f id_rsa
    $ cat id_rsa
    -----BEGIN PRIVATE KEY-----
    MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDhZQ55ImnypiQ+
    kz6OXyQp/kPqBSFlP2IionTvRwGsbDtqXqSC1lxdocmsr+IYcmqqfhNbeK/WyWDm
    WxN6PDw3/88K7xtrLXhEXoVc3A+szj/Y+ZGjkrZmhuLQKfSVqLXgzcZaTTZ2O4RU
    Hsf7zcgl4vdpKPR/tvDMcw7mVhpDJmnD16yNcDjy7lcU140df2tjEsuGb3Rxo2t6
    01nHJuWHEkBRUXidiaqa9vgBzkwGcEUvHbQwu1IrPTd2/sYS4TpaMIYprOgJAQ4R
    Pg10VHG2VJQ8IdHGV8wBCQ7nXz/Goo6Evz+CKyzgn1jGnTvpkF44tkNc+u6V9cvR
    UjqAnPZ1AgMBAAECggEAIOC8mntkYH6PMWiJmrtym0SJdBDBBROYlUxEyeV970CP
    by30idhKe2b+89dpH99Kw2lmGfTyE17nKVr2LaGtOA7yzNnBkmH81d5Czl+UUsT4
    J8aJ0cY0IKMC4VIJaS5Fr4/co+346Ii4fgvKaKblD++6XsbYWWveVrhe30cGiC5D
    Yc8xhoaQmTLHYfPKNyvoSucoVdWup8fJ6yfvhOwBm0leyMShd5YlYTGX+FEye15m
    bFGChZ03MZxowMMhWZdJ1T11pHHFmdcBBvSHUrw9cJMvBgQ4JAWFdtoc7hDx6oyH
    Dx1GhCjGS7td1XxmZvet7qkiXCgrV/KbN///mVyQSQKBgQDmW3t07ZTiey6xYttF
    DyDiV5P3fJF89m8ihl/clonbQdwIYOZ7ePihG4rwioHjAQmAJOVEOrhvDB4SOlzc
    /h0h0TfvkyyxpbItnvy9pHlCdQPAKvkvTOqcXzhehOzrF47Pyw8O+G/IEiOrPCKP
    SzG94X+NVijWE9EZdfUU/CUyqQKBgQD6fCePmBamWVgr1pMEAVMbvrps1SwdWQJP
    ZY5PZSBSuDpcU/iOXa0ixJly1PXFuKmwa8Vwf/vBeMZZoceXlj0NOhRCvt9Ufbxy
    Q/PtMrc1hsAPDIogOn3c73ch9m/k9lnNczC+v9TBL+Md0oIfMG7lsGR7flPfiHU0
    ARmAEv6Q7QKBgAcnIg9AbVYXAx0o96wOSzQcChEuQgpCULMevw1Hc2JmiiGMeLuu
    xvGhvE+5zUyyNIxRGPlkZWO1WZ2xSD7oeRkauQTsaf/eKEk4XZq557YDkik+bFxm
    pAZVApgUwpKOObYEFSSe3EG3JnpjtKMEb7f4r5BA86WqGd0Th5euOK15AoGBAJ77
    RjDBmz6h3nCUlGMqZBFsEO8arhFCeVDjaFvEF6vo+kG3mj7h/g3fMnXL7OI9vpYX
    EQ3CbYvymBIzuHbiCIXoowtqEl2SIJV1w9B9LANxL11d3B1wgopEAx0vSP3Nzlm0
    DvBC2Up0lnZLMgORvhFSz7QCDkIGXj1PpVvAsopRAoGBALRYgLXOVGWmHPV9GRvf
    Tf0oC0p4xgKdynfkUdhOtakwuFm4KZ+kjWkGW6ze1s2N4zhCZZvXAYb58Ljpkbol
    8fjn3VkZFjFjancCzYtKCQIJqGicHcB40D6lFNiCdS4TdkFyFnn7lvNtdBAkuIQe
    VPci1+5nFu+mYrVhz6FkQkBj
    -----END PRIVATE KEY-----
            

    $ openssl rsa -in id_rsa -text -noout
    Private-Key: (2048 bit, 2 primes)
    modulus:
        00:e1:65:0e:79:22:69:f2:a6:24:3e:93:3e:8e:5f:
        24:29:fe:43:ea:05:21:65:3f:62:22:a2:74:ef:47:
        01:ac:6c:3b:6a:5e:a4:82:d6:5c:5d:a1:c9:ac:af:
        e2:18:72:6a:aa:7e:13:5b:78:af:d6:c9:60:e6:5b:
        13:7a:3c:3c:37:ff:cf:0a:ef:1b:6b:2d:78:44:5e:
        85:5c:dc:0f:ac:ce:3f:d8:f9:91:a3:92:b6:66:86:
        e2:d0:29:f4:95:a8:b5:e0:cd:c6:5a:4d:36:76:3b:
        84:54:1e:c7:fb:cd:c8:25:e2:f7:69:28:f4:7f:b6:
        f0:cc:73:0e:e6:56:1a:43:26:69:c3:d7:ac:8d:70:
        38:f2:ee:57:14:d7:8d:1d:7f:6b:63:12:cb:86:6f:
        74:71:a3:6b:7a:d3:59:c7:26:e5:87:12:40:51:51:
        78:9d:89:aa:9a:f6:f8:01:ce:4c:06:70:45:2f:1d:
        b4:30:bb:52:2b:3d:37:76:fe:c6:12:e1:3a:5a:30:
        86:29:ac:e8:09:01:0e:11:3e:0d:74:54:71:b6:54:
        94:3c:21:d1:c6:57:cc:01:09:0e:e7:5f:3f:c6:a2:
        8e:84:bf:3f:82:2b:2c:e0:9f:58:c6:9d:3b:e9:90:
        5e:38:b6:43:5c:fa:ee:95:f5:cb:d1:52:3a:80:9c:
        f6:75
    publicExponent: 65537 (0x10001)
    privateExponent:
        20:e0:bc:9a:7b:64:60:7e:8f:31:68:89:9a:bb:72:
        9b:44:89:74:10:c1:05:13:98:95:4c:44:c9:e5:7d:
        ef:40:8f:6f:2d:f4:89:d8:4a:7b:66:fe:f3:d7:69:
        1f:df:4a:c3:69:66:19:f4:f2:13:5e:e7:29:5a:f6:
        2d:a1:ad:38:0e:f2:cc:d9:c1:92:61:fc:d5:de:42:
        ce:5f:94:52:c4:f8:27:c6:89:d1:c6:34:20:a3:02:
        e1:52:09:69:2e:45:af:8f:dc:a3:ed:f8:e8:88:b8:
        7e:0b:ca:68:a6:e5:0f:ef:ba:5e:c6:d8:59:6b:de:
        56:b8:5e:df:47:06:88:2e:43:61:cf:31:86:86:90:
        99:32:c7:61:f3:ca:37:2b:e8:4a:e7:28:55:d5:ae:
        a7:c7:c9:eb:27:ef:84:ec:01:9b:49:5e:c8:c4:a1:
        77:96:25:61:31:97:f8:51:32:7b:5e:66:6c:51:82:
        85:9d:37:31:9c:68:c0:c3:21:59:97:49:d5:3d:75:
        a4:71:c5:99:d7:01:06:f4:87:52:bc:3d:70:93:2f:
        06:04:38:24:05:85:76:da:1c:ee:10:f1:ea:8c:87:
        0f:1d:46:84:28:c6:4b:bb:5d:d5:7c:66:66:f7:ad:
        ee:a9:22:5c:28:2b:57:f2:9b:37:ff:ff:99:5c:90:
        49
    prime1:
        00:e6:5b:7b:74:ed:94:e2:7b:2e:b1:62:db:45:0f:
        20:e2:57:93:f7:7c:91:7c:f6:6f:22:86:5f:dc:96:
        89:db:41:dc:08:60:e6:7b:78:f8:a1:1b:8a:f0:8a:
        81:e3:01:09:80:24:e5:44:3a:b8:6f:0c:1e:12:3a:
        5c:dc:fe:1d:21:d1:37:ef:93:2c:b1:a5:b2:2d:9e:
        fc:bd:a4:79:42:75:03:c0:2a:f9:2f:4c:ea:9c:5f:
        38:5e:84:ec:eb:17:8e:cf:cb:0f:0e:f8:6f:c8:12:
        23:ab:3c:22:8f:4b:31:bd:e1:7f:8d:56:28:d6:13:
        d1:19:75:f5:14:fc:25:32:a9
    prime2:
        00:fa:7c:27:8f:98:16:a6:59:58:2b:d6:93:04:01:
        53:1b:be:ba:6c:d5:2c:1d:59:02:4f:65:8e:4f:65:
        20:52:b8:3a:5c:53:f8:8e:5d:ad:22:c4:99:72:d4:
        f5:c5:b8:a9:b0:6b:c5:70:7f:fb:c1:78:c6:59:a1:
        c7:97:96:3d:0d:3a:14:42:be:df:54:7d:bc:72:43:
        f3:ed:32:b7:35:86:c0:0f:0c:8a:20:3a:7d:dc:ef:
        77:21:f6:6f:e4:f6:59:cd:73:30:be:bf:d4:c1:2f:
        e3:1d:d2:82:1f:30:6e:e5:b0:64:7b:7e:53:df:88:
        75:34:01:19:80:12:fe:90:ed
    exponent1:
        07:27:22:0f:40:6d:56:17:03:1d:28:f7:ac:0e:4b:
        34:1c:0a:11:2e:42:0a:42:50:b3:1e:bf:0d:47:73:
        62:66:8a:21:8c:78:bb:ae:c6:f1:a1:bc:4f:b9:cd:
        4c:b2:34:8c:51:18:f9:64:65:63:b5:59:9d:b1:48:
        3e:e8:79:19:1a:b9:04:ec:69:ff:de:28:49:38:5d:
        9a:b9:e7:b6:03:92:29:3e:6c:5c:66:a4:06:55:02:
        98:14:c2:92:8e:39:b6:04:15:24:9e:dc:41:b7:26:
        7a:63:b4:a3:04:6f:b7:f8:af:90:40:f3:a5:aa:19:
        dd:13:87:97:ae:38:ad:79
    exponent2:
        00:9e:fb:46:30:c1:9b:3e:a1:de:70:94:94:63:2a:
        64:11:6c:10:ef:1a:ae:11:42:79:50:e3:68:5b:c4:
        17:ab:e8:fa:41:b7:9a:3e:e1:fe:0d:df:32:75:cb:
        ec:e2:3d:be:96:17:11:0d:c2:6d:8b:f2:98:12:33:
        b8:76:e2:08:85:e8:a3:0b:6a:12:5d:92:20:95:75:
        c3:d0:7d:2c:03:71:2f:5d:5d:dc:1d:70:82:8a:44:
        03:1d:2f:48:fd:cd:ce:59:b4:0e:f0:42:d9:4a:74:
        96:76:4b:32:03:91:be:11:52:cf:b4:02:0e:42:06:
        5e:3d:4f:a5:5b:c0:b2:8a:51
    coefficient:
        00:b4:58:80:b5:ce:54:65:a6:1c:f5:7d:19:1b:df:
        4d:fd:28:0b:4a:78:c6:02:9d:ca:77:e4:51:d8:4e:
        b5:a9:30:b8:59:b8:29:9f:a4:8d:69:06:5b:ac:de:
        d6:cd:8d:e3:38:42:65:9b:d7:01:86:f9:f0:b8:e9:
        91:ba:25:f1:f8:e7:dd:59:19:16:31:63:6a:77:02:
        cd:8b:4a:09:02:09:a8:68:9c:1d:c0:78:d0:3e:a5:
        14:d8:82:75:2e:13:76:41:72:16:79:fb:96:f3:6d:
        74:10:24:b8:84:1e:54:f7:22:d7:ee:67:16:ef:a6:
        62:b5:61:cf:a1:64:42:40:63
            

La description des paramètres se trouve dans la RFC 8017 :


    RSAPrivateKey ::= SEQUENCE {
        version           Version,
        modulus           INTEGER,  -- n
        publicExponent    INTEGER,  -- e
        privateExponent   INTEGER,  -- d
        prime1            INTEGER,  -- p
        prime2            INTEGER,  -- q
        exponent1         INTEGER,  -- d mod (p-1)
        exponent2         INTEGER,  -- d mod (q-1)
        coefficient       INTEGER,  -- (inverse of q) mod p
        otherPrimeInfos   OtherPrimeInfos OPTIONAL
    }
            

Nous retrouvons là les différents paramètres du cryptosystème RSA. Enfin, je remets le format OpenSSH, Paramiko ne supportant pas le format PKCS #8 :


    $ mv id_rsa.bak id_rsa
            

Côté serveur

Un routeur MikroTik, sur lequel j'ai ajouté la clé publique du client, assure le rôle de serveur :


    [admin@MikroTik] > ip/address/print
    Columns: ADDRESS, NETWORK, INTERFACE
    # ADDRESS             NETWORK        INTERFACE
    0 192.168.122.254/24  192.168.122.0  ether1

    [admin@MikroTik] > user/add name=brindereseau group=full
    password: *****
    [admin@MikroTik] > user/ssh-keys/import user=brindereseau public-key-file=id_rsa.pub
    [admin@MikroTik] > user/ssh-keys/print
    Columns: USER, KEY-TYPE, BITS, KEY-OWNER
    #  USER          KEY-TYPE  BITS  KEY-OWNER
    0  brindereseau  rsa       2048  lab@brindereseau.fr
    

Lancement de la connexion

Je lance le script Python précédent et affiche les logs Paramiko :


    DEB [20260930-10:09:28.846] thr=2   paramiko.transport: === Key exchange agreements ===
    DEB [20260930-10:09:28.846] thr=2   paramiko.transport: Kex: diffie-hellman-group-exchange-sha256
    DEB [20260930-10:09:28.846] thr=2   paramiko.transport: HostKey: rsa-sha2-256
    DEB [20260930-10:09:28.846] thr=2   paramiko.transport: Cipher: aes192-ctr
    DEB [20260930-10:09:28.846] thr=2   paramiko.transport: MAC: hmac-sha2-256
    DEB [20260930-10:09:28.846] thr=2   paramiko.transport: Compression: none
    DEB [20260930-10:09:28.846] thr=2   paramiko.transport: === End of kex handshake ===
    DEB [20260930-10:09:29.185] thr=2   paramiko.transport: Agreed upon 'rsa-sha2-256' pubkey algorithm
    INF [20260930-10:09:29.190] thr=2   paramiko.transport: Authentication (publickey) successful!
    

Nous retrouvons les paramètres cryptographiques négociés entre les parties, en particulier :

Authentification du serveur auprès du client

Le serveur fournit sa clé publique et la signature

Le serveur fournit sa clé publique et la signature dans le paquet SSH_MSG_KEX_DH_GEX_REPLY :

cap-rsa-signature-ssh-exercice-1

La RFC 8332 décrit l'encodage de la signature :


    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.
    

Le champ rsa_signature_blob est de type string dont la RFC 4251 précise l'encodage : la valeur est précédée de sa longueur codée sur 4 octets.

Par conséquent, pour obtenir la valeur de la signature, il faut retirer les 4 premiers octets 0x00000100 qui donnent la taille de la signature, soit 256 octets ici (taille du module \(n\)).

Autrement dit, la valeur de la signature commence par 0x83e5319fd23f27f82421…

Le client vérifie la signature

Nous considérons que le client a déjà accepté la clé publique du serveur dans son fichier known_hosts lors d'une connexion précédente au serveur.

Il vérifie maintenant la signature reçue en appliquant l'opération verify du schéma de signature \(\text{RSASSA-PKCS1-v1\_5}\) (RFC 8017) résumée ci-dessous en pseudocode :


    RSASSA-PKCS1-V1_5-VERIFY ((n, e), M, S)
      s = OS2IP (S)
      em = RSAVP1 ((n, e), s)
      EM = I2OSP (em, k)
      EM_ = EMSA-PKCS1-V1_5-ENCODE (M, k)
      if EM == EM_
        output "valid signature"
      else
        output "invalid signature"
    

Globalement, cela consiste pour le client à calculer puis comparer \(EM\) et \(EM\_\).

Cette opération prend en entrée :

Chaque partie, le serveur et le client, construit localement et à l'identique le message \(M\), non échangé sur le réseau. La prochaine section le reconstruit côté client.

Le document précise que la signature est codée sur le même nombre d'octets que le module \(n\) :


    S     signature to be verified, an octet string of length k,
          where k is the length in octets of the RSA modulus n
    

La taille de la clé étant de 2048 bits ici, alors \(k = 256\) octets. La méthode EMSA-PKCS1-V1_5-ENCODE prendra cette valeur en paramètre d'entrée.

Le client calcule \(EM\)

À ce stade, nous pouvons déjà exécuter les premières instructions de l'opération et calculer \(EM\) :


    def RSAVP1(n: int, e: int, s: int) -> int:
        return pow(s, e, n)  # s^e mod n

    def I2OSP(i: int, i_len: int) -> bytes:
        return int.to_bytes(i, byteorder="big", length=i_len)

    n = 0xd8de3d67b9ba8623682df6875487d1a74457fc1b9c4767aa7f963b5a526017a8500d608ca975d038059e0ef00eb440565b9967c7a267ea5b3c7e200543864afc2ef532c8fca22b487b5adf3d1a9b86479d035923768d9d4cd0014a302cc4bb06567c9e779889f6403d8f7bd767a1802367e2867f5435f2caaee3252e430aa69934b74b0ecdfa89af802e3ee118d978e763f52a1a4e6751102347c21a8b305cd5530e301d16aa1a59e951a9427c45f9573acea6fcb42d0413e418f715293d1edcb16344b76559106a7d036d105b2d497d527fe8fa473b1fc488de277a1267acd6d947c9e2f10337975e13db6dd10f7bf29d8d5f3df7b28c07e27965393fb5c8e3
    e = 0x010001
    s = 0x83e5319fd23f27f8242190bd94e566b48bdcffe4b368311a7ae924f665a1d64e0156310eb3c7545896200003adae9a9b15c65263b21922cb4d98a97818594a43bb6ae0c076bb064031ea47e08b422f671298761e8503488acccc03b75359e03d8c54923167d121a7ff7fa4811d76e4f11a0452a5b9c46d84b78a8dff06ce9579657ebb16c9723907573146ebb4ae6bb32897cb9afa4346768945163a9f8dacbcdf09888960401fc3611e322d9b70ec85261d149858f8450301390b6cfce099fef45932e893081dba707dd918dc2de6c9e1cca9bf57131a10527537954abb41d9bde00e74d660cf919db962f1ce7e23ea1741c74a36f5f4abbe2f4af4b1629634
    k = 256  # taille du module n en octets
    em = RSAVP1(n, e, s)
    EM = I2OSP(em, k)
    print(EM.hex())
    

L'exécution du script donne :


    0001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    ffffffffffffffffffffffff003031300d060960864801650304020105000420
    dceaa744407a7fba4f2ab68e195b7d37500073912a6cdeb4cc241196867a14a7
    

Je n'ai pas appelé dans le script la primitive de conversion \(\text{OS2IP}\) sur \(S\) car Python nous permet d'utiliser directement sa forme hexadécimale issue de la capture (comme pour \(n\) et \(e\)). En effet :


    e = 0x010001
    print(e)
    # 65537

    e = 65537
    print(e)
    # 65537
    
Le client construit \(M\)

Afin de comparer le \(EM\) obtenu à sa version \(EM\_\) calculée, le client construit auparavant la même concaténation d'informations que le serveur :


    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
    

Il en calcule le hash, SHA-256 ici du fait de la méthode KEX négociée diffie-hellman-group-exchange-sha256 entre les parties, ce qui donne le message \(M\).

Si construire cette concaténation d'informations à la main est possible, je me contente ici, pour la simplicité, de récupèrer la valeur depuis Paramiko en modifiant son code source.

Paramiko construit cette concaténation dans la variable hm du fichier kex_gex.py#L259 :


    # okay, build up the hash H of
    # (V_C || V_S || I_C || I_S || K_S || min || n || max || p || g || e || f || K)  # noqa
    hm = Message()
    hm.add(
        self.transport.local_version,
        self.transport.remote_version,
        self.transport.local_kex_init,
        self.transport.remote_kex_init,
        host_key,
    )
    if not self.old_style:
        hm.add_int(self.min_bits)
    hm.add_int(self.preferred_bits)
    if not self.old_style:
        hm.add_int(self.max_bits)
    hm.add_mpint(self.p)
    hm.add_mpint(self.g)
    hm.add_mpint(self.e)
    hm.add_mpint(self.f)
    hm.add_mpint(K)
    

J'ajoute juste ceci à la fin du code précédent (affichage de hm et calcul du hash SHA-256) :


    print("hm =", bytes(hm).hex())
    H = sha256()
    H.update(bytes(hm))
    print("H =", H.hexdigest())
    

Ce qui donne à l'exécution :


    hm = 000000165353482d322e302d706172616d696b6f5f342e302e300000000e5353482d322e302d524f53535348000004da14adcc13acfe7eac3ae3cfa6c9f7b579f100000138637572766532353531392d736861323536406c69627373682e6f72672c656364682d736861322d6e697374703235362c656364682d736861322d6e697374703338342c656364682d736861322d6e697374703532312c6469666669652d68656c6c6d616e2d67726f757031362d7368613531322c6469666669652d68656c6c6d616e2d67726f75702d65786368616e67652d7368613235362c6469666669652d68656c6c6d616e2d67726f757031342d7368613235362c6469666669652d68656c6c6d616e2d67726f75702d65786368616e67652d736861312c6469666669652d68656c6c6d616e2d67726f757031342d736861312c6469666669652d68656c6c6d616e2d67726f7570312d736861312c6578742d696e666f2d632c6b65782d7374726963742d632d763030406f70656e7373682e636f6d000001667373682d7273612c7373682d656432353531392c65636473612d736861322d6e697374703235362c65636473612d736861322d6e697374703338342c65636473612d736861322d6e697374703532312c7273612d736861322d3531322c7273612d736861322d3235362c7373682d7273612d636572742d763031406f70656e7373682e636f6d2c7373682d656432353531392d636572742d763031406f70656e7373682e636f6d2c65636473612d736861322d6e697374703235362d636572742d763031406f70656e7373682e636f6d2c65636473612d736861322d6e697374703338342d636572742d763031406f70656e7373682e636f6d2c65636473612d736861322d6e697374703532312d636572742d763031406f70656e7373682e636f6d2c7273612d736861322d3531322d636572742d763031406f70656e7373682e636f6d2c7273612d736861322d3235362d636572742d763031406f70656e7373682e636f6d000000786165733132382d6374722c6165733139322d6374722c6165733235362d6374722c6165733132382d6362632c6165733139322d6362632c6165733235362d6362632c336465732d6362632c6165733132382d67636d406f70656e7373682e636f6d2c6165733235362d67636d406f70656e7373682e636f6d000000786165733132382d6374722c6165733139322d6374722c6165733235362d6374722c6165733132382d6362632c6165733139322d6362632c6165733235362d6362632c336465732d6362632c6165733132382d67636d406f70656e7373682e636f6d2c6165733235362d67636d406f70656e7373682e636f6d00000083686d61632d736861322d3235362c686d61632d736861322d3531322c686d61632d736861322d3235362d65746d406f70656e7373682e636f6d2c686d61632d736861322d3531322d65746d406f70656e7373682e636f6d2c686d61632d736861312c686d61632d6d64352c686d61632d736861312d39362c686d61632d6d64352d393600000083686d61632d736861322d3235362c686d61632d736861322d3531322c686d61632d736861322d3235362d65746d406f70656e7373682e636f6d2c686d61632d736861322d3531322d65746d406f70656e7373682e636f6d2c686d61632d736861312c686d61632d6d64352c686d61632d736861312d39362c686d61632d6d64352d3936000000046e6f6e65000000046e6f6e650000000000000000000000000000000121143197c608807b71835bd56af6980ae19a00000041637572766532353531392d7368613235362c6469666669652d68656c6c6d616e2d67726f75702d65786368616e67652d7368613235362c6578742d696e666f2d730000000c7273612d736861322d3235360000002c6165733139322d6374722c6165733235362d6374722c6165733235362d67636d406f70656e7373682e636f6d0000002c6165733139322d6374722c6165733235362d6374722c6165733235362d67636d406f70656e7373682e636f6d0000001b686d61632d736861322d3235362c686d61632d736861322d3531320000001b686d61632d736861322d3235362c686d61632d736861322d353132000000046e6f6e65000000046e6f6e650000000000000000000000000000000117000000077373682d727361000000030100010000010100d8de3d67b9ba8623682df6875487d1a74457fc1b9c4767aa7f963b5a526017a8500d608ca975d038059e0ef00eb440565b9967c7a267ea5b3c7e200543864afc2ef532c8fca22b487b5adf3d1a9b86479d035923768d9d4cd0014a302cc4bb06567c9e779889f6403d8f7bd767a1802367e2867f5435f2caaee3252e430aa69934b74b0ecdfa89af802e3ee118d978e763f52a1a4e6751102347c21a8b305cd5530e301d16aa1a59e951a9427c45f9573acea6fcb42d0413e418f715293d1edcb16344b76559106a7d036d105b2d497d527fe8fa473b1fc488de277a1267acd6d947c9e2f10337975e13db6dd10f7bf29d8d5f3df7b28c07e27965393fb5c8e3000004000000080000002000000001010083da238b86f5b7f7477bcb1b22d1f5291d36678ad75764a8e6f4fd90e0efa544875a7fafe00a1b6abbadb4f34ae807f79e2a760ec6823d608a88d3e61a8ee0610f0c55a5c87183f02e69f9e21819c36eaf4fbfc2d5c7f42f2bee05d9745391e90e1f97857d97579a2b0e88c17fdfa6c49ab9fb6c17d459e0aa20bdff79a5266619694bf87b5eab2f6e80fd5a7036bdac0caf5d01357324612e8503ffa4b3bb616b20d074f0e07932c12dab03f22cd81c58384106f25d4dca608b42b98a2aae8cfcd96124d1baa4afc5a50113055019c1a294b300855ccf25af3b256bb8307730beb41b14226622eadff97d984cd9cef29c0feddc1fbcdae2db57df0d5cfe2afb00000001020000010009af968bad12a5ada074cdede0f67f5f851b34ec7ae07de86b6cb3e15e933439188ad1eb9d14ae626e1ca2ed412a931d6c163258093245a06fdd54f4739f2ba77a29bbb228bab7f8c7ec8dab7099efe9a67c2e951d40d7b6c6830bad02ab88138fe8f9aba53d40d5c62e292ebdca15975a24b094f31b5508ad389eca14cb47ec4506a77e9003767a027517f78035cc14e2dcdc3986bdd7966f94a51885550c9edcacd4441bd7b52497a3077bb822db3d5544df51116dde353c356d7bddcfc9f277d065f907a71415d031faaee7b9bf7d72bd8ba1bff6142d6f2b4338a8bf2bf88922fa974d521248339190897bf2eeb837685fc7b3cd5ccaf0eff03f0b687a4f000001002f92195e10c9091793fb8d5966d981dd04834f56f97b3fffe9743c16169549ad435827fcfc417acb1e83d7235566bdbf88931ea6a328b07a7b1cd133364f11a0c40dd96f03bc6b11aef79034ba486471851bcdd363aa7955c7e5e8a56933eaea42eef3a0ad3edb5b9741f442e5685c3a1cceac96de9a787468519287cabcbba789322ca81f706f35d4ff41663d3d654b3c84123cd3577a44d7a95f84e2b34cc49cc6a7fc11028fdc3b89fc81bd8116f7803fd0e23c1947fe0a7264c6d4c43ef7d81ab1fab07086a12a833dfdb6df37680ab8092915f84fe44731ce391a6121132d04dbc217e5b42e5c65d6b3caceda509991bc30431244b16490eb0a59661903000001003a4b1090cd0ad0cb0e0de162ebbf47c416027a07071ccb3e2e660501a7f064c5e1b498ac9cae44e2e724ff6b15a4346d56f112eea915083eb0a47d9b8ac5dec720eb509503bf4b0e8aaeec7336c4a2ca9aa36fba251cb7162cf49da2ae37489cf364d454e000c7f7a3ed0fffd7aa4e3d9e0ced8555402e8837ce15f92a5706689c8f69d0a63cb79743f87c137f4fa6035995ef47c3129ade63de980d94c5dc7561a0aa9f66971978afb37c67bdf974f26d3115cd87907815139e902ac6a699cded4dcba460241d61d5959ceebefde2e72e4d29e441b405a1c6cede7fb1c09a3bed68f3631ad31f18dc0d7aa7353dffa117087b7d0898742d48aac68622a5118a
    H = e80d640db43aeb7515cf67a0103e71f2b68bef48c203e2f5a034da84d2a3083d
    

Pour la curiosité, décodons le début de hm dont les premiers champs sont :


    string  V_C, the client's version string (CR and NL excluded)
    string  V_S, the server's version string (CR and NL excluded)
    

Pour rappel, la RFC 4251 précise l'encodage du type string : la valeur est précédée de sa longueur codée sur 4 octets. Ce qui donne :


    print(0x00000016, "octets vont suivre")
    # 22 octets vont suivre

    print(bytes.fromhex("5353482d322e302d706172616d696b6f5f342e302e30"))
    # b'SSH-2.0-paramiko_4.0.0'

    print(0x0000000e, "octets vont suivre")
    # 14 octets vont suivre

    print(bytes.fromhex("5353482d322e302d524f53535348"))
    # b'SSH-2.0-ROSSSH'
    

Et ainsi de suite pour les autres champs.

Le client calcule \(EM\_\)

Le client exécute maintenant la méthode EMSA-PKCS1-V1_5-ENCODE pour calculer \(EM\_\) :


    EMSA-PKCS1-v1_5-ENCODE (M, emLen)

    Option:

       Hash     hash function (hLen denotes the length in octets of
                the hash function output)

    Input:

       M        message to be encoded
       emLen    intended length in octets of the encoded message, at
                least tLen + 11, where tLen is the octet length of the
                Distinguished Encoding Rules (DER) encoding T of
                a certain value computed during the encoding operation

     Output:

       EM       encoded message, an octet string of length emLen
    

La fonction hash sera SHA-256 du fait de l'algorithme d'authentification rsa-sha2-256 négocié.

Les paramètres d'entrée sont :

Nous appliquons ensuite ci-dessous rigoureusement les étapes de la RFC 8017 :


    1.  Apply the hash function to the message M to produce a hash
      value H:

         H = Hash(M).
    

Le client applique donc, de nouveau, un hash SHA-256 sur \(M\) :


    from hashlib import sha256
    H = sha256()
    H.update(bytes.fromhex("e80d640db43aeb7515cf67a0103e71f2b68bef48c203e2f5a034da84d2a3083d"))
    print(H.hexdigest())
    # dceaa744407a7fba4f2ab68e195b7d37500073912a6cdeb4cc241196867a14a7
    

Ensuite, il combine plusieurs informations selon une syntaxe ASN.1 encodée en DER, chose fréquente dans le domaine de la cryptographie comme le dit la RFC 6025 : « Abstract Syntax Notation One (ASN.1) is widely used throughout the IETF Security Area and has been for many years. »


    2.  Encode the algorithm ID for the hash function and the hash
        value into an ASN.1 value of type DigestInfo (see
        Appendix A.2.4) with the DER, where the type DigestInfo has
        the syntax

             DigestInfo ::= SEQUENCE {
                 digestAlgorithm AlgorithmIdentifier,
                 digest OCTET STRING
             }

        The first field identifies the hash function and the second
        contains the hash value.  Let T be the DER encoding of the
        DigestInfo value (see the notes below), and let tLen be the
        length in octets of T.
    

La RFC nous mâche le travail et donne l'encodage associé au hash SHA-256 :


    SHA-256: (0x)30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 || H.
    

La construction de T donne :


    H = "dceaa744407a7fba4f2ab68e195b7d37500073912a6cdeb4cc241196867a14a7"
    T = "3031300d060960864801650304020105000420" + H
    print(T)
    # T = 3031300d060960864801650304020105000420dceaa744407a7fba4f2ab68e195b7d37500073912a6cdeb4cc241196867a14a7
    

L'étape 3 consiste juste à vérifier une condition :


    3.  If emLen < tLen + 11, output "intended encoded message length too short" and stop.
    

emLen valant 256 octets et tLen, la longueur de T, valant 52 octets, la condition est respectée :


    emLen = 256
    tLen = 52

    print(emLen < (tLen + 11))
    # False
    

La quatrième étape correspond à la répétition du fameux motif 0xff, caractéristique inhérente au schéma de signature \(\text{RSASSA-PKCS1-v1\_5}\) :


    4.  Generate an octet string PS consisting of emLen - tLen - 3
        octets with hexadecimal value 0xff.  The length of PS will be
        at least 8 octets.
    

Le motif se répète ici \(256 - 52 - 3 = 201\) fois :


    emLen = 256
    tLen = 52
    PS = "ff" * int(emLen - tLen - 3)
    print(PS)
    # ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    

Enfin, la cinquième et dernière étape construit \(EM\) :


    5.  Concatenate PS, the DER encoding T, and other padding to form
        the encoded message EM as

           EM = 0x00 || 0x01 || PS || 0x00 || T.
    

Soit, si je reprends tous les éléments précédents :


    H = "dceaa744407a7fba4f2ab68e195b7d37500073912a6cdeb4cc241196867a14a7"
    T = "3031300d060960864801650304020105000420" + H
    emLen = 256
    tLen = len(T) / 2
    PS = "ff" * int(emLen - tLen - 3)
    EM = "00" + "01" + PS + "00" + T
    print(EM)
    # 0001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    # ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    # ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    # ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    # ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    # ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    # ffffffffffffffffffffffff003031300d060960864801650304020105000420
    # dceaa744407a7fba4f2ab68e195b7d37500073912a6cdeb4cc241196867a14a7
    
Comparaison de \(EM\) et \(EM\_\)

La valeur précédente est retournée à l'appelant RSASSA-PKCS1-V1_5-VERIFY dans la variable \(EM\_\) qui procède à la comparaison avec \(EM\). Si j'assemble les différents bouts de code :


    from hashlib import sha256

    def RSAVP1(n: int, e: int, s: int) -> int:
        return pow(s, e, n)  # s^e mod n

    def I2OSP(i: int, i_len: int) -> bytes:
        return int.to_bytes(i, byteorder="big", length=i_len)

    # fonctionne seulement pour l'algorithme "rsa-sha2-256"
    def EMSA_PKCS1_V1_5_ENCODE(M: bytes, emLen: int) -> bytes:
        # étape 1
        H = sha256()
        H.update(M)
        # étape 2
        T = "3031300d060960864801650304020105000420" + H.hexdigest()
        tLen = len(T) / 2
        # étape 3
        if emLen < (tLen + 11):
            raise ValueError("intended encoded message length too short")
        # étape 4
        PS = "ff" * int(emLen - tLen - 3)
        # étape 5
        EM = "00" + "01" + PS + "00" + T
        # étape 6
        return bytes.fromhex(EM)

    # calcul de EM
    n = 0xd8de3d67b9ba8623682df6875487d1a74457fc1b9c4767aa7f963b5a526017a8500d608ca975d038059e0ef00eb440565b9967c7a267ea5b3c7e200543864afc2ef532c8fca22b487b5adf3d1a9b86479d035923768d9d4cd0014a302cc4bb06567c9e779889f6403d8f7bd767a1802367e2867f5435f2caaee3252e430aa69934b74b0ecdfa89af802e3ee118d978e763f52a1a4e6751102347c21a8b305cd5530e301d16aa1a59e951a9427c45f9573acea6fcb42d0413e418f715293d1edcb16344b76559106a7d036d105b2d497d527fe8fa473b1fc488de277a1267acd6d947c9e2f10337975e13db6dd10f7bf29d8d5f3df7b28c07e27965393fb5c8e3
    e = 0x010001
    s = 0x83e5319fd23f27f8242190bd94e566b48bdcffe4b368311a7ae924f665a1d64e0156310eb3c7545896200003adae9a9b15c65263b21922cb4d98a97818594a43bb6ae0c076bb064031ea47e08b422f671298761e8503488acccc03b75359e03d8c54923167d121a7ff7fa4811d76e4f11a0452a5b9c46d84b78a8dff06ce9579657ebb16c9723907573146ebb4ae6bb32897cb9afa4346768945163a9f8dacbcdf09888960401fc3611e322d9b70ec85261d149858f8450301390b6cfce099fef45932e893081dba707dd918dc2de6c9e1cca9bf57131a10527537954abb41d9bde00e74d660cf919db962f1ce7e23ea1741c74a36f5f4abbe2f4af4b1629634
    k = 256  # taille du module n en octets
    em = RSAVP1(n, e, s)
    EM = I2OSP(em, k)

    # calcul de EM_
    EM_ = EMSA_PKCS1_V1_5_ENCODE(
        M=bytes.fromhex("e80d640db43aeb7515cf67a0103e71f2b68bef48c203e2f5a034da84d2a3083d"),
        emLen=k
    )

    # comparaison de EM et EM_
    if EM == EM_:
        print("valid signature")
    else:
        print("invalid signature")
    

Résultat à l'exécution :


    valid signature
    

La signature étant vérifiée, le client considère réussie l'authentification du serveur.

Reproduire ces constructions et calculs à la main donne une idée des implémentations sous-jacentes et démystifie le fonctionnement, concret, de la signature-based authentication.

Authentification du client auprès du serveur

Le client fournit sa clé publique et la signature

Le client fournit sa clé publique et la signature dans un même paquet MSG_USERAUTH_REQUEST, non visible en clair sur une capture car, une fois l'authentification du serveur auprès du client réussie, le trafic se veut chiffré.

Alors je modifie le code source de Paramiko pour afficher la signature envoyée au serveur, 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
    

Paramiko construit cette combinaison d'informations dans auth_handler.py#L187 :


    def _get_session_blob(self, key, service, username, algorithm):
        m = Message()
        m.add_string(self.transport.session_id)
        m.add_byte(cMSG_USERAUTH_REQUEST)
        m.add_string(username)
        m.add_string(service)
        m.add_string("publickey")
        m.add_boolean(True)
        _, bits = self._get_key_type_and_bits(key)
        m.add_string(algorithm)
        m.add_string(bits)
        return m.asbytes()
    

Le paquet MSG_USERAUTH_REQUEST est lui construit dans auth_handler.py#L327 :


    m = Message()
    m.add_byte(cMSG_USERAUTH_REQUEST)
    m.add_string(self.username)
    m.add_string("ssh-connection")
    m.add_string(self.auth_method)
    # …
    elif self.auth_method == "publickey":
        m.add_boolean(True)
        key_type, bits = self._get_key_type_and_bits(self.private_key)
        algorithm = self._finalize_pubkey_algorithm(key_type)
        m.add_string(algorithm)
        m.add_string(bits)
        blob = self._get_session_blob(
            self.private_key,
            "ssh-connection",
            self.username,
            algorithm,
        )
        sig = self.private_key.sign_ssh_data(blob, algorithm)
        m.add_string(sig)

        # modification du code source
        print("Auth algorithm sent is:", algorithm)
        print("Public key sent is:", bytes(bits).hex())
        print("Signature sent is:", bytes(sig).hex())
    

Je rajoute juste à la fin plusieurs print pour afficher les informations envoyées au serveur.


    Auth algorithm sent is: rsa-sha2-256
    Public key sent is: 000000077373682d727361000000030100010000010100e1650e792269f2a6243e933e8e5f2429fe43ea0521653f6222a274ef4701ac6c3b6a5ea482d65c5da1c9acafe218726aaa7e135b78afd6c960e65b137a3c3c37ffcf0aef1b6b2d78445e855cdc0facce3fd8f991a392b66686e2d029f495a8b5e0cdc65a4d36763b84541ec7fbcdc825e2f76928f47fb6f0cc730ee6561a432669c3d7ac8d7038f2ee5714d78d1d7f6b6312cb866f7471a36b7ad359c726e58712405151789d89aa9af6f801ce4c0670452f1db430bb522b3d3776fec612e13a5a308629ace809010e113e0d745471b654943c21d1c657cc01090ee75f3fc6a28e84bf3f822b2ce09f58c69d3be9905e38b6435cfaee95f5cbd1523a809cf675
    Signature sent is: 0000000c7273612d736861322d323536000001006d3d06d9b3ebe961111f69c8e8ff727311b9d03bcf5ca8b31bbca18a85c28895dcb0e3c474d01aecc620af5a9299418b545a6479724272a6f9b5095cb35e8481d302c61147dfbe9f804d5720230b1e62f73eaa643bb0f133badd6a21c2d6ab6b4dee90d70edd0c1385f0521c215889ad574c3e9b43f8f27a527a128eb2b1d1d1a18f5818217bb2d91e84914befaed19fe48abb4f98e0fce9f610cbab577bf27e3c91c69be0178c58df92d6dce0d6ddf287dce68c1ff48bc0105706410750a36fb572c462d41a88e3e919f77c1e0d46fedc28e4f839e43882bb384cbdb2bdbde2d4257147e733802f7c25648a63c69d32bebc1c48e955b1307f121f63c4dfea03
    

La clé publique est encodée conformément à la RFC 4253 :


    The "ssh-rsa" key format has the following specific encoding:

       string    "ssh-rsa"
       mpint     e
       mpint     n
    

Aussi, pour récupérer la valeur de \(n\), il faut enlever les octets correspondant au type de clé ssh-rsa et à l'exposant \(e\) :


    00 00 00 07           # 7 octets vont suivre (nombre exprimé lui-même sur 4 octets)
    73 73 68 2d 72 73 61  # codage ASCII de "ssh-rsa" sur 7 octets

    00 00 00 03           # 3 octets vont suivre
    01 00 01              # valeur de e en hexadécimal sur 3 octets

    00 00 01 01           # 257 octets vont suivre
    00                    # 1 octet pour le signe positif de n car son premier bit ne vaut pas 0

    # valeur de n en hexadécimal sur 256 octets
    e1650e792269f2a6243e93…
    

De même, la signature est encodée conformément à la RFC 8332 :


    The resulting signature is encoded as follows:

    string   "rsa-sha2-256" / "rsa-sha2-512"
    string    rsa_signature_blob
    

Aussi, pour récupérer la valeur de la signature, il faut enlever les octets correspondant à l'algorithme rsa-sha2-256 :


    00 00 00 0c                         # 12 octets vont suivre
    72 73 61 2d 73 68 61 32 2d 32 35 36 # codage ASCII de "rsa-sha2-256" sur 12 octets

    00 00 01 00                         # 256 octets vont suivre

    # valeur de la signature en hexadécimal sur 256 octets
    6d3d06d9b3ebe961111f69…
    

Le serveur vérifie la signature

Nous considérons que le serveur a autorisé la clé publique du client dans son fichier authorized_keys.

Il vérifie maintenant la signature reçue en appliquant l'opération VERIFY du schéma de signature \(\text{RSASSA-PKCS1-v1\_5}\). L'exécution des premières instructions donne \(EM\) :


    def RSAVP1(n: int, e: int, s: int) -> int:
        return pow(s, e, n)  # s^e mod n

    def I2OSP(i: int, k: int) -> bytes:
        return int.to_bytes(i, byteorder="big", length=k)

    n = 0xe1650e792269f2a6243e933e8e5f2429fe43ea0521653f6222a274ef4701ac6c3b6a5ea482d65c5da1c9acafe218726aaa7e135b78afd6c960e65b137a3c3c37ffcf0aef1b6b2d78445e855cdc0facce3fd8f991a392b66686e2d029f495a8b5e0cdc65a4d36763b84541ec7fbcdc825e2f76928f47fb6f0cc730ee6561a432669c3d7ac8d7038f2ee5714d78d1d7f6b6312cb866f7471a36b7ad359c726e58712405151789d89aa9af6f801ce4c0670452f1db430bb522b3d3776fec612e13a5a308629ace809010e113e0d745471b654943c21d1c657cc01090ee75f3fc6a28e84bf3f822b2ce09f58c69d3be9905e38b6435cfaee95f5cbd1523a809cf675
    e = 0x010001
    s = 0x6d3d06d9b3ebe961111f69c8e8ff727311b9d03bcf5ca8b31bbca18a85c28895dcb0e3c474d01aecc620af5a9299418b545a6479724272a6f9b5095cb35e8481d302c61147dfbe9f804d5720230b1e62f73eaa643bb0f133badd6a21c2d6ab6b4dee90d70edd0c1385f0521c215889ad574c3e9b43f8f27a527a128eb2b1d1d1a18f5818217bb2d91e84914befaed19fe48abb4f98e0fce9f610cbab577bf27e3c91c69be0178c58df92d6dce0d6ddf287dce68c1ff48bc0105706410750a36fb572c462d41a88e3e919f77c1e0d46fedc28e4f839e43882bb384cbdb2bdbde2d4257147e733802f7c25648a63c69d32bebc1c48e955b1307f121f63c4dfea03
    k = 256  # taille du module n en octets
    em = RSAVP1(n, e, s)
    EM = I2OSP(em, k)
    print(EM.hex())
    # 0001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    # ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    # ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    # ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    # ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    # ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
    # ffffffffffffffffffffffff003031300d060960864801650304020105000420
    # 16b2a6f6a002758223443bb464f2cde7cd1950b8640eee07722a35d8668f86a4
    

Le serveur retrouve le motif 0xff, caractéristique du schéma de signature \(\text{RSASSA-PKCS1-v1\_5}\).

Je n'irai pas plus loin dans l'exercice, la suite étant similaire à la section précédente, c'est-à-dire l'authentification du serveur auprès du client :