Files
khadhroony-solana-project/docs/formats/KSPWALLET_V2.md
2026-08-22 03:21:49 +02:00

15 KiB

.kspwallet V2 — spécification du wire binaire natif KSP

1. Statut et objectif

Ce document est l'autorité normative du wire .kspwallet format_version = 2 introduit par 0.2.6-pre.015.

V2 remplace l'enveloppe JSON/Base64url de V1 par un framing binaire KSP canonique. L'objectif est :

réduire fortement la taille persistée
supprimer l'encodage Base64 des champs déjà binaires
éviter qu'un .kspwallet nouvellement créé soit un document JSON lisible comme tel
conserver un format documenté et implémentable hors Rust
préserver les frontières cryptographiques Wallet

Le caractère binaire n'ajoute aucune propriété cryptographique. La confidentialité et l'authenticité continuent de dépendre d'Argon2id, XChaCha20-Poly1305 et Ed25519, jamais de l'absence de JSON.

0.2.6-pre.015 fige la grammaire binaire, les identifiants numériques, les bornes structurelles et les transcripts/AAD V2. La création/ouverture/persistence V2, l'auto-détection V1/V2 et le basculement de l'API générique vers V2 sont matérialisés dans les tranches suivantes de 0.2.6.

V1 reste un format stable supporté. V2 ne réinterprète jamais un document V1 avec sa propre grammaire.

2. Politique de version et API

Les notions suivantes sont distinctes :

DEFAULT_WALLET_FORMAT
LATEST_SUPPORTED_WALLET_FORMAT

À partir de l'intégration V2 :

DEFAULT_WALLET_FORMAT          = V2
LATEST_SUPPORTED_WALLET_FORMAT = V2

Si un futur V3 apparaît, par exemple pour un modèle d'autorisation avec second facteur, LATEST_SUPPORTED_WALLET_FORMAT pourra devenir V3 tandis que DEFAULT_WALLET_FORMAT pourra rester V2. Le default ne suit jamais automatiquement la dernière version.

La politique d'API retenue est :

create_wallet_file(...)       -> format default explicitement choisi, V2
create_wallet_file_v1(...)    -> force V1
create_wallet_file_v2(...)    -> force V2
create_wallet_file_v3(...)    -> force V3 si V3 existe un jour

open_wallet_*_file(...)       -> détecte puis dispatch les versions supportées
open_wallet_*_file_v1(...)    -> exige V1
open_wallet_*_file_v2(...)    -> exige V2
open_wallet_*_file_v3(...)    -> exige V3 si V3 existe un jour

Le même principe s'applique aux opérations dont la version de fichier est pertinente, notamment inspection et import natif.

3. Encodage général

Un document V2 est :

binaire
maximum 1 048 576 octets
entiers multi-octets = unsigned big-endian
ordre des champs = normatif
aucun padding implicite
aucune Base64
aucune compression
aucun trailing byte

Les longueurs sont exprimées en octets.

Une implémentation conforme doit vérifier les bornes avant toute allocation dépendant d'une longueur reçue. Le document entier est déjà borné par 1 048 576 octets avant parsing.

4. Header V2

Le début de fichier est strictement :

Ordre Champ Taille Valeur / règle
1 magic 9 ASCII exact KSPWALLET
2 format_version 2 0x0002
3 document_length 4 longueur totale exacte du fichier
4 flags 2 bit 0 = VIEW activé ; tous les autres bits = 0
5 owner_auth_public_key 32 clé publique Ed25519 OWNER
6 view_descriptor.slot_id 16 conditionnels présent uniquement si flags & 0x0001 != 0

Offsets fixes avant le descripteur conditionnel :

0x0000..0x0008  magic
0x0009..0x000A  format_version
0x000B..0x000E  document_length
0x000F..0x0010  flags
0x0011..0x0030  owner_auth_public_key

Pour un wallet VIEW-enabled, view_descriptor.slot_id occupe ensuite 0x0031..0x0040.

4.1 Flags

0x0001  VIEW_ENABLED
0xFFFE  réservé, doit être zéro

Tout bit réservé non nul rend le document invalide. V2 n'emploie pas les bits réservés comme mécanisme d'extension silencieuse ; une modification incompatible exige un nouveau format.

5. Ordre canonique du body

Après le header :

OWNER key slot                         obligatoire
VIEW key slot                          présent seulement si VIEW_ENABLED
OWNER-CONTROL encrypted compartment    obligatoire
METADATA encrypted compartment         obligatoire
SECRET encrypted compartment           obligatoire
state signature                        obligatoire
EOF                                    immédiat

Aucun compteur de slots ou de compartiments n'est nécessaire : leur cardinalité et leur ordre sont déterminés par V2.

6. Key slot V2

Chaque key slot est encodé ainsi :

Champ Taille Valeur / règle
role 1 0x01 OWNER, 0x02 VIEW
slot_id 16 identifiant binaire exact
kdf_algorithm 1 0x01 Argon2id
kdf_version 4 19
memory_kib 4 1..1 048 576, et >= parallelism * 8
iterations 4 1..64
parallelism 4 1..64
salt_length 1 16..64
salt variable exactement salt_length octets
wrap_algorithm 1 0x01 XChaCha20-Poly1305
wrap_nonce 24 nonce exact
wrap_ciphertext_length 2 16..4096
wrap_ciphertext variable exactement la longueur déclarée

Ordre obligatoire :

premier slot = OWNER
second slot = VIEW seulement si VIEW_ENABLED

Si VIEW est activé :

header.view_descriptor.slot_id == VIEW key slot.slot_id

Toute divergence est invalide avant KDF/déchiffrement.

7. Compartiments V2

Chaque compartiment est encodé :

Champ Taille Valeur / règle
kind 1 0x01 OWNER-CONTROL, 0x02 METADATA, 0x03 SECRET
payload_version 4 1 pour le profil initial V2
algorithm 1 0x01 XChaCha20-Poly1305
nonce 24 nonce exact
ciphertext_length 4 longueur exacte
ciphertext variable ciphertext + tag Poly1305

Bornes initiales :

OWNER-CONTROL : 16..4096 octets
METADATA      : 16..65552 octets
SECRET        : 16..4096 octets

L'ordre exact est OWNER-CONTROL, METADATA, SECRET. Le kind encodé doit correspondre à la position attendue ; il n'autorise pas un réordonnancement.

Les payloads plaintext V2 conservent le modèle fonctionnel établi en V1 pour cette évolution : owner-control, metadata et secret Solana restent des compartiments distincts. Une future modification incompatible de leur sémantique exige une version de format explicite.

8. Signature d'état

La fin du document est :

Champ Taille Valeur / règle
state_signature.algorithm 1 0x01 Ed25519
state_signature.signature 64 signature detached exacte

Aucun octet ne peut suivre ces 65 octets.

9. Identifiants numériques figés

9.1 Rôles

0x01 OWNER
0x02 VIEW

9.2 Algorithmes

KDF
0x01 Argon2id

AEAD
0x01 XChaCha20-Poly1305

state signature
0x01 Ed25519

9.3 Compartiments

0x01 OWNER-CONTROL
0x02 METADATA
0x03 SECRET

0x00 est invalide dans ces espaces. Les autres valeurs sont réservées et rejetées par V2 tant qu'elles ne sont pas explicitement normalisées par une évolution compatible documentée ; aucune valeur inconnue n'est devinée.

10. Canonicalité et rejet

Le parser V2 doit rejeter avant crypto :

fichier > 1 MiB
magic incorrect
format_version != 2
document_length différent de la taille reçue
flag réservé non nul
champ tronqué
longueur qui dépasse le reste du document
role/algorithm/kind inconnu
ordre OWNER/VIEW invalide
VIEW descriptor et VIEW slot incohérents
Argon2 hors bornes
salt hors bornes
ciphertext hors bornes
payload_version non supporté
trailing bytes

Il n'existe qu'un encodage canonique d'un même état sémantique V2 : mêmes champs, même ordre, mêmes largeurs, même endianness et aucune donnée ignorée.

11. Transcripts et AAD V2

V2 possède ses propres domaines ; V1 et V2 ne partagent jamais un domain separator :

KSPWALLET-V2-STATE
KSPWALLET-V2-AAD-OWNER-SLOT
KSPWALLET-V2-AAD-VIEW-SLOT
KSPWALLET-V2-AAD-OWNER-CONTROL
KSPWALLET-V2-AAD-METADATA
KSPWALLET-V2-AAD-SECRET

Le transcript/AAD V2 conserve la discipline TLV déterministe de V1 :

domain || 0x00
puis pour chaque champ :
    tag:u16 big-endian
    length:u64 big-endian
    value:length bytes

Les tags restent alignés avec les familles V1 :

0x0001 magic
0x0002 format_version
0x0003 owner_auth_public_key
0x0010 view_enabled
0x0011 view_role
0x0012 view_slot_id
0x0100 slot_id
0x0101 slot_role
0x0102 kdf_algorithm
0x0103 kdf_version
0x0104 kdf_memory_kib
0x0105 kdf_iterations
0x0106 kdf_parallelism
0x0107 kdf_salt
0x0108 wrap_algorithm
0x0109 wrap_nonce
0x010A wrap_ciphertext
0x0200 compartment_kind
0x0201 compartment_version
0x0202 compartment_algorithm
0x0203 compartment_nonce
0x0204 compartment_ciphertext
0x0500 state_signature_algorithm

Différence normative V2 : les rôles/algorithmes/kinds sont transcriptés sous leur ID numérique d'un octet, et format_version = 2 est transcripté en u32 big-endian. Les domaines distincts empêchent qu'un transcript V1 et un transcript V2 soient interchangeables.

11.1 State transcript

Le state transcript contient :

common
view descriptor
OWNER slot avec wrap nonce+ciphertext
OWNER-CONTROL avec nonce+ciphertext
METADATA avec nonce+ciphertext
SECRET avec nonce+ciphertext
state signature algorithm

Le VIEW wrap mutable n'est pas ajouté au state transcript, selon le modèle d'autorisation VIEW déjà retenu : son identité stable reste liée par le descriptor signé tandis que son credential peut être self-rotaté sans OWNER.

11.2 Slot AAD

Le slot AAD contient :

common
slot_id
role
KDF algorithm/version/parameters/salt
wrap algorithm

Le wrap nonce/ciphertext n'est pas inclus dans son propre AAD.

11.3 Compartment AAD

Le compartment AAD contient :

common
kind
payload_version
algorithm

Le nonce/ciphertext n'est pas inclus dans son propre AAD.

12. V1 et V2

V1 et V2 sont deux formats explicites :

Propriété V1 V2
enveloppe JSON UTF-8 binaire KSP
champs binaires Base64url no-pad bytes directs
version 1 2
canonicalité sémantique JSON + Base64 canonique byte-exact
taille fixture wire-only 2037 octets 628 octets
Argon2id oui oui
XChaCha20-Poly1305 oui oui
Ed25519 OWNER state oui oui
VIEW/OWNER oui oui

La réduction mesurée sur le fixture structurel de référence est :

2037 -> 628 octets
-1409 octets
~69,2 %

Cette mesure n'est pas une promesse de ratio constant : la taille dépend notamment des metadata chiffrées.

13. Fixture structurelle

Fixture canonique de pre.015 :

crates/ksp-wallet-lib/tests/fixtures/kspwallet_v2_wire_only.bin
size   = 628
sha256 = cbacb37326c76e6dde8d614601cd765a8c0fd9744f891d640b8a3cd71cf60a93

Préfixe hexadécimal :

4b535057414c4c4554 0002 00000274 0001
000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
202122232425262728292a2b2c2d2e2f

Lecture :

4b535057414c4c4554 = "KSPWALLET"
0002               = format_version V2
00000274           = 628 octets
0001               = VIEW_ENABLED

La fixture wire_only vérifie framing/canonicalité ; elle n'est pas présentée comme une signature cryptographique V2 valide tant que la création/open V2 n'est pas intégrée par la tranche suivante.

14. Compatibilité et migration

V2 n'autorise aucune migration implicite à l'ouverture.

Politique prévue :

lecture générique V1/V2      oui
création générique           V2
création V1 forcée           oui
création V2 forcée           oui
réécriture V1 -> V2 cachée   non
migration V1 -> V2           opération explicite

Une migration doit authentifier le wallet source avec la capability requise, produire un nouveau document V2 valide et respecter la persistence no-clobber/atomique. Une simple transcodification non authentifiée des bytes V1 n'est pas suffisante puisque V2 possède ses propres domains/transcripts.

15. Future V3 / second facteur

V2 ne réalise aucun second facteur.

Si un futur V3 ajoute une autorisation dépendant d'un facteur externe :

ksp-wallet-lib possède la règle qui exige le facteur
ksp-wallet-lib possède le challenge et la validation cryptographique
aucun WalletOwner ne peut être obtenu si le facteur requis n'est pas satisfait
Wallet Desk ne décide jamais que le second facteur est valide

Une UI comme Wallet Desk peut néanmoins devoir évoluer pour orchestrer l'expérience : attente de confirmation, saisie OTP, enrollment, recovery, hardware/WebAuthn ou consentement externe.

Le réseau ou le fournisseur externe ne doit pas forcer ksp-wallet-lib à dépendre directement de Config/Transport/Tauri. Une abstraction/provider KSP séparée peut fournir la preuve à Wallet tandis que Wallet reste propriétaire de la politique d'autorisation.

L'arrivée d'un V3 ne change pas automatiquement le default : V2 peut rester DEFAULT_WALLET_FORMAT aussi longtemps que KSP le décide explicitement.