Reverse engineering d'un QR code pour preuve de vaccination

image


Lorsque Québec a annoncé qu'il enverrait des courriels de confirmation de vaccination à toutes les personnes vaccinées à l'aide du code QR ci-joint, mes genoux se sont un peu déformés. J'avais hâte de le démonter et de secouer la tête devant la quantité d'informations privées sur la santé qui seront sans aucun doute révélées au cours du processus.



Ma confirmation de vaccination est enfin arrivée, et le résultat est... pas mal du tout. Cependant, il y a toujours du plaisir dans les hacks à zéro connaissance, alors j'ai quand même décidé de bloguer sur mon expérience.



Ma première impression a été : "Oh mon Dieu, c'est un code QR inutilement volumineux." Il n'y a pas beaucoup d'informations répertoriées sous le code QR, ils cryptent donc probablement toutes sortes d'informations personnelles à mon insu. Tu sais, comme ce code-barres au dos de ton permis de conduire .



Naturellement, la première chose que j'ai faite a été de scanner le code à l'aide de l'application QRcode.



résultat
shc:/567629000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000413774



Intéressant. Je pensais qu'il y aurait du bon vieux JSON au format binaire, mais c'était différent. Il semble que l'encodage d'un tas de chiffres en base64 soit inefficace, mais ils ont réussi à tout mettre dans un seul code QR.



Malheureusement, c'est là que se termine la partie zéro connaissance du processus, car j'ai un indicateur assez clair de la prochaine étape : le schéma d'URI. Il est clair qu'il s'agit de communiquer avec une application sur l'appareil de la personne vérifiant le code qui s'enregistrera pour traiter ce schéma shc :



. Mais quel est ce schéma ?



Une petite recherche m'a conduit au Big Book O'URI Schemes de l' IANAshc



répertorié comme pré-enregistré sous le nom SMART Health Cards Framework. Ce n'est donc pas seulement quelque chose que le gouvernement du Québec a imaginé sur le coup, c'est en fait une partie d'un vrai projet! C'est encourageant et inattendu.



Il s'avère que ce format a une documentation complète et des objectifs de conception très raisonnables , ce que je trouve à la fois un soulagement pour le détenteur d'un tel code et un peu frustrant lorsque quelqu'un est sur le point de l'analyser dans son intégralité. Mais ce n'est pas grave ! J'ai du code et un document à suivre, alors enlevons le couvercle et regardons à l'intérieur.



Selon le document, l'utilisation du mode numérique pour coder les données de code QR fournit une densité de données légèrement plus élevée que l'utilisation du mode binaire, ce qui explique l'URI numérique géant plutôt que la chaîne codée en base64 plus sensible. La première énigme est résolue.



La longue chaîne de chiffres semble être codée à partir d'une chaîne ASCII, où chaque paire de chiffres est un nombre décimal qui est le code de caractère. Pour rendre les choses encore plus confuses, la sortie est calculée en utilisant Ord © -45 . Il est temps d'écrire un script pour inverser ce processus.



php -r '$o = ""; foreach (str_split(preg_replace("/[^0-9]/", "", file_get_contents("php://stdin")), 2) as $c) $o .= chr($c + 45); echo $o;' <input.txt | xxd

00000000: 6579 4a72 6157 5169 4f69 4a73 4d33 6c79  eyJraWQiOiJsM3ly
00000010: 5254 4632 526a 646d 6157 5270 6257 5649  RTF2RjdmaWRpbWVI
...
000003b0: 3561 6876 5265 336d 6368 7335 7836 4e49  5ahvRe3mchs5x6NI
000003c0: 4669 3556 5277                           Fi5VRw
      
      





Plusieurs choses peuvent en être tirées. Premièrement, il est évident que PHP est toujours mon langage de programmation rapide. Malheureusement, nous mettrons cette révélation personnelle de côté pour une introspection plus poussée.



D'un point de vue technique, tout ressemble désormais à des chaînes encodées en base64. Et bien sûr, le doc me dit que je devrais regarder JWS, c'est-à-dire un jeton Web signé JSON.



Je vais faire une pause et dire qu'il s'agit en fait d'un excellent cas d'utilisation pour JWT. Fondamentalement, au lieu d'un jeton dénué de sens ou d'un bloc géant de données sensibles, le concept JWT implique que je devrais m'attendre à une liste d'autorisations auxquelles j'ai droit, enveloppé dans un blob signé cryptographiquement par l'émetteur et Services sociaux).



L'avantage de ce modèle est qu'il peut être vérifié par toute personne disposant de la clé publique correspondante, même sans connexion Internet. De plus, la réponse à la question « cette personne a-t-elle le droit de monter à bord d'un avion / d'assister à un concert / de visiter une résidence pour personnes âgées ? devrait répondre directement en ligne, pas implicitement via l'API propriétaire ou un tas de champs secrets liés aux numéros de lot de vaccins, etc.



Maintenant, je n'ai pas de copie de la clé publique correspondante, mais le corps doit être signé, pas crypté, donc je suis toujours je peux le lire.



Peut-être que dans l'esprit de l'ingénierie inverse, je devrais désassembler manuellement le JWS, mais il s'agit d'une spécification assez bien documentée (et surtout bien implémentée). Je vais paresser et utiliser le package Web-token / jwt-framework Composer pour cela .



$ composer require web-token/jwt-framework
      
      







<?php
require_once(__DIR__.'/vendor/autoload.php');

use Jose\Component\Signature\Serializer\JWSSerializerManager;
use Jose\Component\Signature\Serializer\CompactSerializer;

$serializerManager = new JWSSerializerManager([
    new CompactSerializer(),
]);

$input_raw = file_get_contents('php://stdin');
$input_token = implode(
    array_map(
        function ($ord) { return chr($ord + 45); },
        str_split(preg_replace('/[^0-9]+/', '', $input_raw), 2)
    )
);

$jws = $serializerManager->unserialize($input_token);
var_dump($jws);
      
      





$ cat input.txt | php parse.php
object(Jose\Component\Signature\JWS)#5 (4) {
  ["isPayloadDetached":"Jose\Component\Signature\JWS":private]=>
  bool(false)
  ["encodedPayload":"Jose\Component\Signature\JWS":private]=>
  string(772) "hVNhb9..."
  ["signatures":"Jose\Component\Signature\JWS":private]=>
  array(1) {
    [0]=>
    object(Jose\Component\Signature\Signature)#6 (4) {
      ["encodedProtectedHeader":"Jose\Component\Signature\Signature":private]=>
      string(106) "eyJraW..."
      ["protectedHeader":"Jose\Component\Signature\Signature":private]=>
      array(3) {
        ["kid"]=>
        string(43) "l3yrE1..."
        ["zip"]=>
        string(3) "DEF"
        ["alg"]=>
        string(5) "ES256"
      }
      ["header":"Jose\Component\Signature\Signature":private]=>
      array(0) {
      }
      ["signature":"Jose\Component\Signature\Signature":private]=>
      string(64) "�Q�..."
    }
  }
  ["payload":"Jose\Component\Signature\JWS":private]=>
  string(579) "�Sao..."
}
      
      





Ainsi, nous décodons avec succès l'en-tête, mais aucun corps n'arrive. L'indice ici est "zip": "DEF" dans l'en-tête, comme indiqué également dans la spécification.



la charge utile est compressée à l'aide de l'algorithme DEFLATE (voir RFC1951) avant la signature (notez qu'il doit s'agir d'une compression DEFLATE brute, sans en-têtes zlib ou gz




Essayons:



echo json_encode(json_decode(gzinflate($jws->getPayload())), JSON_PRETTY_PRINT);
      
      





NB : nous décodons puis recodons l'objet JSON pour ajouter un espace blanc pour la lisibilité en spécifiant la constante JSON_PRETTY_PRINT



{
    "iss": "https:\/\/covid19.quebec.ca\/PreuveVaccinaleApi\/issuer",
    "iat": 1621476457,
    "vc": {
        "@context": [
            "https:\/\/www.w3.org\/2018\/credentials\/v1"
        ],
        "type": [
            "VerifiableCredential",
            "https:\/\/smarthealth.cards#health-card",
            "https:\/\/smarthealth.cards#immunization",
            "https:\/\/smarthealth.cards#covid19"
        ],
        "credentialSubject": {
            "fhirVersion": "1.0.2",
            "fhirBundle": {
                "resourceType": "Bundle",
                "type": "Collection",
                "entry": [
                    {
                        "resource": {
                            "resourceType": "Patient",
                            "name": [
                                {
                                    "family": [
                                        "Paulson"
                                    ],
                                    "given": [
                                        "Mikkel"
                                    ]
                                }
                            ],
                            "birthDate": "1987-xx-xx",
                            "gender": "Male"
                        }
                    },
                    {
                        "resource": {
                            "resourceType": "Immunization",
                            "vaccineCode": {
                                "coding": [
                                    {
                                        "system": "http:\/\/hl7.org\/fhir\/sid\/cvx",
                                        "code": "208"
                                    }
                                ]
                            },
                            "patient": {
                                "reference": "resource:0"
                            },
                            "lotNumber": "xxxxxx",
                            "status": "Completed",
                            "occurrenceDateTime": "2021-xx-xxT04:00:00+00:00",
                            "location": {
                                "reference": "resource:0",
                                "display": "xxxxxxxxxxxxxxxxxx"
                            },
                            "protocolApplied": {
                                "doseNumber": 1,
                                "targetDisease": {
                                    "coding": [
                                        {
                                            "system": "http:\/\/browser.ihtsdotools.org\/?perspective=full&conceptId1=840536004",
                                            "code": "840536004"
                                        }
                                    ]
                                }
                            },
                            "note": [
                                {
                                    "text": "PB COVID-19"
                                }
                            ]
                        }
                    }
                ]
            }
        }
    }
}
      
      





Il contient un peu plus d'informations personnelles que ce qui est strictement nécessaire, même si je pense que combiner le nom et la date de naissance avec une pièce d'identité avec photo est un processus judicieux. Ils fournissent également des informations spécifiques sur les vaccins plutôt que des approbations spécifiques comme je l'espérais. Encore une fois, cela le rend d'autant plus utilisable dans toutes les juridictions et élimine le besoin de rééditer le JWS chaque fois que la politique change, ce qui, dans le cas du Québec, se produit environ deux fois par semaine.



Tout au long de cette analyse, je me suis demandé ce qui pouvait empêcher quelqu'un de simplement présenter une preuve parfaitement valide de la vaccination d'une autre personne. Étant donné que le corps entier est signé cryptographiquement, vous ne pouvez pas modifier la preuve de vaccination de quelqu'un d'autre pour ajouter votre nom, ce qui signifie que combiner la preuve de vaccination avec une pièce d'identité avec photo est un plan parfaitement raisonnable. Ce sera certainement le cas dans les aéroports, mais je doute fort que dans les sites sportifs, etc. E. demandera une deuxième pièce d'identité. Ils scanneront simplement le code QR, verront une coche sur leur appareil et passeront au suivant.



Une réflexion d'adieu : alors que mon processus visait à déterminer lesquelles de mes données personnelles sont encodées dans un code QR, le modèle JWT est connu pour être facile à gâcher, soit en oubliant de valider avant d'analyser les données, soit en autorisant les données non signées. jetons ... Si les implémentations ne respectent pas une liste blanche centrale de signataires autorisés, il serait trivialement facile de créer un jeton parfaitement valide que vous signez avec votre propre clé. Comme toujours, la sécurité du modèle dépend vraiment de la rigueur avec laquelle la partie de confiance applique la norme.



Cependant, il s'avère que les seules informations personnelles sont exactement les informations contenues dans le document PDF complet sur les vaccinations : nom, date de naissance, sexe (pour une raison quelconque), ainsi que des informations sur la date et les doses spécifiques que le propriétaire reçu à ce jour. Une fois que vous êtes à l'aise avec les implications en matière de confidentialité de la présentation de votre permis de conduire dans un bar, vous n'avez plus à vous soucier de devoir présenter une preuve de vaccination.



Le code est un tas d'ordures, mais si vous voulez voir ce qu'il y a dans votre propre code QR, vous pouvez consulter le référentiel GitHub pour cet article.



All Articles