iso7816 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/AUTHORS +1 -0
- data/CHANGELOG +13 -0
- data/COPYING +728 -0
- data/LICENSE +58 -0
- data/README +9 -0
- data/Rakefile +141 -0
- data/THANKS +0 -0
- data/TODO +2 -0
- data/lib/7816.rb +5 -0
- data/lib/7816/apdu.rb +554 -0
- data/lib/7816/atr.rb +173 -0
- data/lib/7816/card.rb +278 -0
- data/lib/7816/iso_apdu.rb +68 -0
- data/lib/7816/pcsc_helper.rb +102 -0
- data/lib/emv.rb +10 -0
- data/lib/emv/cps_apdu.rb +319 -0
- data/lib/emv/crypto/crypto.rb +108 -0
- data/lib/emv/data/cps_ini_update.rb +15 -0
- data/lib/emv/emv.rb +5 -0
- data/lib/emv/emv_apdu.rb +56 -0
- data/lib/iso7816.rb +9 -0
- data/lib/iso_7816.rb +5 -0
- data/test/apdu_resp_test.rb +63 -0
- data/test/apdu_test.rb +128 -0
- data/test/card_test.rb +36 -0
- data/test/cps_test.rb +35 -0
- data/test/crypto_test.rb +23 -0
- data/test/emv_apdu_test.rb +79 -0
- data/test/iso_apdu_test.rb +108 -0
- data/test/util_test.rb +30 -0
- metadata +135 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
autoload :OpenSSL, 'openssl'
|
2
|
+
|
3
|
+
module EMV
|
4
|
+
module Crypto
|
5
|
+
|
6
|
+
# Generate Session keys according to CPS 5.3
|
7
|
+
def self.generate_session_key ic_card_key, seq, type
|
8
|
+
seq = check_and_convert("seq", seq, 2)
|
9
|
+
ic_card_key = check_and_convert("ic_card_key", ic_card_key, 16)
|
10
|
+
|
11
|
+
derivation_data = case type
|
12
|
+
when :enc
|
13
|
+
"\x01\x82"
|
14
|
+
when :mac
|
15
|
+
"\x01\x01"
|
16
|
+
when :dek
|
17
|
+
"\x01\x81"
|
18
|
+
else
|
19
|
+
raise "invalid type of key: #{type}"
|
20
|
+
end
|
21
|
+
|
22
|
+
derivation_data += seq + ISO7816.s2b("000000000000000000000000")
|
23
|
+
|
24
|
+
cipher = OpenSSL::Cipher::Cipher.new("des-ede-cbc").encrypt
|
25
|
+
cipher.key = ic_card_key
|
26
|
+
cipher.update derivation_data
|
27
|
+
end
|
28
|
+
|
29
|
+
# encrypt ecb according to CPS 5.5.1
|
30
|
+
def self.encrypt_ecb key, data
|
31
|
+
cipher = OpenSSL::Cipher::Cipher.new("des-ede").encrypt
|
32
|
+
cipher.key = key
|
33
|
+
cipher.update(data)
|
34
|
+
end
|
35
|
+
|
36
|
+
#encrypt cbc accroding to CPS 5.5.2
|
37
|
+
def self.encrypt_cbc key, data
|
38
|
+
cipher = OpenSSL::Cipher::Cipher.new("des-ede-cbc").encrypt
|
39
|
+
cipher.key = key
|
40
|
+
data = pad(data)
|
41
|
+
cipher.update data
|
42
|
+
end
|
43
|
+
|
44
|
+
# Mac calculation according to CPS 5.4.1.
|
45
|
+
def self.mac_for_personalization key, input
|
46
|
+
key = check_and_convert "key", key, 16
|
47
|
+
input = pad(input)
|
48
|
+
cipher = OpenSSL::Cipher::Cipher.new("des-ede-cbc").encrypt
|
49
|
+
cipher.key = key
|
50
|
+
mac = ""
|
51
|
+
input.scan(/.{8}/m) {|block|
|
52
|
+
mac = cipher.update block
|
53
|
+
}
|
54
|
+
mac
|
55
|
+
end
|
56
|
+
|
57
|
+
# calculate ISO 9797-1 "Retail Mac"
|
58
|
+
# (MAC Algorithm 3 with output transformation 3,
|
59
|
+
# without truncation) :
|
60
|
+
# DES with final TDES.
|
61
|
+
#
|
62
|
+
# Padding is added if data is not a multiple of 8
|
63
|
+
def self.retail_mac key, data
|
64
|
+
cipher = OpenSSL::Cipher::Cipher.new("des-cbc").encrypt
|
65
|
+
cipher.key = key[0,8]
|
66
|
+
|
67
|
+
data = pad(data)
|
68
|
+
|
69
|
+
single_data = data[0,data.length-8]
|
70
|
+
# Single DES with XOR til the last block
|
71
|
+
if single_data && single_data.length > 0
|
72
|
+
mac_ = cipher.update(single_data)
|
73
|
+
mac_ = mac_[mac_.length-8, 8]
|
74
|
+
else # length of data was <= 8
|
75
|
+
mac_ = "\x00"*8
|
76
|
+
end
|
77
|
+
|
78
|
+
triple_data = data[data.length-8, 8]
|
79
|
+
mac = ""
|
80
|
+
0.upto(7) { |i|
|
81
|
+
mac << (mac_[i] ^ triple_data[i])
|
82
|
+
}
|
83
|
+
# Final Round of TDES
|
84
|
+
cipher = OpenSSL::Cipher::Cipher.new("des-ede").encrypt
|
85
|
+
cipher.key = key
|
86
|
+
cipher.update(mac)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.pad input
|
90
|
+
|
91
|
+
input += "\x80"
|
92
|
+
while (input.length % 8) != 0
|
93
|
+
input << 0x00
|
94
|
+
end
|
95
|
+
input
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.check_and_convert mes, data, length_in_bytes
|
99
|
+
raise "`#{mes}` may not be nil" unless data
|
100
|
+
unless data.length == length_in_bytes || data.length == length_in_bytes*2
|
101
|
+
raise "invalid length for '#{mes}'. Should be #{length_in_bytes}"
|
102
|
+
end
|
103
|
+
data = ISO7816.s2b(data) if data.length == length_in_bytes*2
|
104
|
+
data
|
105
|
+
end
|
106
|
+
|
107
|
+
end #module Crypto
|
108
|
+
end #module EMV
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'tlv'
|
2
|
+
|
3
|
+
module EMV
|
4
|
+
module Data
|
5
|
+
class InitializeUpdateData < TLV::TLV
|
6
|
+
b 6*8, "Identifier of the KMC", :kmc_id
|
7
|
+
b 4*8, "Chip Serial Number", :csn
|
8
|
+
b 8, "Version Number of Master key (KMC)", :kmc_version
|
9
|
+
b 8, "Identifier of Secure Channel Protocol", :sec_channel_proto_id
|
10
|
+
b 2*8, "Sequence Counter", :sequence_counter
|
11
|
+
b 6*8, "Card challenge (r card)", :challenge
|
12
|
+
b 8*8, "Card Cryptogram", :cryptogram
|
13
|
+
end
|
14
|
+
end #module DATA
|
15
|
+
end #module EMV
|
data/lib/emv/emv.rb
ADDED
data/lib/emv/emv_apdu.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
module EMV
|
2
|
+
|
3
|
+
module APDU
|
4
|
+
|
5
|
+
class EMV_APDU < ISO7816::APDU::APDU
|
6
|
+
cla "\x80"
|
7
|
+
end
|
8
|
+
|
9
|
+
def APDU.create_class name, ins, cla="80"
|
10
|
+
cl = "class #{name} < EMV_APDU\n"
|
11
|
+
cl << " cla \"\\x#{cla}\"\n" unless cla == "80"
|
12
|
+
cl << " ins \"\\x#{ins}\"\n"
|
13
|
+
cl << "end"
|
14
|
+
|
15
|
+
eval(cl)
|
16
|
+
end
|
17
|
+
[
|
18
|
+
["APPLICATION_BLOCK", "1E", "84"],
|
19
|
+
["APPLICATION_UNBLOCK", "18", "84"],
|
20
|
+
["CARD_BLOCK", "16", "84"],
|
21
|
+
["GENERATE_APPLICATION_CRYPTOGRAM", "AE"],
|
22
|
+
["GET_DATA", "CA"],
|
23
|
+
["GET_PROCESSING_OPTIONS", "A8"],
|
24
|
+
["PIN_CHANGE_UNBLOCK", "24", "84"],
|
25
|
+
].each{|entry|
|
26
|
+
APDU.create_class *entry
|
27
|
+
}
|
28
|
+
|
29
|
+
|
30
|
+
class EXTERNAL_AUTHENTICATE < ISO7816::APDU::EXTERNAL_AUTHENTICATE
|
31
|
+
end
|
32
|
+
class GET_CHALLENGE < ISO7816::APDU::GET_CHALLENGE
|
33
|
+
end
|
34
|
+
class INTERNAL_AUTHENTICATE < ISO7816::APDU::INTERNAL_AUTHENTICATE
|
35
|
+
end
|
36
|
+
class READ_RECORD < ISO7816::APDU::READ_RECORD
|
37
|
+
end
|
38
|
+
class SELECT < ISO7816::APDU::SELECT
|
39
|
+
p1 "\x04"
|
40
|
+
end
|
41
|
+
class VERIFY < ISO7816::APDU::VERIFY
|
42
|
+
def initialize card
|
43
|
+
super
|
44
|
+
self.plaintext_pin
|
45
|
+
end
|
46
|
+
def plaintext_pin
|
47
|
+
@p2 = ("" << 0x80)
|
48
|
+
end
|
49
|
+
def enciphered_pin
|
50
|
+
@p2 = ("" << 0x88)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end # APDU
|
55
|
+
|
56
|
+
end #EMV
|
data/lib/iso7816.rb
ADDED
data/lib/iso_7816.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/iso7816'
|
3
|
+
|
4
|
+
class TestResponse < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_resp_basics
|
10
|
+
|
11
|
+
response = ISO7816::APDU::Response
|
12
|
+
|
13
|
+
r = response.new "\x90\x00"
|
14
|
+
assert r.normal?
|
15
|
+
assert r.data == ""
|
16
|
+
|
17
|
+
r = response.new "blabla\x90\x00", 6
|
18
|
+
assert r.normal?
|
19
|
+
|
20
|
+
|
21
|
+
r = response.new "blabla\x61\xff", 6
|
22
|
+
assert r.normal?
|
23
|
+
|
24
|
+
["\x62","\x63"].each {|byte|
|
25
|
+
r = response.new "bla#{byte}#{byte}", 3
|
26
|
+
assert r.warning?
|
27
|
+
}
|
28
|
+
|
29
|
+
["\x64", "\x65", "\x66"].each_with_index {|byte, i|
|
30
|
+
r= response.new "#{'a'*i}#{byte}\x00", i
|
31
|
+
assert r.execution_err?
|
32
|
+
}
|
33
|
+
|
34
|
+
["\x67", "\x68", "\x69", "\x6a", "\x6b", "\x6c", "\x6d", "\x6e", "\x6f"].each_with_index { |byte, i|
|
35
|
+
r= response.new "#{'a'*i}#{byte}\x00", i
|
36
|
+
assert r.checking_err?
|
37
|
+
assert r.data == 'a'*i
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
# these are a tad contrived and are only meant to ensure basic coverage...
|
42
|
+
def test_resp_to_s
|
43
|
+
response = ISO7816::APDU::Response
|
44
|
+
r = response.new "\x90\x00"
|
45
|
+
assert r.to_s == "APDU Response\n SW1: 90 (OK: No further qualification)\n SW2: 00 ()\n"
|
46
|
+
|
47
|
+
r = response.new "blabla\x61\xff"
|
48
|
+
assert r.to_s == "APDU Response\n SW1: 61 (OK: SW2 indicates the number of response bytes still available)\n SW2: ff (ff bytes remaining.)\n"
|
49
|
+
|
50
|
+
r = response.new "bla\x62\62"
|
51
|
+
assert r.to_s == "APDU Response\n SW1: 62 (WARN: State of non-volatile memory unchanged)\n SW2: 32 (unknown sw2 for sw1=62: 32)\n"
|
52
|
+
|
53
|
+
r = response.new "\x63\x81"
|
54
|
+
assert r.to_s == "APDU Response\n SW1: 63 (WARN: State of non-volatile memory changed)\n SW2: 81 (File filled up by the last write)\n"
|
55
|
+
|
56
|
+
r = response.new "#{"aa"}\x66\x00", 2
|
57
|
+
assert r.to_s == "APDU Response\n Data(2): 6161\n SW1: 66 (EXE ERR: Reserved for security-related issues)\n SW2: 00 (security (undefined))\n"
|
58
|
+
|
59
|
+
r = response.new "#{"aa"}\x69\x77", 2
|
60
|
+
assert r.to_s == "APDU Response\n Data(2): 6161\n SW1: 69 (CHK ERR: Command not allowed)\n SW2: 77 (unknown sw2 for sw1=69: 77)\n"
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
data/test/apdu_test.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/iso7816'
|
3
|
+
|
4
|
+
class TestAPDU < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
end
|
8
|
+
|
9
|
+
def s2b str
|
10
|
+
ISO7816.s2b str
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_select
|
14
|
+
s = ISO7816::APDU::SELECT.new
|
15
|
+
|
16
|
+
assert_equal "\x00\xa4\x00\x00", s.to_b
|
17
|
+
assert_equal "\n|CLA|INS| P1| P2|\n| 00| a4| 00| 00|", s.to_s
|
18
|
+
|
19
|
+
# add filename
|
20
|
+
|
21
|
+
s.data = "\x37\x00"
|
22
|
+
assert_equal "\x00\xa4\x00\x00\x02\x37\x00", s.to_b
|
23
|
+
assert_equal "\n|CLA|INS| P1| P2|| LC|Data| LE|\n| 00| a4| 00| 00|| 02|3700| |", s.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_invalid_ins
|
27
|
+
apdu = ISO7816::APDU::APDU.new
|
28
|
+
["\x90", "\x95", "\x9f", "\x60", "\x65", "\x6f"]. each {|inv|
|
29
|
+
apdu.ins=inv
|
30
|
+
assert_equal false, apdu.ins_valid?
|
31
|
+
}
|
32
|
+
["\x10", "\x25", "\x3f", "\x40", "\x55", "\xff"]. each {|inv|
|
33
|
+
apdu.ins=inv
|
34
|
+
assert_equal true, apdu.ins_valid?
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_basics
|
39
|
+
a = ISO7816::APDU::APDU.new "test-card"
|
40
|
+
assert_equal "\x00", a.cla
|
41
|
+
assert_equal "\x00", a.ins
|
42
|
+
assert_equal "\x00", a.p1
|
43
|
+
assert_equal "\x00", a.p2
|
44
|
+
assert_equal "", a.data
|
45
|
+
assert_equal "", a.le
|
46
|
+
assert_equal "test-card", a.card
|
47
|
+
|
48
|
+
assert_nothing_raised {
|
49
|
+
a.to_s
|
50
|
+
a.to_b
|
51
|
+
}
|
52
|
+
|
53
|
+
assert_equal false, a.case_4?
|
54
|
+
a.data = "abc"
|
55
|
+
a.le = 4
|
56
|
+
assert_equal true, a.case_4?
|
57
|
+
|
58
|
+
a = ISO7816::APDU::RandomAPDU.new "test-card"
|
59
|
+
[a.cla, a.ins, a.p1, a.p2].each {|by|
|
60
|
+
assert_not_equal nil, by
|
61
|
+
assert_equal 1, by.length
|
62
|
+
}
|
63
|
+
assert_equal true, a.ins_valid?
|
64
|
+
|
65
|
+
assert_equal "", a.data
|
66
|
+
assert_equal "test-card", a.card
|
67
|
+
|
68
|
+
assert_nothing_raised {
|
69
|
+
a.to_s
|
70
|
+
a.to_b
|
71
|
+
}
|
72
|
+
end
|
73
|
+
def test_to_apdu
|
74
|
+
apdu = ISO7816::APDU::APDU.to_apdu(s2b("80b4000008"))
|
75
|
+
assert_equal "\x80", apdu.cla
|
76
|
+
assert_equal "\xb4", apdu.ins
|
77
|
+
assert_equal "\x00", apdu.p1
|
78
|
+
assert_equal "\x00", apdu.p2
|
79
|
+
assert_equal "\x08", apdu.le
|
80
|
+
|
81
|
+
apdu = ISO7816::APDU::APDU.to_apdu(s2b("8020000008aaaaaaaaaaaaaaaa"))
|
82
|
+
assert_equal "\x80", apdu.cla
|
83
|
+
assert_equal "\x20", apdu.ins
|
84
|
+
assert_equal "\x00", apdu.p1
|
85
|
+
assert_equal "\x00", apdu.p2
|
86
|
+
assert_equal "\x08", apdu.lc
|
87
|
+
assert_equal "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa", apdu.data
|
88
|
+
|
89
|
+
assert_raise(RuntimeError){
|
90
|
+
apdu = ISO7816::APDU::APDU.to_apdu(s2b("8020000008aaaaaaaaaaaaaaaaaaaa"))
|
91
|
+
}
|
92
|
+
|
93
|
+
apdu = ISO7816::APDU::APDU.to_apdu(s2b("fefefefefe"))
|
94
|
+
assert_equal "\xfe", apdu.cla
|
95
|
+
assert_equal "\xfe", apdu.ins
|
96
|
+
assert_equal "\xfe", apdu.p1
|
97
|
+
assert_equal "\xfe", apdu.p2
|
98
|
+
assert_equal "\xfe", apdu.le
|
99
|
+
assert_equal "", apdu.lc
|
100
|
+
assert_equal "", apdu.data
|
101
|
+
|
102
|
+
end
|
103
|
+
def test_get_response
|
104
|
+
gr = ISO7816::APDU::GET_RESPONSE.new "dummy-card"
|
105
|
+
assert_equal "dummy-card", gr.card
|
106
|
+
assert_equal "\xc0", gr.ins
|
107
|
+
assert_equal "\x00", gr.cla
|
108
|
+
end
|
109
|
+
|
110
|
+
class DingDong < ISO7816::APDU::APDU
|
111
|
+
cla "\x01"
|
112
|
+
ins "\x02"
|
113
|
+
p1 "\x03"
|
114
|
+
p2 "\x04"
|
115
|
+
le "\x05"
|
116
|
+
end
|
117
|
+
def test_meta_class
|
118
|
+
assert_equal "\x01", DingDong._cla
|
119
|
+
d = DingDong.new
|
120
|
+
assert_equal "\x01", d.cla
|
121
|
+
assert_equal "\x05", d.le
|
122
|
+
to_strinq=%Q{
|
123
|
+
|CLA|INS| P1| P2|| LC|Data| LE|
|
124
|
+
| 01| 02| 03| 04|| | | 05|}
|
125
|
+
assert_equal to_strinq, d.to_s
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
data/test/card_test.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/iso7816'
|
3
|
+
|
4
|
+
class TestCard < Test::Unit::TestCase
|
5
|
+
|
6
|
+
ATR = "\x3b\xd9\x18\x00\x00\x80\x54\x43\x4f\x4c\x44\x82\x90\x00"
|
7
|
+
def setup
|
8
|
+
@server = Thread.start {
|
9
|
+
require 'socket'
|
10
|
+
ssock = TCPServer.open(1024)
|
11
|
+
while (s = ssock.accept)
|
12
|
+
s.send ATR, 0
|
13
|
+
end #while
|
14
|
+
ssock.close
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def teardown
|
19
|
+
@server.exit
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_connect
|
23
|
+
card = ISO7816::Card::TCPCard.new
|
24
|
+
atr = card.connect
|
25
|
+
assert_equal true, card.connected?
|
26
|
+
assert_equal ATR, atr
|
27
|
+
card.disconnect
|
28
|
+
assert_equal false, card.connected?
|
29
|
+
|
30
|
+
atr = card.connect {}
|
31
|
+
assert_equal ATR, atr
|
32
|
+
assert_equal false, card.connected?
|
33
|
+
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/test/cps_test.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/iso7816'
|
3
|
+
|
4
|
+
class TestAPDU < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
end
|
8
|
+
|
9
|
+
def s2b str
|
10
|
+
ISO7816.s2b str
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_store_data_security_level
|
14
|
+
mock_ctx = Object.new
|
15
|
+
mock_ctx.class.class_eval{
|
16
|
+
attr_accessor :level
|
17
|
+
}
|
18
|
+
|
19
|
+
[:no_sec, :mac, :enc_and_mac].each {|lev|
|
20
|
+
mock_ctx.level=lev
|
21
|
+
sd = EMV::APDU::CPS::STORE_DATA.new nil, mock_ctx
|
22
|
+
# shouldn't be set
|
23
|
+
assert_equal nil, sd.security_level
|
24
|
+
assert !sd.secure?
|
25
|
+
|
26
|
+
sd.security_level = :no_sec
|
27
|
+
assert !sd.secure?
|
28
|
+
sd.security_level = :mac
|
29
|
+
assert sd.secure?
|
30
|
+
sd.security_level = :enc_and_mac
|
31
|
+
assert sd.secure?
|
32
|
+
}
|
33
|
+
assert true
|
34
|
+
end
|
35
|
+
end
|