Ingeniería inversa de un código QR para prueba de vacunación

imagen


Cuando Quebec anunció que enviaría correos electrónicos de confirmación de vacunación a todos los que fueron vacunados usando el código QR adjunto, mis rodillas se doblaron un poco. Estaba ansioso por desarmarlo y negar con la cabeza ante la cantidad de información médica privada que sin duda se revelará en el proceso.



Mi confirmación de vacunación finalmente ha llegado y el resultado es ... nada malo. Sin embargo, siempre hay algo de diversión en los trucos de conocimiento cero, así que decidí escribir un blog sobre mi experiencia de todos modos.



Mi primera impresión fue: "Dios mío, este es un código QR innecesariamente grande". No hay mucha información en el código QR, por lo que probablemente cifren todo tipo de información personal sin mi conocimiento. Ya sabes, como ese código de barras en la parte de atrás de tu licencia de conducir .



Naturalmente, lo primero que hice fue escanear el código usando la aplicación QRcode.



resultado
shc:/567629000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000413774



Interesante. Pensé que habría un buen JSON antiguo en formato binario, pero era diferente. Parece que codificar un montón de dígitos en base64 es ineficiente, pero lograron meter todo en un código QR.



Desafortunadamente, aquí es donde termina la parte de conocimiento cero del proceso, porque tengo un indicador bastante claro de dónde ir a continuación: el esquema URI. Está claro que esto es para la comunicación con alguna aplicación en el dispositivo de la persona que verifica el código que se registrará para procesar este esquema shc :



. Pero, ¿cuál es este esquema?



Una pequeña búsqueda me llevó al Big Book O 'URI Schemes de IANA donde shc



enumerados como prerregistrados con el nombre SMART Health Cards Framework. Así que no es solo algo que se le ocurrió al gobierno de Quebec sobre la marcha, ¡en realidad es parte de un proyecto real! Esto es alentador e inesperado.



Resulta que este formato tiene una documentación extensa y objetivos de diseño muy razonables , lo que encuentro tanto un alivio para el poseedor de dicho código como un poco frustrante cuando alguien está a punto de analizarlo en su totalidad. ¡Pero no importa! Tengo un código y un documento a seguir, así que quitemos la tapa y echemos un vistazo al interior.



Según el documento, el uso del modo numérico para codificar los datos del código QR proporciona una densidad de datos ligeramente mayor que el uso del modo binario, lo que explica el número gigante de URI en lugar de la cadena codificada en base64, que es más sensible. El primer acertijo está resuelto.



La cadena larga de números parece estar codificada a partir de una cadena ASCII, donde cada par de dígitos es un número decimal que es el código de carácter. Para hacer las cosas aún más confusas, la salida se calcula usando Ord © -45 . Es hora de escribir un guión para revertir este proceso.



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
      
      





Se pueden aprender varias cosas de esto. Primero, es obvio que PHP sigue siendo mi lenguaje de programación rápido. Lamentablemente, dejaremos a un lado esta revelación personal para una mayor introspección.



Desde un punto de vista técnico, ahora todo parece cadenas codificadas en base64. Y, por supuesto, el documento me dice que debería estar mirando JWS, es decir, un token web firmado por JSON.



Haré una pausa y diré que este es en realidad un gran caso de uso para JWT. Básicamente, en lugar de un token sin sentido o un bloque gigante de datos confidenciales, el concepto JWT implica que debería esperar una lista de permisos a los que tengo derecho, envuelto en un blob que está firmado criptográficamente por el emisor (en este caso, Quebec Santé et Services sociaux).



Lo bueno de este modelo es que puede ser verificado por cualquier persona con la clave pública correspondiente, incluso sin conexión a Internet. Además, la respuesta a la pregunta "¿tiene esta persona derecho a abordar un avión / asistir a un concierto / visitar una residencia para personas mayores?" debería responder directamente en línea, no implícitamente implícito a través de la API propietaria o un montón de campos secretos relacionados con los números de lote de vacunas, etc.



Ahora no tengo una copia de la clave pública correspondiente, pero el cuerpo debe estar firmado, no cifrado, así que todavía puedo leerlo.



Quizás, en el espíritu de la ingeniería inversa, debería desmontar manualmente el JWS, pero esta es una especificación bastante bien documentada (y lo que es más importante, bien implementada). Me voy a quedar perezoso y usaré el paquete web-token / jwt-framework Composer para eso .



$ 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..."
}
      
      





Entonces, decodificamos con éxito el encabezado, pero no llega ningún cuerpo. La sugerencia aquí es "zip": "DEF" en el encabezado, como también se indica en la especificación.



la carga útil se comprime utilizando el algoritmo DEFLATE (consulte RFC1951) antes de firmar (tenga en cuenta que debe ser una compresión DEFLATE sin formato, sin encabezados zlib o gz




Intentemos:



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





NB: decodificamos y luego recodificamos el objeto JSON para agregar espacios en blanco para facilitar la lectura especificando 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"
                                }
                            ]
                        }
                    }
                ]
            }
        }
    }
}
      
      





Hay un poco más de información personal de la estrictamente necesaria, aunque creo que combinar el nombre y la fecha de nacimiento con una identificación con foto es un proceso sensato. También brindan información específica sobre vacunas en lugar de aprobaciones específicas como esperaba. Una vez más, esto lo hace aún más utilizable en todas las jurisdicciones y elimina la necesidad de volver a publicar el JWS cada vez que cambia la política, lo que en el caso de Quebec ocurre aproximadamente dos veces por semana.



A lo largo de este análisis, me he preguntado qué podría evitar que alguien simplemente presente una prueba perfectamente válida de la vacunación de otra persona. Dado que todo el cuerpo está firmado criptográficamente, no puede cambiar la prueba de vacunación de otra persona para agregar su nombre, lo que significa que combinar la prueba de vacunación con una identificación con foto es un plan perfectamente razonable. Este será sin duda el caso en los aeropuertos, pero dudo mucho que en las instalaciones deportivas, etc. E. Le pedirá una segunda identificación. Simplemente escanearán el código QR, verán una marca de verificación en su dispositivo y pasarán al siguiente.



Un pensamiento de despedida: si bien mi proceso estaba orientado a averiguar cuál de mis datos personales está codificado en un código QR, el modelo JWT es conocido por ser fácil de estropear, ya sea olvidándose de validar antes de analizar los datos o permitiendo que no esté firmado tokens ... Si las implementaciones no respetan una lista blanca central de firmantes autorizados, sería trivialmente fácil crear un token perfectamente válido que firme con su propia clave. Como siempre, la seguridad del modelo realmente depende de qué tan rigurosamente aplique el estándar la parte que confía.



Sin embargo, resulta que la única información personal es exactamente la información que está contenida en el documento PDF completo sobre vacunas: nombre, fecha de nacimiento, sexo (por alguna razón), así como información sobre la fecha y dosis específicas que el propietario recibido en la actualidad. Una vez que se sienta cómodo con las implicaciones de privacidad de presentar su licencia de conducir en un bar, ya no tendrá que preocuparse por que le pidan que muestre un comprobante de vacunación.



El código es un montón de basura, pero si desea ver qué hay en su propio código QR, puede consultar el repositorio de GitHub para esta publicación.



All Articles