Somme de contrôle de la clé publique

Non tu peux rester en RFC0016 tant qu’elle n’est pas définitivement approuvée (voir dans le README) et en prod il peut y avoir des changements cassant.

1 Like

Oui, à ma connaissance aucun client en prod n’a implémenté cette RFC.

3 Likes

Cool pour le simple sha256 :slight_smile:

Et pour la forme 3…3:3 même si je la trouve plus simple à mémoriser, effectivement, une semaine de calcul sur un pc standard me semble un peu léger, d’où le fait que je ne me sois pas précipité pour proposer une révision de la RFC.

Peut-être pas pour cette RFC avec le format court de pubkey et peut-être plus pour les adresses envisagées par @elois, je me pose la question d’une forme de checksum constitué de 2 mots de Mnémonic. C’est plus long à écrire que 3 caractères de checksum, mais ça me semble avoir une bien plus grande entropie pour une mémorisation qui me semble plus aisée.
Je ne suis pas sûr que ce soit une bonne idée, mais j’aimerais avoir vos avis histoire de voir si je creuse plus ou non.

Bonne idée, et ça semble même plus simple à retenir et à reconnaître qu’un identicon.

Les inconvénients sont que ça prend plus de place à afficher que 3 caractères, et que ça dépend de la langue.

Mais dans 5Zu8...331q:percolateur cataplasme, on risque d’oublier la clé et de ne se souvenir que des deux mots, qui prennent plus de place pour les yeux et qui sont plus attirants que la clé. Ça pourrait donc avoir l’effet inverse… (saleté d’évolution, on n’est pas aidés par nos réseaux de neurones)

1 Like

Je donne mon avis de béotien. :crazy_face:
Cet affichage avec checksum c’est juste pour vérifier qu’on va faire un versement sur la bonne clé? Me trompe-je ?
Du coup on l’utilisera surtout sur les gmarchés après une recherche dans l’annuaire, donc surtout sur smartphone.
J’aime bien quand c’est écrit gros sur mon smartphone, j’ai peur que ajouter des mots allonge le texte et en diminue la taille. Dur pour mes yeux qui fatigue avec l’age.
Je préfère donc 3 caractères de checksum.
Si on y ajoute le petit grigri proposer par kimamila, ça fait quand même peu de possibilité de se tromper, et une grande difficulté d’être induit en erreur , il me semble.

Pour les autres cas d’usage on fera un copié collé de la clé complète, ou scan de qr-code ce qui devrait éviter les erreurs.
Maintenant je fais confiance aux pros, qui en savent plus que moi. :innocent:

2 Likes

C’est pour vérifier l’intégrité d’une clef pub. Ça apporte une sécurité importante pour les gens qui copient la clef pub à la main, car ça permet au logiciel de vérifier qu’il n’y a pas d’erreur de frappe.

Sur les formes courtes, ça apporte une sécurité supplémentaire par rapport à l’affichage de 8 caractères de la clef pub.

Mais je ne sais pas si c’est plus sécurisé que 11 caractères de la clef pub. Intuitivement je dirais que oui, car le checksum couvre l’ensemble de la clef pub, mais je me méfie de mon intuition.

1 Like

Et ton intuition se trompe bien, l’entropie est la même, 58^11 dans les deux cas, donc la sécurité est strictement la même :wink:

Sauf qu’il doit y avoir plus de redondance avec la checksum, avec le même nombre de caractères, donc moins d’entropie, non ? (puisque toutes les checksums possibles n’ont pas la même probabilité d’être la bonne pour un morceau de clé publique donné)

Non, étant donné un morceau de clé publique 4…4, on peut compléter cette clé publique de manière à avoir tout les checksum possibles.

Ce qui compte pour l’entropie ce n’est pas les probabilités mais ne nombre total de possibilités différents.

De plus, la fonction de hashage cryptographique SHA256 empêche justement de tirer parti de toute probabilité, sunon ce ne serait pas une fonction de hashage cryptographique.

D’un point de vue anti-phising, les 2 situations (11 caractères de la pubkey où 4…4:3) sont strictement équivalentes.

J’ai mergé ces modifications. La RFC_0016 est « under discussion ».

Un cas que la RFC n’explicite pas est quand b58(sha256(pubkey)) match /^1+/. Doit-on garder les 1 au début de la chaîne dont on prend les 3 premiers caractères ?

Il faudrait le préciser au cas où certaines implémentations de base58 les enlèvent automatiquement.

1 Like

Avec la dernière version de duniterpy (0.62.0), same avec python 3.8 et 3.9:

AttributeError: module 'duniterpy.key.base58' has no attribute 'b58decode'

Résolu par:

pubkey_byte = base58.Base58Encoder.decode(str.encode(pubkey))
hash = hashlib.sha256(hashlib.sha256(pubkey_byte).digest()).digest()
return base58.Base58Encoder.encode(hash)[:3]

Il y a un module historique dans DuniterPy qui se nomme base58 et contient une classe Base58Encoder avec les méthodes encode() et decode().

@poka, avec DuniterPy installé, quand tu appelles base58.b58decode(), Python va chercher d’abord dans duniterpy.key.base58 et ne trouve évidemment pas la méthode b58decode().

Ta solution est la bonne ! Merci pour ce retour !

D’où le fait qu’il faut absolument éviter de nommer un module du même nom qu’un module de la bibliothèque standard. Je vais faire un ticket pour qu’on vire ce module qui n’apporte pas grand chose et est même cause de problème, empêchant d’appeler base58.encode().

On va bientôt sortir la version 1.0.0 de DuniterPy ! Ce ticket sera pour un peu plus tard.

2 Likes

:face_with_monocle:

base58 ne semble pas être inclus dans la bibliothèque standard 3.8/9 !?

par ailleurs, le module base58 contient bien la méthode b58decode(), donc le souci de @poka ne vient pas de là ??

Il y a un petit micmac, là…

Je suppose que @poka a importé base58 depuis duniterpy.key et non installé le module éponyme.

Effectivement, mon bout de code implique d’installer et importer le module base58. Je vais ajouter ça dans la RFC.

Je ne sais pas quel est le souci de @poka, mais #chezmoiçamarche une fois que ledit module base58 a été installé.

Du coup j’aimerais bien comprendre quel est le souci, si souci il y a (une fois qu’on a fait import base58 en haut du script), afin de MAJ la RFC si besoin.


And now for something totally different @poka :

hashlib.sha256(hashlib.sha256(pubkey_byte).digest()).digest()

La double passe de sha256, c’est pour le checksum défini par Tortue. Qu’on soit bien sûr de ce dont on parle.


Non, évidemment non, le but est qu’on ait le même checksum quelle que soit l’implémentation de base58 utilisée. Du coup j’ai deux options, je veux bien vos lumières sur le choix :

  • soit retirer les leading 1 du checksum. Facilement compréhensible, mais ça implique de faire des comparaisons sur des chaînes de caractères, assez coûteuses.

  • soit ajouter un bit à 1 devant le hash de la clef publique dans tous les cas, avant de faire la conversion en bae58, pour être certain que le checksum ne commence pas par “1”. Cette opération est moins coûteuse au niveau de l’algo, mais je la trouve sale : si on ne sait pas d’où ça vient, rien ne justifie ce premier bit à 1.


Du coup, nouvelle MR si vous voulez travailler dessus :

et mon implémentation de test en Python

VOIR LA SUITE DE LA DISCUSSION
import base58
import hashlib
import re

test_pubkeys = [
    ["J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX" , "5qC"],
    ["12BjyvjoAf5qik7R8TKDJAHJugsX23YgJGi2LmBUv2nx" , "8BD"],
    ["2BjyvjoAf5qik7R8TKDJAHJugsX23YgJGi2LmBUv2nx" , "8BD"],
    # rare cases with numerous leading 1. 
    ["111111111111111111111111111111111pubKey49311" , "C7L"],
    ["11111111111111111111111111111111pubKey49311" , "C7L"],
    # public key whose checksum may be wrong by beginning with 1
    ["23iex3vD7sp9AK7wakwW1rj4F7RwYfQoXXR87Zttdte8", "4jT"], # NOT 14j !
    # invalid public keys represented in 44 base58 chars. Should raise an error.
    ["Z1111111111111111111111111111111111111111111" , None],
    ["K1111111111111111111111111111111111111111111" , None],
    # Should not raise an error.
    ["J1111111111111111111111111111111111111111111" , "FqK"],
]

def gen_checksum(pubkey):
    """
    Returns the checksum of the input pubkey (encoded in b58)
    """
    pubkey_byte = bytearray(base58.b58decode(pubkey))
    # ensure the pubkey is 32 bytes long
    while len(pubkey_byte) < 32:
        pubkey_byte = bytearray(b"\x00") + pubkey_byte
    while len(pubkey_byte) > 32:
        if pubkey_byte[0] == 0:
            del pubkey_byte[0]
        else:
            print(pubkey_byte[0])  # debug
            raise ValueError("Invalid public key: bytes length is too long")
    # hash the pubkey and convert the hash to base58
    hash = hashlib.sha256(pubkey_byte).digest()
    print("".join("{:02x}".format(x) for x in hash)) #debug
    print(base58.b58encode(hash))  # debug
    ck = base58.b58encode(hash).decode("utf-8")
    # ensure the hash does not begin with a 1
    while re.search(re.compile("^1"), ck):
        ck = ck[1:]
    # keep the first three chars
    return ck[:3]


def test_checksum_generation():
    for pk in test_pubkeys:
        try:
            ck = gen_checksum(pk[0]) 
            assert ck == pk[1]
            print ( pk[0] + ":" + ck + "\n")
        except ValueError as e:
            assert str(e) == "Invalid public key: bytes length is too long"

### main script ###

test_checksum_generation()

1 Like

Je pense que la plupart des implémentations de base58 conservent les 1 au début (dont celle en python), et ça me semble être une bonne chose. Je préférerais ajouter des 1 pour avoir 44 caractères. Du coup la plupart du temps il n’y a rien à faire.

Même si c’est tout à fait improbable, on prend le risque de considérer que la checksum sans les 1 fait au moins 3 caractères. Sinon, le comportement est indéfini. Pour le définir de la manière la plus simple, on peut ajouter des 1 pour avoir 3 caractères. Mais pouf, on a enlevé des 1 pour en remettre, alors autant les avoir gardés dès le début.

Je n’en sais fichtre rien.

Oui, c’est une autre option, qui a le mérite de ne pas nécessiter de comparaison de chaîne de caractères. Si la longueur est inférieure à 44 alors on ajoute un “1” de tête.

Il me semble cependant que cette solution facilite les collisions sur les (rares) cas où les représentations base58 des hashs commenceraient par des “1” ou seraient plus courts que 44.

J’ai fait une simulation, le nombre de collisions est assez proche, et il est même systématiquement supérieur quand on enlève les 1.

code de la simulation
use rand::Rng;

const ITER: usize = 10_000_000; // nombre d'itérations
const N: usize = 32; // nombre de caractères (32 ou 44 ça ne change rien en général)

fn main() {
        let mut rng = rand::thread_rng();
        let dist = rand::distributions::Uniform::new(0u8, 58);

        let (mut a, mut b) = ([0u8; N], [0u8; N]);

        let mut n1 = 0usize;
        let mut n2 = 0usize;
        for _ in 0..ITER {
                a.iter_mut().for_each(|v| *v = rng.sample(dist));
                b.iter_mut().for_each(|v| *v = rng.sample(dist));
                if a[0..3] == b[0..3] {
                        n1 += 1;
                }
                let (mut s1, mut s2) = (0usize, 0usize);
                while a[s1] == 0 {
                        s1 += 1;
                }
                while b[s2] == 0 {
                        s2 += 1;
                }
                if a[s1..s1+3] == b[s2..s2+3] {
                        n2 += 1;
                }
        }
        println!("with: {}   without: {}", n1, n2);
}

Au début je pensais que les deux seraient équivalentes, j’ai commencé à faire le calcul qui avait l’air de montrer que non mais je n’ai pas réussi à le terminer et donc à trancher. J’ai donc fait l’expérience. (en Python puis en Rust pour les perfs)

3 Likes

OK, suite aux remarques de @tuxmain, j’ai mis la MR pour la RFC-checksums à jour avec ce choix :

SI la représentation base58 du hash(pubkey) a une longueur inférieure à 44 :

ajouter des caractères “1” en tête pour atteindre 44 caractères.

ping @elois @poka @1000i100 @kimamila @moul
Si pas de remarques, je merge dans deux semaines.

Implémentation de test
import base58
import hashlib
import re

test_pubkeys = [
    ["J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX" , "5qC"],
    ["12BjyvjoAf5qik7R8TKDJAHJugsX23YgJGi2LmBUv2nx" , "8BD"],
    ["2BjyvjoAf5qik7R8TKDJAHJugsX23YgJGi2LmBUv2nx" , "8BD"],
    # rare cases with numerous leading 1. 
    ["111111111111111111111111111111111pubKey49311" , "C7L"],
    ["11111111111111111111111111111111pubKey49311" , "C7L"],
    # public key whose checksum may be wrong by beginning with 1
    ["23iex3vD7sp9AK7wakwW1rj4F7RwYfQoXXR87Zttdte8", "14j"], # NOT 4jT !
    ["J66fMDgSVVcdRgKucj1D5ssXAcQvg35JopvHNQo6cGY6", "1vQ"], #vQoY2iTFrYyBqSDAiWz9f9rRgNQm7FTsHTNpwqdEbct
    # invalid public keys represented in 44 base58 chars. Should raise an error.
    ["Z1111111111111111111111111111111111111111111" , None],
    ["K1111111111111111111111111111111111111111111" , None],
    # Should not raise an error.
    ["J1111111111111111111111111111111111111111111" , "FqK"],
]

def gen_checksum(pubkey):
    """
    Returns the checksum of the input pubkey (encoded in b58)
    """
    pubkey_byte = bytearray(base58.b58decode(pubkey))
    # ensure the pubkey is 32 bytes long
    while len(pubkey_byte) < 32:
        pubkey_byte = bytearray(b"\x00") + pubkey_byte
    while len(pubkey_byte) > 32:
        if pubkey_byte[0] == 0:
            del pubkey_byte[0]
        else:
            print(pubkey_byte[0])  # debug
            raise ValueError("Invalid public key: bytes length is too long")
    # hash the pubkey and convert the hash to base58
    hash = hashlib.sha256(pubkey_byte).digest()
    print("".join("{:02x}".format(x) for x in hash)) #debug
    print(base58.b58encode(hash))  # debug
    ck = base58.b58encode(hash).decode("utf-8")
    # ensure the b58(hash) is 44 chars
    ck = "1" * (44 - len(ck)) + ck
    # keep the first three chars
    return ck [:3]


def test_checksum_generation():
    for pk in test_pubkeys:
        try:
            ck = gen_checksum(pk[0]) 
            assert ck == pk[1]
            print ( pk[0] + ":" + ck + "\n")
        except ValueError as e:
            assert str(e) == "Invalid public key: bytes length is too long"

### main script ###

test_checksum_generation()

Désolé pour avoir modifié 3 fois cette RFC. J’espère que cette fois, c’est la bonne. Merci aux relecteurs attentifs :+1:

3 Likes

Du coup, ça ferais une représentation ascii de taille fixe (44chr) , mais une représentation binaire qui pourrais dépasser les 32bits dans certains cas (même si c’est pour compléter avec des 0 audela des 32 bits).

Si c’est bien ça, je ne suis pas sûr que ce soit pertinant, et j’ai peur que ça augmente la proportion de clef commençant par des 1 et donc les formats court dont le début serait aisaiement constitué de 1111…

Mais ce sont des réserves au feeling, je n’ai pas fait de calcul pour le vérifier.

J’ai peur aussi que ça complique légèrement la vérification de validité d’une clef (en ayant à tronquer au 32 derniers bits pour faire les calculs).

Non, cette modification concerne la représentation base58 du hash de la clef pub. On en garde 3 caracteres pour avoir le checksum. Ça ne change rien à la représentation de la clef pub.

La RFC prévoit déjà cette troncature, ou cet allongement. J’ai ajouté ces calculs suite à tes retours durant le hackaton.