You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
openpgp-card-app/pytools/gpgcard/gpgcard.py

851 lines
26 KiB
Python

# Copyright 2017 Cedric Mesnil <cslashm@gmail.com>, Ledger SAS
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
try:
from ledgerblue.comm import getDongle
from ledgerblue.commException import CommException
except :
pass
import binascii
from smartcard.System import readers
import sys
import datetime
import pickle
# decode level 0 of tlv|tlv|tlv
# return dico {t: v, t:v, ...}
#tlv: hexstring
def decode_tlv(tlv) :
tags = {}
while len(tlv) :
o = 0
l = 0
if (tlv[0] & 0x1F) == 0x1F:
t = (tlv[0]<<8)|tlv[1]
o = 2
else:
t = tlv[0]
o = 1
l = tlv[o]
if l & 0x80 :
if (l&0x7f) == 1:
l = tlv[o+1]
o += 2
if (l&0x7f) == 2:
l = (tlv[o+1]<<8)|tlv[o+2]
o += 3
else:
o += 1
v = tlv[o:o+l]
tags[t] = v
tlv = tlv[o+l:]
return tags
class GPGCardExcpetion(Exception):
pass
class GPGCard() :
def __init__(self):
self.reset()
self.log = False
def reset(self):
#token info
self.AID = b''
self.aid = b''
self.ext_length = b''
self.ext_capabilities = b''
self.histo_bytes = b''
self.PW_status = b''
#user info
self.login = b''
self.url = b''
self.name = b''
self.sex = b''
self.lang = b''
#keys info
self.cardholder_cert = b''
self.sig_attribute = b''
self.dec_attribute = b''
self.aut_attribute = b''
self.sig_fingerprints = b''
self.dec_fingerprints = b''
self.aut_fingerprints = b''
self.sig_CA_fingerprints = b''
self.dec_CA_fingerprints = b''
self.aut_CA_fingerprints = b''
self.sig_date = b''
self.dec_date = b''
self.aut_date = b''
self.cert_aut = b''
self.cert_dec = b''
self.cert_sig = b''
self.UIF_SIG = b''
self.UIF_DEC = b''
self.UIF_AUT = b''
self.digital_counter = b''
#private info
self.private_01 = b''
self.private_02 = b''
self.private_03 = b''
#keys
self.sig_key = b''
self.dec_key = b''
self.aut_key = b''
def connect(self, device):
self.token = None
if device.startswith("ledger:"):
self.token = getDongle(True)
self.exchange = self._exchange_ledger
self.disconnect = self._disconnect_ledger
elif device.startswith("pcsc:"):
allreaders = readers()
for r in allreaders:
rname = str(r)
#print('try: %s : %s'%(rname,device[5:]))
if rname.startswith(device[5:]):
r.createConnection()
self.token = r
self.connection = r.createConnection()
self.connection.connect()
self.exchange = self._exchange_pcsc
self.disconnect = self._disconnect_pcsc
else:
#print("No")
pass
if not self.token:
print("No token")
### APDU interface ###
def _exchange_ledger(self,cmd,data=None, sw=0x9000):
resp = b''
cond = True
while cond:
try:
resp = resp + self.token.exchange(cmd,300)
sw = 0x9000
cond = False
except CommException as e:
if (e.data) :
resp = resp + e.data
sw = e.sw
if (sw&0xFF00) == 0x6100 :
cmd = binascii.unhexlify("00C00000%.02x"%(sw&0xFF))
else:
cond = False
return resp,sw
def log_apdu(self,l):
self.log = l
def alog(self, m,dt,sw=0):
if self.log:
print("%s %.04x %s"%(m,sw,''.join(["%.02x"%b for b in dt])))
def _exchange_pcsc(self,apdu, data=None, sw_expected=0x9000, sw_mask=0xFFFF):
if data:
data = [x for x in data]
apdu = [x for x in apdu]
#send
if data:
while len(data) > 0xFE:
apdux = apdu[0:5]+[0xfe]+data[0:0xFE]
apdux[0] |= 0x10
self.alog('send', apdux)
resp, sw1, sw2 = self.connection.transmit(apdux)
sw = (sw1<<8)|sw2
self.alog('recv',resp,sw)
if sw != 0x9000:
return resp,sw
data = data[0xFE:]
apdu = apdu+[len(data)]+data
self.alog('send', apdu)
resp, sw1, sw2 = self.connection.transmit(apdu)
sw = (sw1<<8)|sw2
self.alog('recv', resp, sw)
#receive
while sw1==0x61:
apdu = binascii.unhexlify(b"00c00000%.02x"%sw2)
apdu = [x for x in apdu]
self.alog('send', apdu)
resp2, sw1, sw2 = self.connection.transmit(apdu)
sw = (sw1<<8)|sw2
self.alog('recv', resp2, sw)
resp = resp + resp2
resp = bytes(resp)
sw = (sw1<<8)|sw2
if sw&sw_mask == sw_expected:
return resp,sw
raise GPGCardExcpetion(binascii.hexlify(resp), "%.04x"%sw)
def _disconnect_ledger(self):
return self.token.close()
def _disconnect_pcsc(self):
r = self.connection.disconnect()
#self.connection.releaseContext()
return r
def select(self):
apdu = binascii.unhexlify(b"00A4040006D27600012401")
return self.exchange(apdu)
def activate(self):
apdu = binascii.unhexlify(b"00440000")
return self.exchange(apdu)
def terminate(self):
apdu = binascii.unhexlify(b"00E60000")
return self.exchange(apdu)
def get_log(self):
apdu = binascii.unhexlify(b"00040000")
return self.exchange(apdu)
def get_data(self,tag):
apdu = binascii.unhexlify(b"00CA%.04x00"%tag)
return self.exchange(apdu)
def put_data(self,tag,value):
return self.exchange(binascii.unhexlify(b"00DA%.04x"%tag), value)
def verify(self,id,value, pinpad=False):
if pinpad:
apdu = binascii.unhexlify(b"EF2000%.02x00"%id)
else:
apdu = binascii.unhexlify(b"002000%.02x%.02x"%(id,len(value)))+value
return self.exchange(apdu)
def change_reference_data(self,id,value,new_value):
lc = len(value)+len(new_value)
apdu = binascii.unhexlify(b"002400%.02x%.02x"%(id,lc))+value+new_value
return self.exchange(apdu)
def reset_retry_counter(self,RC,new_value):
if len(RC)==0:
p1 = 2
else:
p1 = 0
lc = len(RC)+len(new_value)
apdu = binascii.unhexlify(b"002C%02x81%.02x"%(p1,lc))+RC+new_value
return self.exchange(apdu)
def generate_asym_key_pair(self, mode, key):
apdu = binascii.unhexlify(b"0047%02x0002%.04x"%(mode,key))
return self.exchange(apdu)
### API interfaces ###
def get_all(self, with_key=False):
self.reset()
self.slot,sw = self.get_data(0x01F2)
self.AID,sw = self.get_data(0x4f)
self.login ,sw = self.get_data(0x5e)
self.url,sw = self.get_data(0x5f50)
self.histo_bytes,sw = self.get_data(0x5f52)
cardholder,sw = self.get_data(0x65)
tags = decode_tlv(cardholder)
if 0x5b in tags:
self.name = tags[0x5b]
if 0x5f35 in tags:
self.sex = tags[0x5f35]
if 0x5f35 in tags:
self.lang = tags[0x5f2d]
application_data,sw = self.get_data(0x6E)
tags = decode_tlv(application_data)
if 0x7f66 in tags:
self.ext_length = tags[0x7f66]
if 0x73 in tags:
dicretionary_data = tags[0x73]
tags = decode_tlv(dicretionary_data)
if 0xc0 in tags:
self.ext_capabilities = tags[0xC0]
if 0xc4 in tags:
self.PW_status = tags[0xC4]
if 0xC1 in tags:
self.sig_attribute = tags[0xC1]
if 0xC2 in tags:
self.dec_attribute = tags[0xC2]
if 0xC3 in tags:
self.aut_attribute = tags[0xC3]
if 0xC5 in tags:
fingerprints = tags[0xC5]
self.sig_fingerprints = fingerprints[0:20]
self.dec_fingerprints = fingerprints[20:40]
self.aut_fingerprints = fingerprints[40:60]
if 0xC6 in tags:
fingerprints = tags[0xC6]
self.sig_CA_fingerprints = fingerprints[0:20]
self.dec_CA_fingerprints = fingerprints[20:40]
self.aut_CA_fingerprints = fingerprints[40:60]
if 0xcd in tags:
dates = tags[0xCD]
self.sig_date = dates[0:4]
self.dec_date = dates[4:8]
self.aut_date = dates[8:12]
self.cardholder_cert = self.get_data(0x7f21)
self.UIF_SIG,sw = self.get_data(0xD6)
self.UIF_DEC,sw = self.get_data(0xD7)
self.UIF_AUT,sw = self.get_data(0xD8)
sec_template,sw = self.get_data(0x7A)
tags = decode_tlv(sec_template)
if 0x93 in tags:
self.digital_counter = tags[0x93]
self.private_01,sw = self.get_data(0x0101)
self.private_02,sw = self.get_data(0x0102)
self.private_03,sw = self.get_data(0x0103)
self.private_04,sw = self.get_data(0x0104)
if with_key:
self.sig_key,sw = self.get_data(0x00B6)
self.dec_key,sw = self.get_data(0x00B8)
self.aut_key,sw = self.get_data(0x00A4)
return True
def set_all(self):
self.put_data(0x4f, self.AID[10:14])
self.put_data(0x0101, self.private_01)
self.put_data(0x0102, self.private_02)
self.put_data(0x0103, self.private_03)
self.put_data(0x0104, self.private_04)
self.put_data(0x5b, self.name)
self.put_data(0x5e, self.login)
self.put_data(0x5f2d, self.lang)
self.put_data(0x5f35, self.sex)
self.put_data(0x5f50, self.url)
self.put_data(0xc1, self.sig_attribute)
self.put_data(0xc2, self.dec_attribute)
self.put_data(0xc3, self.aut_attribute)
self.put_data(0xc4, self.PW_status)
self.put_data(0xc7, self.sig_fingerprints)
self.put_data(0xc8, self.dec_fingerprints)
self.put_data(0xc9, self.aut_fingerprints)
self.put_data(0xca, self.sig_CA_fingerprints)
self.put_data(0xcb, self.dec_CA_fingerprints)
self.put_data(0xcc, self.aut_CA_fingerprints)
self.put_data(0xce, self.sig_date)
self.put_data(0xcf, self.dec_date)
self.put_data(0xd0, self.aut_date)
#self.put_data(0x7f21, self.cardholder_cert)
self.put_data(0xd6, self.UIF_SIG)
self.put_data(0xd7, self.UIF_DEC)
self.put_data(0xd8, self.UIF_AUT)
if len(self.sig_key):
self.put_data(0x00B6, self.sig_key)
if len(self.dec_key):
self.put_data(0x00B8, self.dec_key)
if len(self.aut_key):
self.put_data(0x00A4, self.aut_key)
return True
def _backup_file_name(self,file_name):
return file_name #file_name+"_slot%d"%(self.slot[0]+1)+".pickle"
def backup(self, file_name, with_key=False):
self.get_all(with_key)
file_name = self._backup_file_name(file_name)
f = open(file_name,mode='w+b')
pickle.dump(
(self.AID,
self.private_01, self.private_02, self.private_03, self.private_04,
self.name, self.login, self.sex, self.url,
self.sig_attribute, self.dec_attribute, self.aut_attribute,
self.PW_status,
self.sig_fingerprints, self.dec_fingerprints, self.aut_fingerprints,
self.sig_CA_fingerprints, self.dec_CA_fingerprints, self.aut_CA_fingerprints,
self.sig_date, self.dec_date, self.aut_date,
self.cardholder_cert,
self.UIF_SIG, self.UIF_DEC, self.UIF_AUT,
self.sig_key, self.dec_key, self.aut_key),
f, 2)
return True
def restore(self, file_name):
file_name = self._backup_file_name(file_name)
f = open(file_name,mode='r+b')
(self.AID,
self.private_01, self.private_02, self.private_03, self.private_04,
self.name, self.login, self.sex, self.url,
self.sig_attribute, self.dec_attribute, self.aut_attribute,
self.PW_status,
self.sig_fingerprints, self.dec_fingerprints, self.aut_fingerprints,
self.sig_CA_fingerprints, self.dec_CA_fingerprints, self.aut_CA_fingerprints,
self.sig_date, self.dec_date, self.aut_date,
self.cardholder_cert,
self.UIF_SIG, self.UIF_DEC, self.UIF_AUT,
self.sig_key, self.dec_key, self.aut_key) = pickle.load(f)
self.set_all()
return True
def seed_key(self):
apdu = binascii.unhexlify(b"0047800102B600")
self.exchange(apdu)
apdu = binascii.unhexlify(b"0047800102B800")
self.exchange(apdu)
apdu = binascii.unhexlify(b"0047800102A400")
self.exchange(apdu)
def decode_AID(self):
return {
'AID': ('AID' , "%x"%int.from_bytes(self.AID,'big')),
'RID': ('RID' , "%x"%int.from_bytes(self.AID[0:5],'big')),
'APP': ('application' , "%.02x"%self.AID[5]),
'VER': ('version' , "%.02x.%.02x"%(self.AID[6], self.AID[7])),
'MAN': ('manufacturer' , "%x"%int.from_bytes(self.AID[8:10],'big')),
'SER': ('serial' , "%x"%int.from_bytes(self.AID[10:14],'big'))
}
def decode_histo(self):
return {
'HIST': ('historical bytes', binascii.hexlify(self.histo_bytes))
}
def decode_extlength(self):
if self.ext_length:
return {
'CMD': ('Max command length' , "%d" %((self.ext_length[2]<<8)|self.ext_length[3])),
'RESP':( 'Max response length' , "%d" %((self.ext_length[6]<<8)|self.ext_length[7]))
}
else:
return {
'CMD': ('Max command length' , "unspecified"),
'RESP':( 'Max response length' ,"unspecified"),
}
def decode_capabilities(self):
d = {}
b1 = self.ext_capabilities[0]
if b1&0x80 :
if self.ext_capabilities[1] == 1:
d['SM'] = ('Secure Messaging', "yes: 128 bits")
elif self.ext_capabilities[1] == 2:
d['SM'] = ('Secure Messaging', "yes: 256 bits")
else:
d['SM'] = ('Secure Messaging', "yes: ?? bits")
else:
d['SM'] = ('Secure Messaging', "no")
if b1&0x40 :
d['CHAL'] = ('Get Challenge', "yes")
else:
d['CHAL'] = ('Get Challenge', "no")
if b1&0x20 :
d['KEY'] = ('Key import', "yes")
else:
d['KEY'] = ('Key import', "no")
if b1&0x10 :
d['PWS'] = ('PW status changeable', "yes")
else:
d['PWS'] = ('PW status changeable', "no")
if b1&0x08 :
d['PDO'] = ('Private DOs', "yes")
else:
d['PDO'] = ('Private DOs', "no")
if b1&0x04 :
d['ATTR'] = ('Algo attributes changeable', "yes")
else:
dd['ATTR'] = ('Algo attributes changeable', "no")
if b1&0x02 :
d['PSO'] = ('PSO:DEC support AES', "yes")
else:
d['PSO'] = ('PSO:DEC support AES', "no")
d['CHAL_MAX'] = ('Max GET_CHALLENGE length',
"%d"% ((self.ext_capabilities[2]<<8)|self.ext_capabilities[3]))
d['CERT_MAX'] = ('Max Cert length',
"%d"% ((self.ext_capabilities[4]<<8)|self.ext_capabilities[5]))
d['PDO_MAX'] = ('Max special DO length',
"%d"% ((self.ext_capabilities[6]<<8)|self.ext_capabilities[7]))
if self.ext_capabilities[8] :
d['PIN2'] = ('PIN 2 format supported', "yes")
else:
d['PIN2'] = ('PIN 2 format supported',"no")
return d
def decode_pws(self):
d = {}
if self.PW_status[0]==0:
d['ONCE'] = ('PW1 valid for several CDS', 'yes')
elif self.PW_status[0]==1:
d['ONCE'] = ('PW1 valid for several CDS', 'no')
else:
d['ONCE'] = ('PW1 valid for several CDS', 'unknown (%d)'%self.PW_status[0])
if self.PW_status[1] & 0x80:
fmt = "Format-2"
else:
fmt = "UTF-8"
pwlen = self.PW_status[1] & 0x7f
d['PW1'] = ("PW1 format", "%s : %d bytes"%(fmt,pwlen))
if self.PW_status[2] & 0x80:
fmt = "Format-2"
else:
fmt = "UTF-8"
pwlen = self.PW_status[2] & 0x7f
d['RC'] = ("RC format", "%s : %d bytes"%(fmt,pwlen))
if self.PW_status[3] & 0x80:
fmt = "Format-2"
else:
fmt = "UTF-8"
pwlen = self.PW_status[3] & 0x7f
d['PW3'] = ("PW3 format", "%s : %d bytes"%(fmt,pwlen))
d['CNT1'] = ('PW1 counter', "%x"%self.PW_status[4])
d['CNTRC'] =('RC counter', "%x"%self.PW_status[5])
d['CNT3'] = ('PW3 counter', "%x"%self.PW_status[6])
return d
#slot
def select_slot(self, slot):
""" Args:
slot (int) : slot id (1 to MAX) to select
"""
self.put_data( 0x01F2, (slot-1).to_bytes(1,'big'))
#USER Info
def set_serial(self, ser):
ser=binascii.unhexlify(ser)
self.AID = self.AID[0:10]+ser
self.put_data(0x4f, self.AID[10:14])
# internals are always store as byres, get/set automatically convert from/to
def set_name(self,name):
""" Args:
name (str) : utf8 string
"""
self.name = name.encode('utf-8')
self.put_data( 0x5b, self.name)
def get_name(self):
return self.name.decode('utf-8')
def set_login(self,login):
""" Args:
login (str) : utf8 string
"""
self.login = login.encode('utf-8')
self.put_data( 0x5e, self.login)
def get_login(self):
return self.login.decode('utf-8')
def set_url(self,url):
""" Args:
url (str) : utf8 string
"""
self.url = url.encode('utf-8')
self.put_data(0x5f50, self.url)
def get_url(self):
return self.url.decode('utf-8')
def set_sex(self,sex):
""" Args:
sex (str) : ascii string ('9', '1', '2')
"""
self.sex = sex.encode('utf-8')
self.put_data(0x5f35, self.sex)
def get_sex(self):
return self.sex.decode('utf-8')
def set_lang(self,lang):
""" Args:
lang (str) : utf8 string
"""
self.lang = lang.encode('utf-8')
self.put_data(0x5f2d, self.lang)
def get_lang(self):
return self.lang.decode('utf-8')
#PINs
def verify_pin(self,id,value, pinpad=False):
""" Args:
id (int) : 0x81, 0x82, ox83
value (str) : ascii string
"""
value = value.encode('ascii')
resp,sw = self.verify(id,value, pinpad)
return sw == 0x9000
def change_pin(self, id, value,new_value):
""" Args:
id (int) : 0x81, ox83
value (str) : ascii string
"""
value = value.encode('ascii')
new_value = new_value.encode('ascii')
resp,sw = self.change_reference_data(id,value,new_value)
return sw == 0x9000
def change_RC(self,new_value):
""" Args:
id (int) : 0x81, ox83
value (str) : ascii string
"""
new_value = new_value.encode('ascii')
resp,sw = self.put_data(0xd3,new_value)
return sw == 0x9000
def reset_PW1(self,RC,new_value):
""" Args:
id (int) : 0x81, ox83
value (str) : ascii string
"""
new_value = new_value.encode('ascii')
RC = RC.encode('ascii')
resp,sw = self.reset_retry_counter(RC,new_value)
return sw == 0x9000
#keys
def get_key_uif(self,key):
"""
Returns: (int) 0,1,2,256(not supported)
"""
uif = None
if key=='sig':
uif = self.UIF_SIG
if key=='aut':
uif = self.UIF_DEC
if key=='dec':
uif = self.UIF_AUT
if uif:
uif = int.from_bytes(uif,'big')
else:
uif = 256
return uif
def get_key_fingerprints(self, key):
"""
Returns: (str) fingerprints hex string
"""
fprints = None
if key=='sig':
fprints = self.sig_fingerprints
if key=='aut':
fprints = self.aut_fingerprints
if key=='dec':
fprints = self.dec_fingerprints
if fprints:
fprints = binascii.hexlify(fprints)
else:
fprint = '-'
return fprints.decode('ascii')
def set_key_fingerprints(self, key, fprints):
fprints = binascii.unhexlify(fprints)
if key=='sig':
self.sig_fingerprints = fprints
self.put_data(0xc7, fprints)
if key=='aut':
self.aut_fingerprints = fprints
self.put_data(0xc9, fprints)
if key=='dec':
self.dec_fingerprints = fprints
self.put_data(0xc8, fprints)
def get_key_CA_fingerprints(self, key):
"""
Returns: (str) CA fingerprints hex string
"""
fprints = None
if key=='sig':
fprints = self.sig_CA_fingerprints
if key=='aut':
fprints = self.aut_CA_fingerprints
if key=='dec':
fprints = self.dec_CA_fingerprints
if fprints:
fprints = binascii.hexlify(fprints)
else:
fprint = b'-'
return fprints.decode('ascii')
def get_key_date(self, key):
"""
Returns: (str) date
"""
fdate = None
if key=='sig':
fdate = self.sig_date
if key=='aut':
fdate = self.aut_date
if key=='dec':
fdate = self.dec_date
if fdate:
fdate = datetime.datetime.fromtimestamp(int.from_bytes(fdate,'big')).isoformat(' ')
else:
fprint = b'-'.decode('ascii')
return fdate
def get_key_attribute(self, key):
"""
for RSA:
{'id': (int) 0x01,
'nsize': (int)
'esize' (int)
'format': (int)
}
for ECC:
{'id': (int) 0x18|0x19,
'OID': (bytes)
}
Args:
key: (str) 'sig' | 'aut', 'dec'
"""
attributes = None
if key=='sig':
attributes = self.sig_attribute
if key=='dec':
attributes = self.dec_attribute
if key=='aut':
attributes = self.aut_attribute
if not attributes:
return None
if len(attributes) == 0:
return None
if attributes[0] == 0x01:
return {
'id': 1,
'nsize': (attributes[1]<<8) | attributes[2],
'esize': (attributes[3]<<8) | attributes[4],
'format': attributes[5]
}
if attributes[0] == 18 or attributes[0] == 19 :
return {
'id': attributes[0] ,
'oid': attributes[1:]
}
print ("NONE: %s"%binascii.hexlify(attributes))
return None
def set_template(self, sig, dec, aut):
"""
See get_template
"""
if (sig):
self.put_data(0x00C1, binascii.unhexlify(sig))
if dec:
self.put_data(0x00C2, binascii.unhexlify(dec))
if aut:
self.put_data(0x00C3, binascii.unhexlify(aut))
pass
def asymmetric_key(self, op, key) :
"""
Args:
op: (int) 0x80 generate, 0x81 read pub, 0x82 read pub&priv
key: (str) 'sig' | 'aut', 'dec'
Returns:
for RSA:
{'id': (int) 0x01,
'n': (bytes)
'e' (bytes)
'd': (bytes)
}
for ECC:
{'id': (int) 0x18|0x19,
'OID': (bytes)
}
"""
attributes = None
if key=='sig':
attributes = self.sig_attribute
key = 0xb600
if key=='dec':
attributes = self.dec_attribute
key = 0xb800
if key=='aut':
attributes = self.aut_attribute
key = 0xa400
if not attributes:
return None
if len(attributes) == 0:
return None
resp,sw = self.generate_asym_key_pair(op,key)
if sw != 0x9000:
return None
resp,sw = self.generate_asym_key_pair(0x82,key)
tags = decode_tlv(resp)
tags = decode_tlv(tags[0x7f49])
if attributes[0] == 0x01:
return {
'id': 1,
'n': tags[0x81],
'e': tags[0x82],
'd': tags[0x98],
}