Skip to content

Instantly share code, notes, and snippets.

@zunda
Last active December 14, 2022 07:07
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zunda/f115745da9eb3546c11f9814972c3933 to your computer and use it in GitHub Desktop.
Save zunda/f115745da9eb3546c11f9814972c3933 to your computer and use it in GitHub Desktop.
Read the payload from a SMART Health Card
#!/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"]}"
{
"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