Medley Developer Blog

株式会社メドレーのエンジニア・デザイナーによるブログです

Rubyを使ってHPKIカードのデータを読み取る

こんにちは、開発本部の宮内です。今回、HPKIカードについて調査を行いましたので、それについて書きます。

tl;dr

JAHIS HPKI 対応 IC カードガイドライン Ver.3.0を参考にして、HPKIテストカードから実際に公開鍵証明書を取得しました。

今後もHPKIについて調査を続行していきたいと思います。

HPKIとは?

HPKIとは厚生労働省が認める医療福祉関係資格(医師・薬剤師・看護師など26種類の保健医療福祉分野の国家資格と、院長・管理薬剤師など5種類の管理者資格)を認証することができるPKIです。

配布されたHPKIカードには、ルートCA、中間CA、証明書が格納されています。

このカードは、電子署名などに使用することができ、今後普及していけば、医療文書(処方箋や病院への紹介状など)を印刷、押印、送付するなどの非効率な業務をすることなく、すべてデジタル化することができるようになります。

また、電子認証用の証明書も含まれているため、認証・認可処理にも使用することができます。

今回、HPKIテストカードを用いて調査を行いました。 f:id:medley_inc:20180712151907j:plain

調査環境

  • macOS v10.13.5
  • ACR39-NTTCom
  • Ruby v2.5.1
  • smartcard v0.5.6
  • HPKIテスト用カード

PC/SC

HPKIカードのようなICカードとやり取りを行うには、PC/SCというAPI仕様を使う必要があります。

PC/SCはもともとWindows環境のみで利用可能でしたが、pcsc-liteというOSS実装があり、現在では様々なUNIX like OSでも利用できます。

macOSの場合、/System/Library/Frameworks/PCSC.framework/PCSCにライブラリが用意されており、特に準備する必要なく利用可能です。(2018年07月現在)

ただし、ICカードリーダーのドライバーをインストールする必要があります。

今回利用したACR39-NTTComダウンロードページmacOS v10.13に対応したドライバーが配布されていなかったため、ICカードリーダーのチップメーカーであるACS社のダウンロードページからドライバーを入手しました。

smartcard

検証する際に使用したgemはsmartcardです。 普通のrubygemと同じくgem installして利用します。

gem install smartcard

ICカードリーダーをPCに接続し、

ruby -rsmartcard -e 'pp Smartcard::PCSC::Context.new.readers'

を実行し、ICカードリーダー名が表示されれば接続成功です。

アプリケーション識別子の取得

実際にHPKIテストカードから情報を取得していきます。

ガイドラインの「附属書A(参考)PKI カードアプリケーション利用のシーケンス」にある「A.2.2 JIS X 6320-15 に従った PKI カードアプリケーションの検索と利用」を実装していきます。

f:id:medley_inc:20180712152004p:plain 引用 ガイドライン

prog01.rb

# prog01.rb
require "smartcard"

def puts_response(response)
  puts "status = %04X" % response[:status]
  puts "data = %s" % response[:data].map { |i| "%02X" % i }.join(" ")
end

context = Smartcard::PCSC::Context.new
begin
  card = context.card context.readers.first

  # SELECTコマンドで`E8 28 BD 08 0F`をパーシャル指定したDFを指定
  apdu = [0x00,
          0xA4,
          0x04,
          0x00,
          0x05,
          0xE8, 0x28, 0xBD, 0x08, 0x0F,
          0x00]
  response = card.transmit apdu.pack("C*")
  response = Smartcard::Iso::IsoCardMixin.deserialize_response response.unpack("C*")
  puts_response response

  while response[:status] == 0x9000
    # SELECTコマンドで次のDFを探す
    apdu = [0x00,
            0xA4,
            0x04,
            0x02,
            0x05,
            0xE8, 0x28, 0xBD, 0x08, 0x0F,
            0x00]
    response = card.transmit apdu.pack("C*")
    response = Smartcard::Iso::IsoCardMixin.deserialize_response response.unpack("C*")
    puts_response response
  end
ensure
  context.release
end

上記のプログラムを実行すると、次のような出力が得られます。

status = 9000
data = 6F 12 84 10 E8 28 BD 08 0F A0 00 00 03 91 00 00 00 00 00 01
status = 9000
data = 6F 12 84 10 E8 28 BD 08 0F A0 00 00 03 91 00 00 00 00 00 02
status = 6A82
data =

SELECTコマンドを発行するとBER-TLVで符号化されたFCI(ファイル制御情報)が取得できます。

1つ目のデータから見ていきます。

1バイト目は6Fなので、このデータはファイル制御パラメタ及びファイル管理データの集合を表します。

f:id:medley_inc:20180712152144p:plain 引用 JIS X 6320-4 表8-ファイル制御情報用の産業感共通利用テンプレート

2バイト目は12なので、後続するデータの長さが18バイトあることを表します。

3バイト目は84なので、データ要素がDF名であることを表します。

f:id:medley_inc:20180712152208p:plain 引用 JIS X 6320-4 表10-ファイル制御パラメタデータオブジェクト

4バイト目は10なので、後続するデータの長さが16バイトあることを表します。 5バイト目以降は、DF名(= アプリケーション識別子)です。

2つ目のデータもデータ構造は同じなため省略します。

これでHPKIテストカードには、

  • E8 28 BD 08 0F A0 00 00 03 91 00 00 00 00 00 01
  • E8 28 BD 08 0F A0 00 00 03 91 00 00 00 00 00 02

という2つのアプリケーション識別子が含まれていることが分かります。

公開鍵証明書を取得する

前段にてHPKIテストカードに含まれているアプリケーション識別子が分かりましたので、次は公開鍵証明書を取得していきます。

ガイドラインの「A.3.2 証明書の読み出し」にあるコマンドの通りにAPDUを発行しても、正しいデータは返ってきません。 これは、HPKIテストカードのEF識別子が、ガイドラインに記載されているEF識別子とは異なるためです。

HPKIカードはJIS X 6320に準拠しているため、各種暗号情報オブジェクトへのパス情報を含んだEF.ODが存在しています。 このEF.ODを使い公開鍵証明書へのパスを取得してから、公開鍵証明書を取得していきます。

f:id:medley_inc:20180712152231p:plain f:id:medley_inc:20180712152245p:plain 引用 ガイドライン

EF.ODを読み込む

prog02.rb

# prog02.rb
require "smartcard"
require "openssl"

def puts_response(response)
  puts "status = %04X" % response[:status]
  puts "data = %s" % response[:data].map { |i| "%02X" % i }.join(" ")
end

def decode_asn1(response)
  data = response[:data].reverse_each.drop_while { |i| i == 0xFF }.reverse
  return if data.empty?
  OpenSSL::ASN1.decode_all data.pack("C*")
end

context = Smartcard::PCSC::Context.new
begin
  card = context.card context.readers.first

  [
    [0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xA0, 0x00, 0x00, 0x03, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01],
    [0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xA0, 0x00, 0x00, 0x03, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02]
  ].each do |aid|
    # SELECTコマンドでアプリケーションを選択する
    apdu = [0x00,
            0xA4,
            0x04,
            0x00,
            0x10,
            *aid,
            0x00]
    card.transmit apdu.pack("C*")

    # EF.ODの読み出し
    apdu = [0x00,
            0xB0,
            0x91,
            0x00,
            0x00]
    response = card.transmit apdu.pack("C*")
    response = Smartcard::Iso::IsoCardMixin.deserialize_response response.unpack("C*")
    pp decode_asn1 response
  end
ensure
  context.release
end

EF.ODを読み込むとDER符号化されたデータが返ってきます。 これを OpenSSL::ANS1 モジュールで復号化すると、次に取得するべきEF識別子が分かります。

EF.ODのASN.1定義は以下のようになっているため、タグが4であるデータを読み込めば良さそうです。

CIOChoice ::= CHOICE {
  privateKeys          [0] PrivateKeys,
  publicKeys           [1] PublicKeys,
  trustedPublicKeys    [2] PublicKeys,
  secretKeys           [3] SecretKeys,
  certificates         [4] Certificates,
  trustedCertificates  [5] Certificates,
  usefulCertificates   [6] Certificates,
  dataContainerObjects [7] DataContainerObjects,
  authObjects          [8] AuthObjects,
}

prog02.rbを実行して実際に得られたデータ

[
 # 中略
 #<OpenSSL::ASN1::ASN1Data:0x00007f8b8e0ef7b0
  @indefinite_length=false,
  @tag=4,
  @tag_class=:CONTEXT_SPECIFIC,
  @value=
   [#<OpenSSL::ASN1::Sequence:0x00007f8b8e0ef7d8
     @indefinite_length=false,
     @tag=16,
     @tag_class=:UNIVERSAL,
     @tagging=nil,
     @value=
      [#<OpenSSL::ASN1::OctetString:0x00007f8b8e0ef800
        @indefinite_length=false,
        @tag=4,
        @tag_class=:UNIVERSAL,
        @tagging=nil,
        @value="\x00\x04">]>]>
 # 中略
]
[
 # 中略
 #<OpenSSL::ASN1::ASN1Data:0x00007f8b8d118df0
  @indefinite_length=false,
  @tag=4,
  @tag_class=:CONTEXT_SPECIFIC,
  @value=
   [#<OpenSSL::ASN1::Sequence:0x00007f8b8d118e18
     @indefinite_length=false,
     @tag=16,
     @tag_class=:UNIVERSAL,
     @tagging=nil,
     @value=
      [#<OpenSSL::ASN1::OctetString:0x00007f8b8d118e40
        @indefinite_length=false,
        @tag=4,
        @tag_class=:UNIVERSAL,
        @tagging=nil,
        @value="\x00\x04">]>]>,
 # 中略
]

どちらのアプリケーションも00 04がEF.CD(証明書オブジェクト情報)のEF識別子だということが分かります。

EF.CDを読み込む

prog03.rb

# prog03.rb
require "smartcard"
require "openssl"

def puts_response(response)
  puts "status = %04X" % response[:status]
  puts "data = %s" % response[:data].map { |i| "%02X" % i }.join(" ")
end

def decode_asn1(response)
  data = response[:data].reverse_each.drop_while { |i| i == 0xFF }.reverse
  return if data.empty?
  OpenSSL::ASN1.decode_all data.pack("C*")
end

context = Smartcard::PCSC::Context.new
begin
  card = context.card context.readers.first

  [
    [0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xA0, 0x00, 0x00, 0x03, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01],
    [0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xA0, 0x00, 0x00, 0x03, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02]
  ].each do |aid|
    # SELECTコマンドでアプリケーションをapdu
    選択する = [0x00,
            0xA4,
            0x04,
            0x00,
            0x10,
            *aid,
            0x00]
    card.transmit apdu.pack("C*")

    # SELECTコマンドでEF識別子`00 04`を選択する
    apdu = [0x00,
            0xA4,
            0x02,
            0x0C,
            0x02,
            0x00, 0x04]
    card.transmit apdu.pack("C*")

    # READ BINARYコマンドでファイルを読み込む
    data = []
    offset = 0
    loop do
      apdu = [0x00,
              0xB0,
              (offset & 0x7FFF) >> 8,
              (offset & 0x00FF),
              0x00]
      response = card.transmit apdu.pack("C*")
      response = Smartcard::Iso::IsoCardMixin.deserialize_response response.unpack("C*")
      data.concat response[:data]
      break if response[:data].all? { |e| e == 0xFF }
      break unless response[:status] == 0x9000
      offset += response[:data].size
    end
    pp decode_asn1 data: data
  end
ensure
  context.release
end

prog03.rbを実行して実際に得られたデータ

[
 # 中略
 #<OpenSSL::ASN1::Sequence:0x00007ffdf99aaf70
  @indefinite_length=false,
  @tag=16,
  @tag_class=:UNIVERSAL,
  @tagging=nil,
  @value=
   [#<OpenSSL::ASN1::OctetString:0x00007ffdf99ab038
     @indefinite_length=false,
     @tag=4,
     @tag_class=:UNIVERSAL,
     @tagging=nil,
     @value="\x00\x16">,
    #<OpenSSL::ASN1::Integer:0x00007ffdf99aafe8
     @indefinite_length=false,
     @tag=2,
     @tag_class=:UNIVERSAL,
     @tagging=nil,
     @value=#<OpenSSL::BN 0>>,
    #<OpenSSL::ASN1::ASN1Data:0x00007ffdf99aaf98
     @indefinite_length=false,
     @tag=0,
     @tag_class=:CONTEXT_SPECIFIC,
     @value="\x05\x17">]>
 # 中略
]
[
 # 中略
 #<OpenSSL::ASN1::Sequence:0x00007ffdfa072308
  @indefinite_length=false,
  @tag=16,
  @tag_class=:UNIVERSAL,
  @tagging=nil,
  @value=
   [#<OpenSSL::ASN1::OctetString:0x00007ffdfa072448
     @indefinite_length=false,
     @tag=4,
     @tag_class=:UNIVERSAL,
     @tagging=nil,
     @value="\x00\x16">,
    #<OpenSSL::ASN1::Integer:0x00007ffdfa0723d0
     @indefinite_length=false,
     @tag=2,
     @tag_class=:UNIVERSAL,
     @tagging=nil,
     @value=#<OpenSSL::BN 0>>,
    #<OpenSSL::ASN1::ASN1Data:0x00007ffdfa072380
     @indefinite_length=false,
     @tag=0,
     @tag_class=:CONTEXT_SPECIFIC,
     @value="\x05%">]>
]
 # 中略

これで公開鍵証明書ファイルのEF識別子が00 16であることが判明しました。

公開鍵証明書を読み込む

prog04.rb

# prog04.rb
require "smartcard"
require "openssl"

context = Smartcard::PCSC::Context.new
begin
  card = context.card context.readers.first

  [
    [0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xA0, 0x00, 0x00, 0x03, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01],
    [0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xA0, 0x00, 0x00, 0x03, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02]
  ].each do |aid|
    # SELECTコマンドでアプリケーションを選択する
    apdu = [0x00,
            0xA4,
            0x04,
            0x00,
            0x10,
            *aid,
            0x00]
    card.transmit apdu.pack("C*")

    # SELECTコマンドでEF識別子`00 16`を選択する
    apdu = [0x00,
            0xA4,
            0x02,
            0x0C,
            0x02,
            0x00, 0x16]
    card.transmit apdu.pack("C*")

    # READ BINARYコマンドでファイルを読み込む
    data = []
    offset = 0
    loop do
      apdu = [0x00,
              0xB0,
              (offset & 0x7FFF) >> 8,
              (offset & 0x00FF),
              0x00]
      response = card.transmit apdu.pack("C*")
      response = Smartcard::Iso::IsoCardMixin.deserialize_response response.unpack("C*")
      data.concat response[:data]
      break if response[:data].all? { |e| e == 0xFF }
      break unless response[:status] == 0x9000
      offset += response[:data].size
    end
    cert = OpenSSL::X509::Certificate.new(data.reverse_each.drop_while { |i| i == 0xFF }.reverse.pack("C*"))
    puts cert.to_text
  end
ensure
  context.release
end

HPKIテストカードからDER符号化された公開鍵証明書データが取得できるので、OpenSSL::X509::Certificate.newインスタンス化できます。

上記のprog04.rbを実行すると下記のような出力が得られます。

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 13023 (0x32df)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=JP, O=Japan Medical Association, OU=Digital Certificate Center, CN=HPKI-01-HPKI_JV2-forNonRepudiation
        Validity
            Not Before: Aug 15 15:00:00 2017 GMT
            Not After : Aug 15 14:59:59 2018 GMT
        Subject: C=JP, CN=JMACombi20413/serialNumber=TESTC20413
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:94:dd:09:40:f4:58:f9:0f:ec:3a:ea:e3:47:33:
 # 中略
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Authority Key Identifier:
                keyid:44:E9:20:05:4D:6D:C4:B7:FA:4B:F0:1B:C6:EA:C8:D6:5B:16:22:F4
                DirName:/C=JP/O=Ministry of Health, Labour and Welfare/OU=Director-General for Policy Planning and Evaluation/OU=MHLW HPKI Root CA V2
                serial:02

            X509v3 Subject Key Identifier:
                9E:E5:71:59:1E:A7:FC:1E:4A:31:F8:7B:30:0B:E3:7F:05:3D:9A:40
            X509v3 Key Usage: critical
                Non Repudiation
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl.pki.med.or.jp/repository/crl/crl-sign2.crl

            X509v3 Subject Directory Attributes:
                0402..(..B..1(1&0$."1 ...
*.............Medical Doctor
            X509v3 Certificate Policies: critical
                Policy: 1.2.392.100495.1.5.1.1.0.1
                  CPS: http://www.pki.med.or.jp/certpolicy/

    Signature Algorithm: sha256WithRSAEncryption
         84:ae:95:45:5e:e7:64:8b:0c:6e:20:5f:9f:1f:0d:5c:ae:4a:
 # 中略

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 12927 (0x327f)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=JP, O=Japan Medical Association, OU=Digital Certificate Center, CN=HPKI-01-HPKI_JV2-forAuthentication-forIndividual
        Validity
            Not Before: Aug 15 15:00:00 2017 GMT
            Not After : Aug 15 14:59:59 2018 GMT
        Subject: C=JP, CN=JMACombi20413/serialNumber=TESTC20413
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:c6:f9:06:26:58:5e:11:b7:12:f2:8a:3e:97:0a:
 # 中略
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Authority Key Identifier:
                keyid:62:12:93:82:DE:3C:D7:FF:A8:D3:63:01:D3:01:6A:AE:6C:3B:C0:D4
                DirName:/C=JP/O=Ministry of Health, Labour and Welfare/OU=Director-General for Policy Planning and Evaluation/OU=MHLW HPKI Root CA V2
                serial:03

            X509v3 Subject Key Identifier:
                45:2B:7B:B4:47:89:3D:6C:05:6D:82:4D:4C:C8:80:B8:B4:B0:89:81
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl.pki.med.or.jp/repository/crl/crl-auth2.crl

            X509v3 Subject Directory Attributes:
                0402..(..B..1(1&0$."1 ...
*.............Medical Doctor
            X509v3 Certificate Policies: critical
                Policy: 1.2.392.100495.1.5.1.2.0.1
                  CPS: http://www.pki.med.or.jp/certpolicy/

    Signature Algorithm: sha256WithRSAEncryption
 # 中略

それぞれのアプリケーションから正しく公開鍵証明書が取得できました。

電子認証ガイドラインによると、電子認証に使用する証明書はIssuerのCN(Common Name)がHPKI-01-*-forAuthentication-forIndividualであることが定められているため、 使用したHPKIテストカードでは、電子認証に使用するアプリケーション識別子はE8 28 BD 08 0F A0 00 00 03 91 00 00 00 00 00 02であることが分かります。 また、電子署名に使用するアプリケーション識別子はE8 28 BD 08 0F A0 00 00 03 91 00 00 00 00 00 01であることが分かりました。

最後に

以上でガイドラインの「A.2.2 JIS X 6320-15 に従った PKI カードアプリケーションの検索と利用」にある「PKI カードアプリケーションの検索」まで実装できました。

今後、次のステップである暗号計算を実装していきたいと思います。