aboutsummaryrefslogtreecommitdiffstats
path: root/cards/cyberflex_card.py
blob: db5bf2aa63adbc67f70e9e54c040ec99242e165c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
import utils, crypto_utils, binascii
from java_card import *

KEY_AUTH = 0x01
KEY_MAC = 0x02
KEY_KEK = 0x03
DEFAULT_KEYSET = {
    KEY_AUTH: "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F",
    KEY_MAC: "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F",
    KEY_KEK: "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F"}
DEFAULT_CARD_MANAGER_AID = "\xA0\x00\x00\x00\x03\x00\x00"
SECURE_CHANNEL_NONE = -1
SECURE_CHANNEL_CLEAR = 0
SECURE_CHANNEL_MAC = 1
SECURE_CHANNEL_MACENC = 3
MAC_LENGTH = 8

class Cyberflex_Card(Java_Card):
    APDU_INITIALIZE_UPDATE = C_APDU('\x80\x50\x00\x00')
    APDU_EXTERNAL_AUTHENTICATE = C_APDU('\x84\x82\x00\x00')
    APDU_GET_STATUS = C_APDU('\x84\xF2\x00\x00\x4f') ## TODO: C_APDU('\x84\xF2\x00\x00\x4f\x00') ?
    APDU_DELETE = C_APDU('\x84\xe4\x00\x00')
    DRIVER_NAME = "Cyberflex"
    
    ATRS = [ 
        ## Cyberflex Access 32k v2 ???
        ("3B 75 13 00 00 9C 02 02 01 02",
         "FF FF FF FF FF FF FF FF FF FF"),
        ## Cyberflex Access Developer 32k
        ("3B 17 13 9C 12 00 00 00 00 00",
         "FF FF 00 FF 00 00 00 00 00 00"),
        ## Cyberflex Access e-gate 32K
        ("3B 75 94 00 00 62 02 02 00 80",
         "FF FF FF 00 00 FF FF FF 00 00"),
        ## Cyberflex Access 32K v4
        ("3b 76 00 00 00 00 9c 11 01 00 00",
         "FF FF FF FF FF FF FF FF FF FF FF"),
        ## Cyberflex Access 64K v1 (non-FIPS-compliant, softmask 1.1)
        ("3b 75 00 00 00 29 05 01 01 01",
         "FF FF 00 00 00 FF FF FF 00 00"),
        ## Cyberflex Access 64K v1 (FIPS-compliant, softmask 2.1)
        ("3b 75 00 00 00 29 05 01 02 01",
         "FF FF 00 00 00 FF FF FF 00 00")
    ]
    
    ## Will convert the ATRS to binary strings
    ATRS = [(binascii.a2b_hex("".join(_a.split())),
        binascii.a2b_hex("".join(_b.split()))) for (_a,_b) in ATRS]
    del _a, _b
    
    def __init__(self, card = None, keyset = None):
        Java_Card.__init__(self, card = card)
        
        if keyset is not None:
            self.keyset = keyset
        else:
            self.keyset = dict(DEFAULT_KEYSET)
        self.card_manager_aid = DEFAULT_CARD_MANAGER_AID
        
        self.session_key_enc = None
        self.session_key_mac = None
        self.last_mac = None
        self.secure_channel_state = SECURE_CHANNEL_NONE
    
    def before_send(self, apdu):
        """Will be called by send_apdu before sending a command APDU.
        Is responsible for authenticating/encrypting commands when needed."""
        if apdu.cla == '\x84':
            ## Need security
            
            if self.secure_channel_state == SECURE_CHANNEL_NONE:
                raise Exception, "Need security but channel is not established"
            if self.secure_channel_state == SECURE_CHANNEL_CLEAR:
                return apdu
            elif self.secure_channel_state == SECURE_CHANNEL_MAC:
                apdu.lc = apdu.lc + MAC_LENGTH
                
                mac = crypto_utils.calculate_MAC(self.session_key_mac, apdu.render(), self.last_mac)
                self.last_mac = mac
                apdu.data = apdu.data + mac
            elif self.secure_channel_state == SECURE_CHANNEL_MACENC:
                raise Exception, "MAC+Enc Not implemented yet"
        return apdu

    def open_secure_channel(self, keyset_version = 0x0, key_index = 0x0, 
        security_level = SECURE_CHANNEL_MAC):
        """Opens a secure channel by sending an InitializeUpdate and 
        ExternalAuthenticate.
        keyset_version is either the explicit key set version or 0x0 for 
            the implicit key set version.
        key_index is either 0x0 for implicit or 0x1 for explicit key index.
        security_level is one of SECURE_CHANNEL_CLEAR, SECURE_CHANNEL_MAC 
            or SECURE_CHANNEL_MACENC.
            Note that SECURE_CHANNEL_CLEAR is only available for cards that 
            are not secured.
        
        Returns: True on success, generates an exception otherwise.
        Warning: Cyberflex Access 64k v2 cards maintain a failure counter 
            and will lock their key set if they receive 3 InitializeUpdate
            commands that are not followed by a successful 
            ExternalAuthenticate!
            If this function does not return True you should not retry 
            the call, but must closely inspect the situation."""
        
        if security_level not in (SECURE_CHANNEL_CLEAR, SECURE_CHANNEL_MAC, SECURE_CHANNEL_MACENC):
            raise ValueError, "security_level must be one of SECURE_CHANNEL_CLEAR, SECURE_CHANNEL_MAC or SECURE_CHANNEL_MACENC"
        
        host_challenge = crypto_utils.generate_host_challenge()
        
        apdu = C_APDU(self.APDU_INITIALIZE_UPDATE,
            p1 = keyset_version, p2 = key_index,
            data = host_challenge)
        
        self.secure_channel_state = SECURE_CHANNEL_NONE
        self.last_mac = '\x00' * 8
        self.session_key_enc = None
        self.session_key_mac = None
        
        result = self.send_apdu(apdu)
        if result.sw != self.SW_OK:
            raise Exception, "Statusword after InitializeUpdate was %s. Warning: No successful ExternalAuthenticate; keyset might be locked soon" % binascii.b2a_hex(result[-2:])
        
        card_challenge = result.data[12:20]
        card_cryptogram = result.data[20:28]
        
        self.session_key_enc = crypto_utils.get_session_key(
            self.keyset[KEY_AUTH], host_challenge, card_challenge)
        self.session_key_mac = crypto_utils.get_session_key(
            self.keyset[KEY_MAC], host_challenge, card_challenge)
        
        if not crypto_utils.verify_card_cryptogram(self.session_key_enc,
            host_challenge, card_challenge, card_cryptogram):
            raise Exception, "Validation error, card not authenticated. Warning: No successful ExternalAuthenticate; keyset might be locked soon"
        
        host_cryptogram = crypto_utils.calculate_host_cryptogram(
            self.session_key_enc, card_challenge, host_challenge)
        
        apdu = C_APDU(self.APDU_EXTERNAL_AUTHENTICATE,
            p1 = security_level, p2 = 0,
            data = host_cryptogram)
            
        self.secure_channel_state = SECURE_CHANNEL_MAC
        result = self.send_apdu(apdu)
        self.secure_channel_state = security_level
        
        if result.sw != self.SW_OK:
            self.secure_channel_state = SECURE_CHANNEL_NONE
            raise Exception, "Statusword after ExternalAuthenticate was %s. Warning: No successful ExternalAuthenticate; keyset might be locked soon" % binascii.b2a_hex(result[-2:])
        
        return True
    
    def select_application(self, aid):
        result = Java_Card.select_application(self, aid)
        if self.last_sw == self.SW_OK and aid[:5] != DEFAULT_CARD_MANAGER_AID[:5]:
            self.secure_channel_state = SECURE_CHANNEL_NONE
        return result
    
    def get_status(self, reference_control=0x20):
        """Sends a GetStatus APDU und returns the result.
        reference_control is either:
        0x20 Load files
        0x40 Applications
        0x60 Applications and load files
        0x80 Card manager
        0xA0 Card manager and load files
        0xC0 Card manager and applications
        0xE0 Card manager, applications and load files.
        
        Returns: the response APDU which can be parsed with 
        utils.parse_status()"""
        return self.send_apdu(
            C_APDU(self.APDU_GET_STATUS,
                p1 = reference_control)
            )
    
    def delete(self, aid):
        if aid[:5] == DEFAULT_CARD_MANAGER_AID[:5]:
            print "Cowardly refusing to delete the card manager."
            raise ValueError, "Undeletable object"
        tlvaid = chr(0x4f) + chr(len(aid)) + aid
        apdu = C_APDU(self.APDU_DELETE,
            data = tlvaid)
        result = self.send_apdu(apdu)
        
        return result.data[0] == 0x0
    
    def cmd_delete(self, aid):
        """Delete the object identified by aid."""
        aid = binascii.a2b_hex("".join(aid.split()))
        self.delete(aid)
    
    def cmd_status(self, reference_control = "0x20"):
        """Execute a GetStatus command and return the result."""
        reference_control = int(reference_control, 0)
        result = self.get_status(reference_control)
        utils.parse_status(result.data)
    
    def cmd_secure(self, keyset_version=None, key_index=None, security_level=None):
        """Open a secure channel. 
        If given, keyset_version and key_index must be integers while security_level can be one of 0, clear, 1, mac, 3, macenc, mac+enc."""
        if keyset_version is None and key_index is None and security_level is None:
            arg1 = 0
            arg2 = 0
            arg3int = SECURE_CHANNEL_MAC
        elif keyset_version is not None and key_index is not None and security_level is not None:
            arg1 = int(keyset_version,0)
            arg2 = int(key_index,0)
            
            if arg1 not in range(256):
                raise ValueError, "keyset_version must be between 0 and 255 (inclusive)."
            if arg2 not in (0,1):
                raise ValueError, "key_index must be 0 or 1."
            
            arg3 = security_level.strip().lower()
            try:
                arg3int = int(security_level,0)
            except:
                arg3int = None
            
            if arg3 == "clear":
               arg3int = SECURE_CHANNEL_CLEAR 
            elif arg3 == "mac":
                arg3int = SECURE_CHANNEL_MAC
            elif arg3 in ("macenc", "mac+enc"):
                arg3int = SECURE_CHANNEL_MACENC
        else:
            raise TypeError, "Must give none or three arguments."
        self.open_secure_channel(arg1, arg2, arg3int)
    
    def cmd_setkey(self, key_index, key):
        """Set a key in the current keyset. 
        key_index should be one of 0, all, 1, enc, auth, 2, mac, 3, kek."""
        arg1 = key_index.strip().lower()
        try:
            arg1int = int(arg1,0)
        except:
            arg1int = None
        
        if len(key) != 16:
            arg2 = binascii.a2b_hex("".join(key.split()))
        else:
            arg2 = key
        
        if len(arg2) != 16:
            raise TypeError, "Need either exactly 16 binary bytes or 16 hexadezimal bytes for the key argument."
        
        if arg1int == 0 or arg1 == "all":
            all = True
        else:
            all = False
        
        if all or arg1int == KEY_AUTH or arg1 in("auth","enc"):
            self.keyset[KEY_AUTH] = arg2
        if all or arg1int == KEY_MAC or arg1 == "mac":
            self.keyset[KEY_MAC] = arg2
        if all or arg1int == KEY_KEK or arg1 == "kek":
            self.keyset[KEY_KEK] = arg2
    
    def cmd_printkeyset(self):
        """Print the current keyset."""
        print "ENC,AUTH:", utils.hexdump(self.keyset[KEY_AUTH], short=True)
        print "MAC:     ", utils.hexdump(self.keyset[KEY_MAC], short=True)
        print "KEK:     ", utils.hexdump(self.keyset[KEY_KEK], short=True)
    
    def cmd_resetkeyset(self):
        """Reset the keyset to the default keyset for this card."""
        self.keyset = dict(DEFAULT_KEYSET)
    
    def cmd_savekeyset(self, filename):
        """Saves the keyset to the named file."""
        fd = file(filename, "w")
        fd.write(self.keyset[KEY_AUTH])
        fd.write(self.keyset[KEY_MAC])
        fd.write(self.keyset[KEY_KEK])
        fd.close()
    
    def cmd_loadkeyset(self, filename):
        """Loads the keyset from the named file."""
        fd = file(filename, "r")
        keys = fd.read(16*3)
        if len(keys) != 16*3:
            del keys
            fd.close()
            raise ValueError, "The file must must contain three keys of 16 bytes each."
        self.keyset[KEY_AUTH] = keys[:16]
        self.keyset[KEY_MAC] = keys[16:16*2]
        self.keyset[KEY_KEK] = keys[16*2:16*3]
        del keys
        fd.close()
    
    _secname = {SECURE_CHANNEL_NONE: "",
        SECURE_CHANNEL_CLEAR: " [clear]",
        SECURE_CHANNEL_MAC: " [MAC]",
        SECURE_CHANNEL_MACENC: " [MAC+enc]"}
    def get_prompt(self):
        return "(%s)%s" % (self.DRIVER_NAME, 
            Cyberflex_Card._secname[self.secure_channel_state])
    
    
    APPLICATIONS = dict(Java_Card.APPLICATIONS)
    APPLICATIONS.update( {
        "card_manager": DEFAULT_CARD_MANAGER_AID
        } )

    COMMANDS = dict(Java_Card.COMMANDS)
    COMMANDS.update( {
        "status": cmd_status,
        "open_secure_channel": cmd_secure,
        "set_key": cmd_setkey,
        "print_keyset": cmd_printkeyset,
        "reset_keyset": cmd_resetkeyset,
        "save_keyset": cmd_savekeyset,
        "load_keyset": cmd_loadkeyset,
        "delete": cmd_delete
        } )
    STATUS_WORDS = dict(Java_Card.STATUS_WORDS)
    STATUS_WORDS.update( {
        "\x62\x83": "The Card Manager is locked (SelectApplication).",
        "\x63\x00": "Authentication of the host cryptogram failed.",
        "\x63\x10": "More data is available for return than is specified in the Le value.",
        "\x64\x00": "Technical problem that has no specified diagnosis.",
        "\x65\x81": "Memory failure.",
        "\x67\x00": "The specified length of the input data (Lc) is incorrect.",
        "\x69\x81": "No key is specified (GetResponse, called internally).",
        "\x69\x82": "Security status not satisfied. For example, MAC verification failed, the authentication key is locked, or the current security domain requires DAP verification and no verification data was included with the command.",
        "\x69\x83": "The key is blocked (GetResponse, called internally).",
        "\x69\x85": """A requirement for using the command is not satisfied. For example:
+ Command issued outside of a secure channel.
+ Current application does not have the required application privilege
  or life cycle state.
+ The required preceding command was not present.
+ The object to delete is referenced by another object on the card.""",
        "\x69\x87": "The MAC or other verification data is missing (Install).",
        "\x69\x99": "Application selection failed (SelectApplication).",
        "\x6A\x80": """Invalid or inconsistent input data, including input data that is inconsistent with a command header parameter, and LV/TLV-format elements in the input data that are not self-consistent. For example:
+ Incorrect number of padding bytes, incorrect key used for
  encryption, or the specified key set or key index value is invalid.
+ Referenced AID is not found in the card registry or package, or the
  newly specified AID already exists in the registry.
+ Inappropriate application privilege byte value (installing security
  domain), or card already has a default selected application
  (specifying default selected application).
+ First block of input data for a load file is not preceded by the
  correct tag and/or valid length, or the load file refers to a
  nonexistent package.""",
        "\x6A\x81": "Target is locked (SelectApplication).",
        "\x6A\x82": "Registry contains no valid application (or no additional valid application) with the specified AID (SelectApplication).",
        "\x6A\x84": "Insufficient EEPROM memory available to add the object to the card.",
        "\x6A\x86": "Incorrect or unsupported value is specified for P1, P2, or both.",
        "\x6A\x88": "Data referred to in P1, P2, or both is not found.",
        "\x6D\x00": "Unsupported value entered for the INS byte.",
        "\x6E\x00": "Unsupported value entered for the CLA byte.",
        "\x6F\x00": "JVM error that has no specified diagnosis.",
        "\x90\x00": "Command succeeded.",
        "\x94\x81": "Target has an invalid life cycle state.",
        "\x94\x84": "Unsupported algorithm ID in input data (PutKey).",
        "\x94\x85": "Invalid key check value in input data (PutKey).",
        } )

if __name__ == "__main__":
    c = Cyberflex_Card()
    print utils.hexdump( c.select_application(DEFAULT_CARD_MANAGER_AID) )
    
    c.open_secure_channel(security_level = SECURE_CHANNEL_MAC)
    utils.parse_status(c.get_status(224)[:-2])