Somme de contrôle de la clé publique

Bonjour !

@kimamila @vit @moul @tuxmain

Ce fil concerne le format d’affichage des clefs publiques dans les clients (et notamment la question du checksum).

La question a été un peu explorée dans le sujet clefs publiques commençant par « 1 ». Au moment où je lance ce sujet, ces deux clefs publiques (binaires identiques) sont considérées différentes par Duniter et donnent des checksums différentes :

12BjyvjoAf5qik7R8TKDJAHJugsX23YgJGi2LmBUv2nx:8pQ
2BjyvjoAf5qik7R8TKDJAHJugsX23YgJGi2LmBUv2nx:5vi

le « 1 » de tête correspond à la valeur 0. Il est affiché, ou non, suivant la lib base58 utilisée.

@elois vient de coder dans Duniter que la clef privée correspondant à ces deux pubkeys puisse débloquer la monnaie reçue sur ces deux comptes. Il nous suggère d’utiliser une même checksum pour les deux pubkeys.

On en a rapidement discuté avec @moul, et ce sujet n’est pas léger, vu qu’il s’agit d’interopérabilité entre les clients et potentiellement d’un changement dans les pratiques des utilisateurs. Je vois deux situations à gérer :

  • l’utilisateur utilise plusieurs clients. Il doit avoir, pour les mêmes identifiants, la même clef publique et le même checksum
  • l’utilisateur veut envoyer de la monnaie sur une clef pub dont il a le checksum. Le client doit interpréter correctement le checksum, et ne pas renvoyer une erreur pour un checksum valide qu’il ne saurait pas traiter.

Je vois plusieurs options :

  1. Ne rien changer : continuer à utiliser le format checksum tel que défini par @Tortue, et laisser les « 1 » de tête. Dans ce cas, on considère que le checksum sert uniquement à valider que la chaîne entrée est cohérente, et ne sert pas à identifier la clef publique. Problème : suivant les clients, l’utilisateur aura un « 1 » de tête, ou non, et un checksum différent.

  2. détecter les cas où la pubkey a un « 1 » de tête, l’enlever, puis utiliser le format de checksum pré-cité et afficher la pubkey sans le « 1 » de tête. On continue à baser le checksum sur la chaîne de caractères, mais 1/ on n’a plus les « 1 » de tête, donc les clefs pub sont identiques entre tous les clients, et 2/ les clefs pub entrées avec un « 1 » de tête seraient tout de même cohérentes, car validées par le format de checksum actuel.

  3. partir sur un format d’affichage de pubkey qui se base sur la représentation binaire de la pubkey et contient le checksum. Problème : la représentation base58 de la clef pub est entièrement transformée. @moul a proposé ceci :

> import base58
> moul_pubkey = "GfKERHnJTYzKhKUma5h1uWhetbA8yHKymhVH2raf2aCP"
> moul_pubkey_checksum = base58.b58encode_check(moul_pubkey)
> moul_pubkey_checksum
> b'3cuhfZK54A9QKvtGTsTEvaX81Xta6dPdjHFuhS2mWCcbCdFHsT91hKStGQqtBy7Lgr'
> base58.b58decode_check(moul_pubkey_checksum)
> b'GfKERHnJTYzKhKUma5h1uWhetbA8yHKymhVH2raf2aCP'
> base58.b58decode(moul_pubkey_checksum)
> b'GfKERHnJTYzKhKUma5h1uWhetbA8yHKymhVH2raf2aCP{h\xbb#'
  1. redéfinir un format de checksum à partir du binaire de la clef publique, de 4 caractères pour le différencier de celui de @Tortue, par exemple en prenant les 4 derniers caractères de base58.b58encode_check(pubkey). Les derniers pour éviter de retrouver des « leading 1 » non souhaités.

Je suis fermement opposé à l’option 3. Je considère que n’importe qui doit pouvoir auditer la blockchain et y retrouver sa clef publique (ou son adresse) telle qu’il la connaît.

Je suis favorable à l’option 2. Le temps que tous les clients l’adoptent, il y aurait quelques cas où un utilisateur pourrait voir son checksum différemment sur deux clients, mais ce serait assez mineur.


Comme souvent, je pose des questions à la limite de mon champs de compétence, donc n’hésitez pas à me dire si je dis des sottises :wink:

2 « J'aime »

À partir de la clé publique avec somme de contrôle intégré, il est toujours possible d’accéder à la clé publique classique :

>>> import base58

>>> moul_pubkey = "GfKERHnJTYzKhKUma5h1uWhetbA8yHKymhVH2raf2aCP"
>>> moul_pubkey_checksum = base58.b58encode_check(moul_pubkey)

>>> moul_pubkey_checksum
b'3cuhfZK54A9QKvtGTsTEvaX81Xta6dPdjHFuhS2mWCcbCdFHsT91hKStGQqtBy7Lgr'

>>> moul_pubkey = base58.b58decode_check(moul_pubkey_checksum)
>>> moul_pubkey
b'GfKERHnJTYzKhKUma5h1uWhetbA8yHKymhVH2raf2aCP'
1 « J'aime »

À noter que j’ai toujours en tête que l’on passe un jour sous un format d’adressage compressé qui permette de payer à n’importe-quel script et qui aura son checksun intégré. On paiera alors une adresse qui pourra être indifféremment un script ou une pubkey. L’adresse fera 23 octets en binaire et sera donc représentée par 32 caractères en base58.

Il faut donc bien garder en tête que ce format de checksum pour clé publique est temporaire, pour 1 an ou 2 grands max :wink:

2 « J'aime »

Je trouve incohérent et peu pratique d’avoir une checksum différente en fonction des « 1 », car la différence pour l’algo qui calcule la checksum est la taille de la clé publique (32 octets avec le « 1 », 31 sans). Cette différence serait plutôt un bug des implémentations à taille variable (comme en Python, où une clé publique n’est qu’une chaîne à taille dynamique).

En fait, ça reviendrait à dire qu’une clé publique peut faire moins de 32 octets, donc qu’elle n’est définie non seulement par sa valeur, mais aussi par sa longueur.

Edit: pour l’instant j’ai modifié natools.py afin qu’il ajoute des zéros à la clé publique binaire pour qu’elle fasse 32 octets. Par exemple, les clés /^1{1,44}$/ ont toujours la même checksum, 3ud.

2 « J'aime »

Oui, je suis d’accord : de mon point de vue, c’ets un bug de newbie (pour Cesium je parle).
Je vais aussi devoir corriger cesium afin qu’il veille à convertir en 32 octets. Le checksum généré sera donc uniquement celui qui correspond au 32 octets.
Ensuite, je vais faire en sorte que la lecture d’un checksum sur une clef de 31 octet fonctionne, en fallback. Nous pourrons retirer ce fallback dans quelques mois/années, quand plus personne n’utilisera l’ancien format.

Pas compliqué à gérer, donc, je trouve. Juste un autre bug :slight_smile:

1 « J'aime »

Ah oui bonne idée, comme ça l’utilisateur sait quand même si sa clé est valide. Je vais ajouter cette tolérance dans natools, avec un avertissement disant que le format est déconseillé.

1 « J'aime »

Un peu d’aide demandé pour le calcul des checksum sur le model proposé par Tortue.
Je reprends l’exemple exact qu’a fournit Tortue.

Pour rappel:

En dart je fais:

// var pubkeyUTF = utf8.decode("J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX".codeUnits);
Digest pubkeyS256 = sha256.convert("J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX".codeUnits);
Digest pubkeyS256S256 = sha256.convert(pubkeyS256.bytes);
var pubkeyCheksum = Base58Encode(pubkeyS256S256.bytes);

print(pubkeyS256);
print(pubkeyS256S256);
print(pubkeyCheksum);

Aucune des étapes ne correspond à ce que fournit Tortue:

53c97ed12bb13e0b9b1c423610c8e37b739616dda708a59555154163c2951781
91050afa42eaddaa650d18731263ea9939f5188b21d5c623a79a3f354d9c3fa0
Am6aR5v1KBD9SiUAVdAuFk7PVrorjLgP65LeDuKwDn8w

:pleading_face:


J’ai envie de dire, pareil en bash:

$ echo -n "J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX" | sha256sum
53c97ed12bb13e0b9b1c423610c8e37b739616dda708a59555154163c2951781

Ce n’est pas un sha256sum, c’est quoi alors ?

1 « J'aime »
"J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX".codeUnits

Je ne connais pas Dart. .codeUnits, ça te donne des bytes ? convertit la str base58 en bytes ?

On travaille sur la représentation en bytes de la clef pub, pas sur la string base58.
Pour ref , deux implémentations :

code Python (silkaj.crypto_tools)
def gen_checksum(pubkey):
    """
    Returns the checksum of the input pubkey (encoded in b58)
    """
    pubkey_byte = base58.b58decode(pubkey)
    hash = hashlib.sha256(hashlib.sha256(pubkey_byte).digest()).digest()
    return base58.b58encode(hash)[:3].decode("utf-8")
code JS (PaperWallet / Vignette : duniter_tools.js)
//public_key_checksum_from_b58(string_b58) → (string_b58)
//return a checksum of a public key
//return -1 if bad format of public key
function public_key_checksum_from_b58(public_keyb58) {
	if (check_B58(public_keyb58) == false)
		return -1;
	var public_key = Base58.decode(public_keyb58);
	if (public_key.byteLength !== 32)
		return -1;
	return public_key_checksum(public_key);
};

//public_key_checksum(Uint8Array) → (string_b58)
//return a checksum of a public key 
//return -1 if bad format of public key 
function public_key_checksum(public_key) {
	if( public_key.byteLength !== 32 )
		return -1;
	var checksum;
	var shaX2_pubkey;

	nacl_factory.instantiate(function (nacl) {
		shaX2_pubkey = nacl.crypto_hash_sha256(nacl.crypto_hash_sha256(public_key));
		checksum = Base58.encode(shaX2_pubkey).substring(0,3);
	});
	return checksum;
};

Dans les deux cas, on voit bien qu’on part d’une str base58 qu’on « decode » (convertit en bytes), avant d’appliquer les deux sha256.

2 « J'aime »

J’ai fait un ticket pour que dup-crypto-rs gère les checksums. (@elois je peux me lancer dans une PR ça ne doit pas être trop compliqué)

2 « J'aime »

Aaaaaaaah c’est bon j’ai réussi à avoir le même checksum !!!

Pardon je faisais n’importe quoi …


Donc en dart:

List<int> pubkeyByte =
          Base58Decode('J4c8CARmP9vAFNGtHRuzx14zvxojyRWHW2darguVqjtX');
Digest pubkeyS256 = sha256.convert(sha256.convert(pubkeyByte).bytes);
String pubkeyCheksum = Base58Encode(pubkeyS256.bytes);
print(pubkeyCheksum);

-> KAvdGW7rpV68WDQPVN2TCrCoyvAHqMK1nSTS8y68bmB

Que pensez-vous de ce format pour visualiser les pubkey ?

Exemple : D2meevcAHFTS2gQMvmRW5Hzi25jDdikk4nC4u1FkwRaU
HumainReadable: D2me.wRaU:76W

image

ou bien

image

(avec des couleurs différentes pour chacune des 3 parties ?)
(Quand on clique dessus ça copie la clé publique complète)
(Bon je m’éloigne du sujet de ce post va peut être falloir migrer ces messages dans un nouveau topic)

1 « J'aime »

Pour revenir au sujet de ce topic sinon, que pensez-vous d’un checksum uniquement numérique (3 chiffres) ? Ce serait beaucoup plus lisible et mémorisable je trouve.

Ce réduit beaucoup le nombre de checksum possible (999) mais bon, c’est un checksum, je sais pas, qu’en pensez vous ?

Si oui, quelle forme numérique prendre ?

Pour les checksums, actuellement, en Python, on a :

  • Implémentation DuniterPy
  • Implémentation Silkaj
  • Implémentation Tikka

Dans Tikka, j’affiche le début de la clef (8:3). Mais j’aime bien ta seconde proposition.

C’est donc un bon sujet de discussion d’harmonisation pour la réunion mensuelle. :wink:

3 « J'aime »

Je préfère aussi :wink:

1 « J'aime »

@tuxmain je préfère attendre qu’il y ai une spec commune qui fasse consensus :slight_smile:

2 « J'aime »

Il y a peut-être plusieurs implémentations, mais le format et la somme de contrôle sont les mêmes :

<pubkey>:<checksum>

Je pense qu’il y a consensus sur ce format de somme de contrôle de la clé publique.
À quoi fais-tu référence Éloïs ?

D’ailleurs, pourquoi deux passes de SHA256 ? C’est mieux qu’une seule ?

1 « J'aime »

à la discussion précise de ce sujet :wink:

Kimamila propose que, après décodage de la clef pub, on vérifie qu’elle fasse bien 32 octets avant de calculer le checksum. Si ce n’était pas le cas, on ajouterait des octets nuls en début de pubkey. Avec cette spec, les deux clefs pub indiquées ci-avant auraient le même checksum, 8pQ.

Je suis tout à fait d’accord avec cette spec, reste qu’à la rédiger !

Je crois que ça vient de BTC, dont s’est inspiré @Tortue. Mais @elois peut préciser. Je ne sais pas si c’est mieux.

2 « J'aime »

En effet ça viens du BTC et ça ne sert à rien. Un seul SHA256 est satisfaisant.

2 « J'aime »

Après la discussion de mardi dernier, voici la RFC concernant le format de checksum:

comme convenu :

  • la double passe de sha256 est conservée. Quoique peu utilisées, des checksums sont déjà dans la nature, et nous préférons conserver la compatibilité avec l’ancien format.
  • la forme courte d’affichage est définie pour clef pub + checksum

TODO / incertitudes :

  • la RFC donne la longueur des clefs publiques comprise entre 40 et 44 caractères, conformément à l’implémentation de @elois, et conformément au DUBPv13. Dois-je la définir à {43,44} tant queDUBPv12 est valables ?

  • la MR vise-t-elle le bon dépôt ?

Remarques concernant la forme courte:

  • sur des clefs publiques commençant par 1, les formes courtes peuvent être très différentes suivant les librairies. 1111…v2nx:56f et 5Hj6…v2nx:56f peuvent désigner une même clef publique de longueur 40 OU 44 caractères.

  • la forme courte autorise des collisions entre clefs publiques. Je n’ai pas évalué la probabilité de collision.

3 « J'aime »