Last active
December 14, 2022 07:07
-
-
Save zunda/f115745da9eb3546c11f9814972c3933 to your computer and use it in GitHub Desktop.
Read the payload from a SMART Health Card
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/ruby | |
# | |
# usage: echo shc:/0123..(decoded from a QR code) | ruby shc-payload.rb | jq | |
# Prints the payload from SMART Health Card on QR code | |
# https://spec.smarthealth.cards/ | |
# | |
# Copyright 2021 by zunda <zundan at gmail.com> | |
# | |
# Permission is granted for use, copying, modification, distribution, | |
# and distribution of modified versions of this work as long as the | |
# above copyright notice is included. | |
# | |
require "base64" | |
require "json" | |
require "zlib" | |
require "open-uri" | |
require "openssl" | |
class SmartHealthCard | |
attr_reader :header, :payload, :signature | |
def initialize(encoded) | |
x = "" | |
encoded.scan(%r|\s*shc:/(?:\d/\d/)?(\d+)|).each do |c| | |
# blindly scan through possibly chunked QR data | |
x += c[0] | |
end | |
# Followed | |
# https://github.com/dvci/health_cards/blob/main/lib/health_cards/jws.rb | |
# and health_cards-0.0.2 gem | |
@header_encoded, @payload_encoded, signature_encoded = x.scan(/\d\d/).map{|d| (d.to_i + 45).chr}.join.split(".") | |
@header = JSON.parse(Base64.urlsafe_decode64(@header_encoded)) | |
@payload = JSON.parse(Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(Base64.urlsafe_decode64(@payload_encoded))) | |
@signature = Base64.urlsafe_decode64(signature_encoded) | |
end | |
def pubkey | |
unless @pubkey | |
iss = payload["iss"] | |
raise "Issuer is not specified in payload" unless iss | |
@pubkey = JSON.parse(URI.open("#{iss}/.well-known/jwks.json").read) | |
end | |
@pubkey | |
end | |
def verify | |
# Followed | |
# https://github.com/dvci/health_cards/blob/main/lib/health_cards/key.rb | |
# https://github.com/dvci/health_cards/blob/main/lib/health_cards/public_key.rb | |
group = OpenSSL::PKey::EC::Group.new('prime256v1') | |
ec_key = OpenSSL::PKey::EC.new(group) | |
byte_size = (ec_key.group.degree + 7) / 8 | |
sig_bytes = signature[0..(byte_size - 1)] | |
sig_char = signature[byte_size..] || '' | |
sig_asn1 = OpenSSL::ASN1::Sequence.new( | |
[sig_bytes, sig_char].map{|i| | |
OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(i, 2)) | |
} | |
).to_der | |
pubkey_jwk = pubkey.fetch("keys")&.find{|k| k["kid"] == header["kid"]} | |
raise "Public key not found" unless pubkey_jwk | |
pubkey_bn = ['04'].pack('H*') + Base64.urlsafe_decode64(pubkey_jwk["x"]) + Base64.urlsafe_decode64(pubkey_jwk["y"]) | |
ec_key.public_key = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(pubkey_bn, 2)) | |
signing_input = "#{@header_encoded}.#{@payload_encoded}" | |
return ec_key.verify(OpenSSL::Digest.new('SHA256'), sig_asn1, signing_input) | |
end | |
end | |
c = SmartHealthCard.new(ARGF.read) | |
puts c.payload.to_json | |
$stderr.puts "#{c.verify ? "Verified" : "Not verified"} by #{c.payload["iss"]}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"iss": "https://travel.hawaii.gov", | |
"nbf": 1631672860, | |
"vc": { | |
"type": [ | |
"https://smarthealth.cards#health-card", | |
"https://smarthealth.cards#immunization", | |
"https://smarthealth.cards#covid19" | |
], | |
"credentialSubject": { | |
"fhirVersion": "4.0.1", | |
"fhirBundle": { | |
"resourceType": "Bundle", | |
"type": "collection", | |
"entry": [ | |
{ | |
"fullUrl": "resource:0", | |
"resource": { | |
"resourceType": "Patient", | |
"name": [ | |
{ | |
"family": "(名字)", | |
"given": [ | |
"(名前)" | |
] | |
} | |
], | |
"birthDate": "(誕生日yyyy-mm-dd)" | |
} | |
}, | |
{ | |
"fullUrl": "resource:1", | |
"resource": { | |
"resourceType": "Immunization", | |
"status": "completed", | |
"vaccineCode": { | |
"coding": [ | |
{ | |
"system": "http://hl7.org/fhir/sid/cvx", | |
"code": "208" | |
} | |
] | |
}, | |
"patient": { | |
"reference": "resource:0" | |
}, | |
"occurrenceDateTime": "(1度目接種日yyyy-mm-dd)", | |
"lotNumber": "(ロット番号)" | |
} | |
}, | |
{ | |
"fullUrl": "resource:2", | |
"resource": { | |
"resourceType": "Immunization", | |
"status": "completed", | |
"vaccineCode": { | |
"coding": [ | |
{ | |
"system": "http://hl7.org/fhir/sid/cvx", | |
"code": "208" | |
} | |
] | |
}, | |
"patient": { | |
"reference": "resource:0" | |
}, | |
"occurrenceDateTime": "(2度目接種日yyyy-mm-dd)", | |
"lotNumber": "(ロット番号)" | |
} | |
} | |
] | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment