Vérifier les signatures des requêtes iOS sur Mac cloud : HMAC et rejeu
Après l’intégration d’une API de signature dans une application multiplateforme développée sous Windows, les requêtes simples passent, mais celles qui contiennent des paramètres de requête en chinois ou un corps JSON échouent. Exécuter le module de signature seul sur un Mac cloud, valider d’abord le contrat au niveau des octets, puis le réintégrer au projet iOS permet de cerner le problème plus facilement que de multiplier les essais d’appels depuis un bureau à distance.
Nous allons créer un script de validation hors ligne avec Swift et CryptoKit. Il ne nécessite ni API réelle, ni clé de production, ni certificat de signature. Le client et le serveur doivent conserver le même jeu de données de test.
Définir le contrat
HMAC-SHA256 traite des octets, pas des requêtes qui « semblent identiques ». Il faut d’abord convenir de six champs : la version du protocole, la méthode, la cible de la requête, l’horodatage, le nonce et l’empreinte SHA-256 du corps.
Dans cet exemple, la méthode est toujours en majuscules ; la cible comprend le chemin et la chaîne de requête brute, sans le fragment ; l’horodatage est un nombre entier de secondes en notation décimale ; un nouveau nonce est généré pour chaque requête. L’empreinte du corps est représentée en hexadécimal minuscule.
Ne triez pas les paramètres de requête de votre propre initiative. C’est au protocole de l’API de définir les équivalences éventuelles entre clés répétées, valeurs vides, encodages en pourcentage et ordres des paramètres. Le client doit signer après l’encodage définitif, tandis que le serveur doit récupérer la représentation convenue avant que le framework ne réécrive le chemin ou les paramètres.
HMAC atteste que le message correspond à la clé. Il ne chiffre pas le corps et ne bloque pas automatiquement les attaques par rejeu. Il ne remplace pas le chiffrement du transport et ne transforme pas une clé partagée intégrée au paquet de l’application en justificatif d’identité fiable.
Figer les octets
Ne vous contentez pas de séparer les champs par des sauts de ligne : un saut de ligne présent dans un champ pourrait rendre le résultat ambigu. Dans cet exemple, chaque champ UTF-8 est précédé d’une longueur entière non signée sur huit octets, en ordre gros-boutiste, puis suivi de son contenu. Le serveur doit appliquer exactement la même règle.
Enregistrez le contenu suivant dans request-signing.swift. Sur le Mac cloud, exécutez d’abord xcrun swift --version pour vérifier la chaîne d’outils, puis lancez xcrun swift request-signing.swift. Le script se termine normalement en cas de succès et renvoie un code de sortie non nul si une assertion échoue.
import Foundation
import CryptoKit
func hex<S: Sequence>(_ bytes: S) -> String
where S.Element == UInt8 {
bytes.map { String(format: "%02x", $0) }.joined()
}
func frame(_ fields: [String]) -> Data {
var output = Data()
for field in fields {
let bytes = Data(field.utf8)
var length = UInt64(bytes.count).bigEndian
withUnsafeBytes(of: &length) {
output.append(contentsOf: $0)
}
output.append(bytes)
}
return output
}
func canonical(
target: String,
body: Data,
timestamp: String,
nonce: String
) -> Data {
frame([
"request-v1",
"POST",
target,
timestamp,
nonce,
hex(SHA256.hash(data: body))
])
}
let vectorKey = SymmetricKey(
data: Data(repeating: 0x0b, count: 20)
)
let vector = HMAC<SHA256>.authenticationCode(
for: Data("Hi There".utf8),
using: vectorKey
)
precondition(
hex(vector) ==
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
)
let key = SymmetricKey(data: Data(repeating: 0x42, count: 32))
let body = Data(#"{"count":1}"#.utf8)
let target = "/v1/orders?a=1&b=2"
let timestamp = "1700000000"
let nonce = "fixture-nonce-001"
let message = canonical(
target: target, body: body,
timestamp: timestamp, nonce: nonce
)
let tag = Data(HMAC<SHA256>.authenticationCode(
for: message, using: key
))
func accepts(_ candidate: Data) -> Bool {
HMAC<SHA256>.isValidAuthenticationCode(
tag, authenticating: candidate, using: key
)
}
precondition(accepts(message))
precondition(!accepts(canonical(
target: target, body: Data(#"{"count":2}"#.utf8),
timestamp: timestamp, nonce: nonce
)))
precondition(!accepts(canonical(
target: "/v1/orders?b=2&a=1", body: body,
timestamp: timestamp, nonce: nonce
)))
precondition(!accepts(canonical(
target: target, body: body,
timestamp: timestamp, nonce: "fixture-nonce-002"
)))
La clé, l’horodatage et le nonce fixes sont réservés aux données de test. Le vecteur de référence vérifie l’appel à l’algorithme cryptographique ; les assertions suivantes vérifient que les champs du protocole participent effectivement à la signature. Ces deux catégories de tests ne se remplacent pas.
Couvrir les différences
Lors de l’intégration à l’application, générez une seule fois le Data du corps définitif et utilisez-le à la fois pour calculer l’empreinte et pour envoyer la requête réseau. Ne réencodez pas le JSON après la signature et ne laissez pas un middleware remplacer le corps silencieusement.
Vérifier l’encodage
Ajoutez trois groupes de données de test : des paramètres contenant du chinois et des émojis, des clés de requête répétées, et un corps vide comparé à un objet JSON vide. Pour chaque groupe, conservez le contenu des champs, la représentation hexadécimale des octets canonisés et la signature attendue. Chaque côté doit lire ces données séparément ; la fonction testée ne doit jamais servir à produire les résultats attendus à la volée.
Distinguez bien une chaîne vide, un champ absent et la chaîne "null". Si le protocole interdit une entrée, rejetez-la avant le calcul HMAC au lieu de lui substituer discrètement une valeur par défaut.
Vérifier le transport
Un proxy inverse peut réécrire le préfixe du chemin, et un framework peut décoder %20 et le signe plus en un même espace. Précisez si la signature porte sur la cible externe de la requête ou sur la route interne, puis capturez les octets à la frontière correspondante.
Pour le diagnostic, ne journalisez que les données de test ou des empreintes expurgées des informations sensibles. Les corps de production, les en-têtes d’authentification et les clés ne doivent pas figurer dans les journaux de compilation. Ne transmettez pas non plus les clés comme arguments de ligne de commande.
Bloquer le rejeu
Même après validation de la signature, le serveur doit contrôler le format de l’horodatage et l’écart autorisé. La durée de la fenêtre de validité dépend des exigences métier. Les tests doivent couvrir au minimum quatre cas : une valeur dans la fenêtre, une valeur exactement à la limite, une valeur juste au-delà et une date future.
Effectuez ensuite une écriture atomique avec le couple « identifiant du justificatif d’authentification + nonce » comme clé de déduplication. Conservez cette entrée jusqu’à ce que la requête ne puisse plus satisfaire le contrôle temporel. Une lecture suivie d’une insertion crée une condition de concurrence : deux requêtes simultanées pourraient être acceptées. Il faut donc s’appuyer sur une écriture conditionnelle atomique ou une contrainte d’unicité.
L’ancien horodatage du script hors ligne ne convient pas à une validation en ligne. Les requêtes en ligne doivent utiliser l’heure actuelle et un nouveau nonce. La réutilisation éventuelle de la clé d’idempotence métier lors d’une nouvelle tentative doit faire l’objet d’un accord distinct : la déduplication des nonces ne garantit pas l’idempotence des commandes.
Intégrer les contrôles de validation
Intégrez d’abord le script aux vérifications des commits, puis utilisez les mêmes données de test dans les tests serveur. En plus des modifications du corps, ajoutez des cas qui doivent être rejetés : clé incorrecte, signature tronquée et encodage de signature invalide. Le destinataire doit utiliser la fonction de vérification de la bibliothèque cryptographique, et non une simple comparaison d’égalité entre chaînes.
Enfin, envoyez simultanément deux requêtes valides strictement identiques et vérifiez qu’une seule atteint le traitement métier. Simulez ensuite une nouvelle tentative légitime dans la fenêtre de validité et vérifiez qu’elle produit le résultat d’idempotence convenu.
Il suffit de conserver la version de la chaîne d’outils, celle des données de test et les numéros des assertions en échec. La validation ne se résume pas à « une requête a réussi » : les deux côtés doivent calculer la même signature pour les mêmes octets et rejeter les entrées invalides comme les requêtes répétées de manière cohérente et reproductible.
Questions fréquentes
Un HMAC valide empêche-t-il le rejeu ?
Non. Une requête signée intacte peut être envoyée à nouveau. Le serveur doit vérifier la fenêtre temporelle et dédupliquer atomiquement le couple identifiant des accès et nonce. Les reprises métier exigent un contrat d’idempotence distinct.
Faut-il décoder puis réencoder le JSON avant de le signer ?
Pas dans cette méthode. Le client calcule le condensat des octets réellement envoyés, et le serveur celui du corps brut reçu. Un réencodage peut modifier l’ordre des clés, les espaces ou la représentation des nombres.
Louez un Mac dans le cloud
Louez un ordinateur physique dédié à la journée, à la semaine, au mois ou au trimestre. Planifiez vos builds selon votre projet ; la location s’arrête à l’échéance sans renouvellement.
Choisir un modèle