WWDC Quick Look 💓 By SwiftGGTeam
Explore Verifiable Health Records

Explore Verifiable Health Records

元の動画を見る

ハイライト

iOS 15 では、ヘルス アプリで、SMART Health Cards 仕様に基づいて検証可能な健康記録 (Verifiable Health Record) を保存および共有できます。ユーザーは、QR コードをスキャンするかファイルをダウンロードすることで、ワクチン接種記録、検査結果、その他の認証情報をインポートできます。これらの記録は暗号的に署名されており、第三者は CryptoKit を使用してその信頼性を検証できます。開発者が使用できるのは、HKVerifiableClinicalRecordQueryユーザーにこれらの記録を共有するよう依頼してください。

主な内容

検証可能な健康記録とは何ですか?

00:23

検証可能な医療記録は、医療発行者 (病院、研究所など) によって発行されるデジタル認証情報であり、次の内容が含まれます。

  • 複数の FHIR リソース (患者リソース + 臨床リソース)
  • SMART Health Card形式のパッケージ
  • JSON Web Signature (JWS) 暗号署名

通常の健康記録との違い:

  • 発行者による署名があり、誰でも真正性を確認できます
  • 必要な情報のみが含まれ、データは最小限に抑えられます
  • オフラインで検証できるため、発行者のサーバーに問い合わせる必要はありません

インポート方法

03:55

ユーザーは次の 3 つの方法でインポートできます。

  1. Health Records Connect (米国、英国、カナダのみ)
  • ヘルスケアアプリで健康記録が有効な医療機関とつながります
  • 検証可能な記録を自動的にダウンロードします
  1. ファイルをダウンロード
  • 開ける.smart-health-cardsファイル拡張子
  • ヘルスケアアプリが自動的に認識してインポートします
  1. QR コードをスキャン
  • カメラで SMART Health Card QR コードをスキャンします
  • ヘルスケアアプリのインポートインターフェイスに直接ジャンプします

HealthKit へのアクセス

04:42

検証可能な健康記録へのアクセスには以下が必要です。

  1. com.apple.developer.healthkit entitlement
  2. 検証可能な健康記録 特別な権利
  3. 各クエリにはユーザーからの明示的な承認が必要です

ライセンス モデルは従来の健康記録とは異なります。

  • 事前の型式許可申請は不要です。
  • クエリが実行されるたびに認証シートがポップアップ表示されます
  • ユーザーは具体的にどのレコードを共有するかを選択します
  • 認可は 1 回限りであり、長期的なアクセスは確立されません。

##詳細

HKVerifiableClinicalRecordQuery

04:47

import HealthKit

// クエリを作成する
let recordTypes = ["https://smarthealth.cards#health-card",
                   "https://smarthealth.cards#immunization"]
let predicate = HKQuery.predicateForClinicalRecords(
    with RelevantDateWithin: DateInterval(start: Date().addingTimeInterval(-7*86400),
                                          end: Date())
)

let query = HKVerifiableClinicalRecordQuery(
    recordTypes: recordTypes,
    predicate: predicate
) { query, records, error in

    guard let records = records, !records.isEmpty else {
        print("No records shared or error: \(error?.localizedDescription ?? "")")
        return
    }

    // ユーザーが共有したレコードを処理する
    for record in records {
        print("Issuer: \(record.issuer)")
        print("JWS: \(record.jws)")
    }
}

// クエリを実行する(認可 sheet が表示される)
healthStore.execute(query)

キーポイント:

  • recordTypesクエリ対象のレコード タイプを指定する文字列配列です。
  • 指定されたすべてのタイプを含むレコードのみが認証シートに表示されます。
  • predicate時間範囲をさらにフィルタリングできます
  • 実行するたびに権限シートがポップアップ表示されます。

JWS の解析と検証

10:54

import CryptoKit
import Combine

// JWS データ構造を定義する
struct SmartHealthCardJWS: Codable {
    let header: JWSHeader
    let payload: JWSPayload
    let signature: Data

    struct JWSHeader: Codable {
        let alg: String       // 签名算法
        let kid: String       // 公開鍵 ID
        let zip: String?      // 圧縮アルゴリズム
    }

    struct JWSPayload: Codable {
        let iss: String       // 発行者 URL
        let nbf: Date         // 有効開始時刻
        let exp: Date?        // 有効期限
        let vc: VerifiableCredential

        struct VerifiableCredential: Codable {
            let type: [String]
            let credentialSubject: CredentialSubject

            struct CredentialSubject: Codable {
                let fhirVersion: String
                let fhirBundle: FHIRBundle
            }
        }

        struct FHIRBundle: Codable {
            let resourceType: String
            let entry: [Entry]

            struct Entry: Codable {
                let resource: FHIRResource
            }
        }
    }

    // 从紧凑序列化格式解析
    init(fromCompactSerialization string: String) throws {
        let parts = string.split(separator: ".").map(String.init)
        guard parts.count == 3 else {
            throw NSError(domain: "JWSParsing", code: -1, userInfo: nil)
        }

        // Header をデコードする
        let headerData = Data(base64URLEncoded: parts[0]) ?? Data()
        let headerDecoder = JSONDecoder()
        header = try headerDecoder.decode(JWSHeader.self, from: headerData)

        // Payload をデコードする(圧縮されている可能性がある)
        let payloadData = Data(base64URLEncoded: parts[1]) ?? Data()
        let decoderPayload: Data
        if header.zip == "DEF" {
            decoderPayload = try payloadData.decompressed(using: .deflate)
        } else {
            decoderPayload = payloadData
        }

        let payloadDecoder = JSONDecoder()
        payload = try payloadDecoder.decode(JWSPayload.self, from: decoderPayload)

        // 保存签名
        signature = Data(base64URLEncoded: parts[2]) ?? Data()
    }
}

// 署名を検証する
extension SmartHealthCardJWS {
    func verifySignature() -> AnyPublisher<Bool, Error> {
        // 発行者の公開鍵を取得する
        let issuerURL = URL(string: payload.iss)!
        let jwksURL = issuerURL.appendingPathComponent(".well-known/jwks.json")

        return URLSession.shared.dataTaskPublisher(for: jwksURL)
            .map(\.data)
            .decode(type: JWKSResponse.self, decoder: JSONDecoder())
            .map { jwks in
                // kid に対応する公開鍵を見つける
                guard let jwk = jwks.keys.first(where: { $0.kid == self.header.kid }) else {
                    throw NSError(domain: "JWSVerification", code: -2, userInfo: nil)
                }

                // CryptoKit の公開鍵に変換する
                let publicKey = try P256.Signing.PublicKey(x963Representation: jwk.toX963())

                // 署名を検証する
                let signedData = (parts[0] + "." + parts[1]).data(using: .utf8)!
                return publicKey.isValidSignature(self.signature, for: signedData)
            }
            .eraseToAnyPublisher()
    }
}

// 使用例
let jwsString = "..." // HKVerifiableClinicalRecord.jws から取得
let jws = try SmartHealthCardJWS(fromCompactSerialization: jwsString)

jws.verifySignature()
    .sink { isValid in
        print("Signature is valid: \(isValid)")
    }
    .store(in: &cancellables)

キーポイント:

  • JWS はコンパクトなシリアル化形式を使用します: header.payload.signature
  • ヘッダーはアルゴリズム (ES256) と公開鍵 ID を指定します
  • ペイロードは DEFLATE で圧縮されているため、最初に解凍する必要があります
  • 発行者からの公開鍵.well-known/jwks.json得る
  • CryptoKitの使用P256.Signing.PublicKey署名を検証する

データ抽出

13:36

// 提取疫苗接种信息
func extractImmunizations(from record: HKVerifiableClinicalRecord) -> [Immunization] {
    guard let jwsData = record.jws.data(using: .utf8),
          let jws = try? SmartHealthCardJWS(fromCompactSerialization: jwsData) else {
        return []
    }

    var immunizations: [Immunization] = []

    for entry in jws.payload.vc.fhirBundle.entry {
        if let resource = entry.resource as? ImmunizationResource {
            immunizations.append(Immunization(
                vaccine: resource.vaccineCode,
                date: resource.occurrenceDateTime,
                manufacturer: resource.manufacturer
            ))
        }
    }

    return immunizations
}

キーポイント:

  • FHIR バンドルには複数のリソースが含まれています
  • 各リソースには対応する resourceType フィールドがあります。
  • 詳細については、「セッションを焼き付けずに FHIR を処理する」を参照してください。

重要ポイント

1. 医療および健康アプリに健康認証情報の検証を統合する

アプリにワクチン接種や検査結果の検証が含まれる場合は、紙の証明書や単純な写真のアップロードの代わりに、検証可能な健康記録を使用できます。ユーザーは確認済みの認証情報をヘルスケア App から直接共有し、追加の写真は必要ありません。

開始方法: 統合HKVerifiableClinicalRecordQuery、健康認証情報の検証を必要とするページでクエリをトリガーします。ユーザーの承認後、必要な特定の情報 (ワクチンの種類、ワクチン接種日など) を抽出します。

2. 健康診断書発行システムの構築

あなたの組織が医療機関の場合は、SMART Health Card の発行者になることができます。発行プロセスには次のものが含まれます。

  1. FHIR リソースを構築する
  2. SMART Health Card にパッケージ化する
  3. 組織の秘密鍵で署名する
  4. ユーザーがダウンロードする QR コードまたはファイルを生成します

開始方法: SMART Health Cards の仕様を参照し、CryptoKit を使用して署名ロジックを実装します。秘密キーが HSM に安全に保管され、公開キーが公開されていることを確認します。.well-known/jwks.json

3. 非医療アプリでユーザーのプライバシーを保護する

アプリが医療用ではない場合でも、ユーザーの健康情報が公開される可能性があります。検証可能な医療記録を使用することは、機密データを直接収集するよりもプライバシーに優しく、どの情報を共有するかをユーザーが制御し、各共有は 1 回限りの使用となります。

開始方法: アプリに健康関連のシナリオ (ジム、保険など) が含まれる場合は、ユーザーの医療記録を直接収集しないでください。アプリが必要なフィールドのみを読み取って、ヘルスケアアプリから検証可能な記録を共有するようにユーザーをガイドします。

関連セッション

コメント

GitHub Issues · utterances