Lorsqu’une équipe répartie sur plusieurs fuseaux horaires transmet sa configuration de signature, elle se heurte souvent au même problème : « La signature réussit côté serveur, mais la vérification échoue sur iOS. » Le JSON affiché est identique des deux côtés, et régénérer les clés ne change rien. Pour distinguer un problème d’appel cryptographique, de décodage du transport ou de reconstruction du message, commencez par établir une référence de vérification sur un Mac distant, indépendamment de toute API métier, puis comparez les octets bruts. Les exemples ci-dessous utilisent Swift et CryptoKit, avec uniquement des données de test publiques : aucune clé privée de production n’est nécessaire.
Fixer l’algorithme et le contrat d’entrée
Le contrat doit préciser qu’il s’agit d’Ed25519 standard, avec une clé publique sous forme brute de 32 octets et une signature sous forme brute de 64 octets. Dans CryptoKit, le type correspondant est Curve25519.Signing.PublicKey ; ne le remplacez pas par un type destiné à l’accord de clés sous prétexte que son nom est similaire.
La partie qui signe doit indiquer si la signature porte sur les octets bruts du corps de la réponse ou sur le résultat d’une procédure de canonicalisation. Dans le premier cas, le client doit vérifier la signature avant d’analyser le contenu : il ne faut pas décoder le JSON puis le réencoder pour effectuer la vérification. Si les deux parties conviennent d’une canonicalisation, elles doivent en fixer la version des règles et les échantillons de test.
Une signature Ed25519 atteste que le message correspond à une clé publique de confiance. Elle n’assure pas la confidentialité et n’empêche pas automatiquement les attaques par rejeu. Si la clé publique provient de la même réponse, elle-même susceptible d’être remplacée, une vérification réussie ne suffit pas à établir la confiance.
Il faut également distinguer Ed25519 standard d’Ed25519ph. Calculer d’abord l’empreinte du message puis la transmettre à l’interface standard ne revient pas à utiliser la variante avec préhachage. Le protocole ne peut pas omettre ces conditions au seul motif qu’« il s’agit toujours d’Ed25519 ».
Valider un cas minimal avec un vecteur de référence public
Sur le Mac distant où les outils de développement sont déjà installés, exécutez xcrun swift --version et consignez la version de la chaîne d’outils ainsi que la sortie de sw_vers. Enregistrez le code ci-dessous dans verify.swift, puis lancez xcrun swift verify.swift. Il utilise le vecteur de test du message vide défini dans la RFC 8032 ; ces données hexadécimales publiques ne sont pas des clés métier.
import Foundation
import CryptoKit
enum FixtureError: Error {
case invalidHex
case checkFailed
}
func hex(_ text: String) throws -> Data {
let chars = Array(text.utf8)
guard chars.count % 2 == 0 else {
throw FixtureError.invalidHex
}
var bytes = [UInt8]()
for offset in stride(from: 0, to: chars.count, by: 2) {
let pair = String(
decoding: chars[offset..<(offset + 2)],
as: UTF8.self
)
guard let value = UInt8(pair, radix: 16) else {
throw FixtureError.invalidHex
}
bytes.append(value)
}
return Data(bytes)
}
func require(_ value: Bool) throws {
guard value else {
throw FixtureError.checkFailed
}
}
let publicBytes = try hex(
"d75a980182b10ab7d54bfed3c964073a0" +
"ee172f3daa62325af021a68f707511a"
)
let signature = try hex(
"e5564300c360ac729086e2cc806e828a8" +
"4877f1eb8e5d974d873e0652249015555" +
"5fb8821590a33bacc61e39701cf9b46bd" +
"25bf5f0595bbe24655141438e7a100b"
)
try require(publicBytes.count == 32)
try require(signature.count == 64)
let key = try Curve25519.Signing.PublicKey(
rawRepresentation: publicBytes
)
try require(key.isValidSignature(signature, for: Data()))
try require(!key.isValidSignature(
signature, for: Data([0x00])
))
var altered = signature
altered[altered.startIndex] ^= 1
try require(!key.isValidSignature(altered, for: Data()))
try require(!key.isValidSignature(
Data(signature.dropLast()), for: Data()
))
var rejectedKey = false
do {
_ = try Curve25519.Signing.PublicKey(
rawRepresentation: Data(publicBytes.dropLast())
)
} catch {
rejectedKey = true
}
try require(rejectedKey)
Le script ne produit aucune sortie lorsqu’il réussit. En cas d’échec, il lève une erreur et se termine avec un code de sortie non nul. Les contrôles lèvent ici des erreurs plutôt que d’utiliser assert, afin d’éviter que la compilation avec optimisation ne supprime les assertions. Vérifiez d’abord que ces données fixes passent les contrôles avant d’introduire des échantillons métier : chercher simultanément un problème réseau et un problème d’interface cryptographique multiplierait les variables.
Ramener les différences de texte à des différences d’octets
Conservez un fichier contenant le message avant signature et un autre contenant les données effectivement reçues par le client. Pour des jeux de test publics, utilisez cmp pour les comparer, puis xxd -g 1 pour repérer la première différence. Le corps d’une réponse de production peut contenir des informations sensibles et ne doit pas être versé tel quel dans des journaux partagés.
Commencez par les éléments qu’un éditeur peut modifier : présence d’un saut de ligne final supplémentaire, fins de ligne LF ou CRLF, marqueur BOM UTF-8 en début de fichier, normalisation Unicode des caractères. Deux caractères accentués visuellement identiques peuvent correspondre à des séquences d’octets différentes.
Évitez de créer les fichiers à la volée avec echo, qui peut ajouter un saut de ligne. Lisez le message directement à partir des données brutes du fichier, sans passer par une conversion en chaîne puis revenir à des données binaires. Si le protocole signe effectivement des chaînes, imposez explicitement UTF-8 et prévoyez des jeux de test distincts pour le saut de ligne final, le message vide et les caractères combinés.
Valider séparément l’enveloppe de transport
Si l’API transmet les données en Base64, décodez-les avant d’en vérifier la longueur ; 128 caractères hexadécimaux ne constituent pas une signature de 128 octets. Une clé publique brute n’est pas non plus un fichier de clé contenant des en-têtes : les données encapsulées ne doivent pas être transmises intégralement à rawRepresentation.
En cas d’échec du décodage, ne remplacez jamais le résultat par des données vides pour poursuivre la vérification. Distinguez en interne les erreurs « enveloppe invalide », « longueur incorrecte » et « signature non concordante », tout en évitant d’exposer à l’extérieur des détails d’analyse inutiles.
Constituer des contre-exemples qui révèlent les erreurs d’intégration
Le jeu minimal de tests doit comprendre : un message correct avec sa signature correcte, un message auquel un octet nul a été ajouté, une signature dont un bit a été inversé, une signature tronquée, une clé publique tronquée et une tentative de vérification de la signature d’origine avec une autre clé publique valide. Ce dernier cas doit utiliser une véritable clé publique différente et valide, et non une clé endommagée qui provoque seulement un échec à l’initialisation.
Au niveau métier, ajoutez un échantillon dont le corps est correct, mais dont la signature appartient au message précédent, afin de détecter une réutilisation erronée de résultats en cache. Si le contenu signé comprend une version, une période de validité ou un identifiant d’usage, vérifiez aussi que ces champs font réellement partie des octets signés, et qu’ils ne figurent pas uniquement dans un objet externe non signé.
Ces tests vérifient les chemins de rejet ; n’utilisez pas des altérations aléatoires à la place de jeux de test fixes. Seuls des échantillons fixes permettent à des membres travaillant sur différents nœuds ou sur des créneaux différents de reproduire exactement le même échec.
Transmettre un dossier de preuves entre équipes
Lors de la passation, consignez l’identifiant du commit, les versions du système et de la chaîne d’outils, la variante de l’algorithme, l’identifiant de la clé publique, le nombre d’octets du message et de la signature, ainsi que les résultats des tests. Les jeux de test publics peuvent être conservés avec ces éléments. Les messages métier réels doivent être conservés avec un accès contrôlé adapté à leur sensibilité ; une empreinte ne peut pas non plus remplacer le message d’origine pour vérifier sa signature.
Une fois la référence en ligne de commande validée, placez les mêmes jeux de test dans la cible de tests iOS et appelez la couche de vérification réellement utilisée par l’application. Vous couvrirez ainsi le décodage Base64, la sélection de la clé publique et le chemin de lecture du message, au lieu de simplement démontrer que CryptoKit fonctionne.
Le critère de réussite final n’est pas d’avoir « vérifié une fois » : les entrées correctes doivent réussir de façon constante, chaque contre-exemple doit être explicitement rejeté, les deux parties doivent s’accorder sans ambiguïté sur les octets signés, et l’équipe suivante doit pouvoir relancer tous les contrôles sans clé privée.
Questions fréquentes
La validation côté client exige-t-elle la clé privée ?
Non. Une clé publique de confiance, les octets du message et la signature suffisent. La clé privée de production ne doit pas être transmise à cet environnement de test.
Pourquoi deux objets JSON équivalents échouent-ils à la vérification ?
Ed25519 vérifie des octets, pas la sémantique JSON. L’ordre des clés, les espaces et la représentation Unicode changent le message. Vérifiez les octets reçus ou définissez une canonisation commune.
Choisissez un Mac cloud pour votre prochain projet
Choisissez un emplacement proche de celui où se trouve principalement votre équipe, avec un Mac mini physique qui vous est entièrement dédié. Le bureau à distance dans le navigateur nécessite un navigateur récent et une connexion stable. Vérifiez la configuration, la durée de facturation et la disponibilité avant de commander.