Announcement

Collapse
No announcement yet.

Vaccine Passport QR-Code

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

  • Vaccine Passport QR-Code

    Disclaimer: this is not about the merits or not of the vaccine, nor about the merits or not of a “Vaccine Passport QR-Code”. I’m just trying to understand the mechanics behind the QR-Code, and what information it has. Please let’s keep the discussion on the programing. Thanks.

    We recently received a “Vaccine Passport QR-Code” here in Quebec, although it is not used anywhere yet. I wanted to know exactly what type of personal information was hiding behind the QR-Code. It involved many aspects I had never encountered before. So I thought I would share.

    After getting my QR-Code document, Basically a human readable PDF with, Name, gender, date of birth, Vaccine Name, Vaccine code, Lot number, date it was given, Location it was given, and of course a QR-Code.

    My first instinct was to scan the QR-Code with my phone (many apps available). I got something like this.

    shc:/56762909524320603460292437404460312229595326546034602925407728043360287028647167452228092861…many more here..7205910610777322474

    Having never seen a shc:/ prefix, and not knowing what the numbers meant, made me even more curious. It’s a series of base 10 numbers, how are they used and why? So I googled, and found that shc:/ is a SMART Health Cards. SMART Health Cards Framework info here (https://smarthealth.cards)

    So here in Quebec they decided to use the SMART Health Cards Framework.

    Step 1: Convert the Numeric QR-Code
    https://smarthealth.cards/#every-hea...d-in-a-qr-code
    The reason to use numbers, In Numeric Mode, 20% more data can fit in a given QR, vs Binary Mode (has to do with QRcode encoding, 6bits vs 8bits)
    To Convert back from Numeric, we need to take every 2 digits and add 45 to them and use the CHR$() of that number.

    FOR lcount = 1 TO LEN(sTemp) STEP 2 ' 2 digits + 45, convert to Chr$
    sBuffer += CHR$(VAL(MID$(sTemp,lcount,2)) + 45)
    NEXT lcount


    We get a JWT (JSON Web Token https://tools.ietf.org/html/rfc7519), which is a 3 part base64url encoded string, each part separated by a period (no white spaces [Space, Tab, CR, LF]). The 3 Parts are a Header, Payload, and Signature
    (No line feeds).

    eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.1VNNaxsxEP0rZnJde1ebBOO9xSm0LqEUmvZSfJC1Y6-KPhZJa-qG_e-d0TolhzhQ6KW6SXrz5s170hPoGKGBLqU <more here> F7Nr_uL_3sYl3460fgM.YbNzeh49SR2pir4UmQ-2mKpIQMS_lFm6FDDQjkU3390cawI9Kv3n8tA6CqYoM4lHdfELNqUZlqR7RAh7j4zMEw

    Step 2: Convert Base64URL to string
    We separate the 3 parts based on the period delimiter (period is not used in Base64URL encoding so it’s a good separator)

    lCount2 = PARSECOUNT(sBuffer, ".") ' base64URL parts separated by a period
    DIM asTemp (1 TO lCount2) ' Dim array 1 to 3 --- 1=Header 2=Payload 3=Signature
    PARSE sBuffer, asTemp(), "." ' So we can Parse on period into our array.


    Then we Base64URL_Decode each part.
    Base64URL is similar to Base64 but the last 2 characters in the Table “+/” (Plus, Forward Slash) are replaced by “-_” (Minus, Underscore), and we do not use padding (no “=” Equal). https://datatracker.ietf.org/doc/html/rfc4648

    Part 1 Header info.

    asTemp(1) = UTF8TOCHR$(Base64URL_Decode(asTemp(1),0))

    The Header becomes

    {"zip":"DEF","alg":"ES256","kid":"3Kfdg-XwP-7gXyywtUfUADwBumDOPKMQx-iELL11W9s"}

    This tells us that the Payload is also compressed (zip), and that the Signature is ES256 (SHA256 with ECDSA an Asymmetric Key Cryptography algorithm Elliptic Curve Digital Signature Algorithm using P-256 and SHA-256) https://ldapwiki.com/wiki/ES256 and finally the kid we will need to match to get the Public Key (more on this later)

    Part 2 The Payload.
    We need to UnZip but it uses a RAW format compression (raw encoding with no header or trailer at all). To keep the payload as small as possible, to fit as a QR-Code.

    sTemp = Base64URL_Decode(asTemp(2))
    lSiz2 = LEN(stemp)
    lresult= InflateRawStrm2 (sTemp,lSiz2) ' lSiz2 size compressed, if success you get size uncompressed in lSiz2
    asTemp(2) = UTF8TOCHR$(sTemp) ' sTemp gets uncompressed result


    So at this point we have the payload decoded and unzipped, which gives us a JSON record with the content I wanted to see. (No line feeds) Line feed added for clarity

    '
    Code:
    {
        "iss": "https://c19.cards/issuer",
        "nbf": 1591037940,
        "vc": {
            "type": [
                "https://smarthealth.cards#covid19",
                "https://smarthealth.cards#health-card",
                "https://smarthealth.cards#immunization"
            ],
            "credentialSubject": {
                "fhirVersion": "4.0.1",
                "fhirBundle": {
                    "resourceType": "Bundle",
                    "type": "collection",
                    "entry": [
                        {
                            "fullUrl": "resource:0",
                            "resource": {
                                "resourceType": "Patient",
                                "name": [
                                    {
                                        "family": "Anyperson",
                                        "given": [
                                            "Johnathan",
                                            "Biggleston III"
                                        ]
                                    }
                                ],
                                "birthDate": "1951-01-20"
                            }
                        },
                        {
                            "fullUrl": "resource:1",
                            "resource": {
                                "resourceType": "Immunization",
                                "meta": {
                                    "security": [
                                        {
                                            "code": "IAL1.2"
                                        }
                                    ]
                                },
                                "status": "completed",
                                "vaccineCode": {
                                    "coding": [
                                        {
                                            "system": "http://hl7.org/fhir/sid/cvx",
                                            "code": "207"
                                        }
                                    ]
                                },
                                "patient": {
                                    "reference": "resource:0"
                                },
                                "occurrenceDateTime": "2021-01-01",
                                "location": {
                                    "reference": "resource:3"
                                },
                                "performer": [
                                    {
                                        "actor": {
                                            "display": "ABC General Hospital"
                                        }
                                    }
                                ],
                                "lotNumber": "Lot #0000001"
                            }
                        },
                        {
                            "fullUrl": "resource:2",
                            "resource": {
                                "resourceType": "Immunization",
                                "status": "completed",
                                "vaccineCode": {
                                    "coding": [
                                        {
                                            "system": "http://hl7.org/fhir/sid/cvx",
                                            "code": "207"
                                        }
                                    ]
                                },
                                "patient": {
                                    "reference": "resource:0"
                                },
                                "occurrenceDateTime": "2021-01-29",
                                "performer": [
                                    {
                                        "actor": {
                                            "display": "ABC General Hospital"
                                        }
                                    }
                                ],
                                "lotNumber": "Lot #0000007"
                            }
                        }
                    ]
                }
            }
        }
    }
    '
    Part 3 Signature
    The signature as we find from the Part 1 Header, is ES256 (SHA256 with ECDSA an Asymmetric Key Cryptography algorithm Elliptic Curve Digital Signature Algorithm using P-256 and SHA-256).

    All we need to do is Base64URL_Decode, it should be 64 byte. To be used to verify that the JWT has not been altered.

    Sadly, although I understand what needs to be done, I could not figure out how to perform the ECDSA verification.

    This is what I understood. The 64byte value from signature gets split into two 32 bytes.

    Signature= Base64URL_Decode(YbNzeh49SR2pir4UmQ-2mKpIQMS_lFm6FDDQjkU3390cawI9Kv3n8tA6CqYoM4lHdfELNqUZlqR7RAh7j4zMEw)

    (Values shown as HEX for Clarity, but they are used their binary form)
    61B3737A1E3D491DA98ABE14990FB698AA4840C4BF9459BA1430D08E4537DFDD1C6B023D2AFDE7F2D03A0AA62833894775F10B36A51996A47B44087B8F8CCC13
    Split:
    R: 61B3737A1E3D491DA98ABE14990FB698AA4840C4BF9459BA1430D08E4537DFDD
    S: 1C6B023D2AFDE7F2D03A0AA62833894775F10B36A51996A47B44087B8F8CCC13


    They split them in 2 but it may be a Java 32 bit requirement.

    From the Payload we know where to get the Public Key, from the iss object.

    "iss": "https://c19.cards/issuer",

    We fetch a JSON response from https://c19.cards/issuer to which we append /.well-known/jwks.json
    https://c19.cards/issuer/.well-known/jwks.json

    We get:

    '
    Code:
    {
      "keys": [
        {
          "kty": "EC",
          "kid": "3Kfdg-XwP-7gXyywtUfUADwBumDOPKMQx-iELL11W9s",
          "use": "sig",
          "alg": "ES256",
          "crv": "P-256",
          "x": "11XvRWy1I2S0EyJlyf_bWfw_TQ5CJJNLw78bHXNxcgw",
          "y": "eZXwxvO1hvCY0KucrPfKo7yAyMT6Ajc3N7OkAB6VYy8"
        },
        {
          "kty": "EC",
          "kid": "7rIUtD5K5Izrptr3ywgESTxhgJtJSeb06V42hg7WUDs",
          "use": "enc",
          "alg": "ECDH-ES",
          "crv": "P-256",
          "x": "Lhaq5B4HhCDJSSQU_rJLShIIr6S5PxfXKTGKfKSP_CY",
          "y": "EaxJ3zkqswLnw5GOf95A3atOTToVYmZPrp_t7WBcHcM"
        }
      ]
    }
    
    '
    In this case we have 2 public keys available. So using the JWT Header “kid” we match the proper one
    "kid": "3Kfdg-XwP-7gXyywtUfUADwBumDOPKMQx-iELL11W9s"

    And these should be our Public Key
    "x": "11XvRWy1I2S0EyJlyf_bWfw_TQ5CJJNLw78bHXNxcgw",
    "y": "eZXwxvO1hvCY0KucrPfKo7yAyMT6Ajc3N7OkAB6VYy8"


    Public Key = Base64URL_decode(11XvRWy1I2S0EyJlyf_bWfw_TQ5CJJNLw78bHXNxcgw) + Base64URL_decode(eZXwxvO1hvCY0KucrPfKo7yAyMT6Ajc3N7OkAB6VYy8)


    So using the 3 items below we should be able to verify the JWT:
    1. the Header(Part 1) + “.” + Payload(Part 2) base64URL Encoded
    2. the Signature(Part 3) Base64URL_Decoded
    3. the Public key


    And that is as far as I could take it; I’m stuck with the crypto part.

    Maybe a forum member with better understanding of ECDSA can show me what to do?


    Here is all files required for this endeavor.
    • CovidQRcode.bas
    • JSONParser.inc
    • Sample3.txt
    • zlib1.dll

    Covid QRCODE.zip

  • #2
    Sheesh. I thought Quebec provincial sales tax was hard enough!

    (FWIW, I had no side effects from my two shots of Pfizer.)
    Michael Mattias
    Tal Systems (retired)
    Port Washington WI USA
    [email protected]
    http://www.talsystems.com

    Comment

    Working...
    X