2022-08-22 22:49:51 +00:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
/ *
* This file is part of the Pico HSM distribution ( https : / / github . com / polhenarejos / pico - hsm ) .
* Copyright ( c ) 2022 Pol Henarejos .
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , version 3.
*
* This program is distributed in the hope that it will be useful , but
* WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the GNU
* General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program . If not , see < http : / / www . gnu . org / licenses / > .
* /
"""
2022-10-10 09:02:07 +00:00
import sys
try :
from smartcard . CardType import AnyCardType
from smartcard . CardRequest import CardRequest
from smartcard . Exceptions import CardRequestTimeoutException
2022-10-30 07:51:57 +00:00
except ModuleNotFoundError :
2022-10-10 09:02:07 +00:00
print ( ' ERROR: smarctard module not found! Install pyscard package. \n Try with `pip install pyscard` ' )
sys . exit ( - 1 )
try :
2022-10-30 07:51:57 +00:00
from cvc . certificates import CVC
2022-10-10 09:02:07 +00:00
from cvc . asn1 import ASN1
from cvc . oid import oid2scheme
from cvc . utils import scheme_rsa
2022-10-30 07:51:57 +00:00
except ModuleNotFoundError :
2022-10-10 09:02:07 +00:00
print ( ' ERROR: cvc module not found! Install pycvc package. \n Try with `pip install pycvc` ' )
sys . exit ( - 1 )
2022-10-31 14:09:54 +00:00
try :
from cryptography . hazmat . primitives . asymmetric import ec
from cryptography . hazmat . primitives . kdf . hkdf import HKDF
from cryptography . hazmat . primitives . serialization import Encoding , PublicFormat
from cryptography . hazmat . primitives . ciphers . aead import ChaCha20Poly1305
from cryptography . hazmat . primitives import hashes
2022-11-03 14:48:33 +00:00
except ModuleNotFoundError :
2022-10-31 14:09:54 +00:00
print ( ' ERROR: cryptography module not found! Install cryptography package. \n Try with `pip install cryptography` ' )
sys . exit ( - 1 )
2022-08-18 16:17:21 +00:00
import json
import urllib . request
import base64
from binascii import hexlify
2022-08-24 11:53:11 +00:00
import sys
import argparse
import os
2022-10-31 14:09:54 +00:00
import platform
2022-08-25 11:38:09 +00:00
from datetime import datetime
from argparse import RawTextHelpFormatter
2022-08-18 23:44:27 +00:00
2022-10-31 14:09:54 +00:00
2022-08-18 16:17:21 +00:00
class APDUResponse ( Exception ) :
def __init__ ( self , sw1 , sw2 ) :
self . sw1 = sw1
self . sw2 = sw2
super ( ) . __init__ ( f ' SW: { sw1 : 02X } { sw2 : 02X } ' )
2022-08-18 23:44:27 +00:00
2022-08-25 11:38:09 +00:00
def send_apdu ( card , command , p1 , p2 , data = None ) :
lc = [ ]
dataf = [ ]
if ( data ) :
lc = [ 0x00 ] + list ( len ( data ) . to_bytes ( 2 , ' big ' ) )
dataf = data
2022-08-18 16:17:21 +00:00
le = [ 0x00 , 0x00 ]
2022-08-18 18:09:23 +00:00
if ( isinstance ( command , list ) and len ( command ) > 1 ) :
apdu = command
else :
apdu = [ 0x00 , command ]
2022-08-18 23:44:27 +00:00
2022-08-25 11:38:09 +00:00
apdu = apdu + [ p1 , p2 ] + lc + dataf + le
2022-08-18 18:09:23 +00:00
response , sw1 , sw2 = card . connection . transmit ( apdu )
2022-08-18 16:17:21 +00:00
if ( sw1 != 0x90 ) :
2022-08-18 23:44:27 +00:00
raise APDUResponse ( sw1 , sw2 )
2022-08-18 16:17:21 +00:00
return response
2022-04-13 23:03:03 +00:00
2022-08-24 11:53:11 +00:00
def parse_args ( ) :
parser = argparse . ArgumentParser ( )
subparser = parser . add_subparsers ( title = " commands " , dest = " command " )
2022-08-24 15:47:28 +00:00
parser_init = subparser . add_parser ( ' initialize ' , help = ' Performs the first initialization of the Pico HSM. ' )
parser_init . add_argument ( ' --pin ' , help = ' PIN number ' )
2022-08-25 11:47:42 +00:00
parser_init . add_argument ( ' --so-pin ' , help = ' SO-PIN number ' )
2022-08-24 11:53:11 +00:00
parser_attestate = subparser . add_parser ( ' attestate ' , help = ' Generates an attestation report for a private key and verifies the private key was generated in the devices or outside. ' )
2022-08-24 15:47:28 +00:00
parser_attestate . add_argument ( ' -k ' , ' --key ' , help = ' The private key index ' , metavar = ' KEY_ID ' )
2022-08-24 11:53:11 +00:00
parser_pki = subparser . add_parser ( ' pki ' , help = ' Performs PKI operations. ' )
subparser_pki = parser_pki . add_subparsers ( title = ' commands ' , dest = ' subcommand ' )
parser_pki_init = subparser_pki . add_parser ( ' initialize ' , help = ' Initializes the Public Key Infrastructure (PKI) ' )
parser_pki_init . add_argument ( ' --certs-dir ' , help = ' Store the PKI certificates into this directory. ' , default = ' certs ' )
parser_pki_init . add_argument ( ' --default ' , help = ' Setups the default public PKI from public Pico HSM PKI. ' , action = ' store_true ' )
parser_pki_init . add_argument ( ' --force ' , help = ' Forces the download of certificates. ' , action = ' store_true ' )
2022-08-25 11:38:09 +00:00
parser_rtc = subparser . add_parser ( ' datetime ' , help = ' Datetime operations with the integrated Real Time Clock (RTC). ' )
parser_rtc . add_argument ( ' subcommand ' , choices = [ ' set ' , ' get ' ] , help = ' Sets or gets current datetime. ' )
parser_opts = subparser . add_parser ( ' options ' , help = ' Manage extra options. ' , formatter_class = RawTextHelpFormatter )
parser_opts . add_argument ( ' subcommand ' , choices = [ ' set ' , ' get ' ] , help = ' Sets or gets option OPT. ' )
parser_opts . add_argument ( ' opt ' , choices = [ ' button ' , ' counter ' ] , help = ' Button: press-to-confirm button. \n Counter: every generated key has an internal counter. ' )
parser_opts . add_argument ( ' onoff ' , choices = [ ' on ' , ' off ' ] , help = ' Toggles state ON or OFF ' , metavar = ' ON/OFF ' , nargs = ' ? ' )
2022-10-31 14:09:54 +00:00
parser_secure = subparser . add_parser ( ' secure ' , help = ' Manages security of Pico Fido. ' )
parser_secure . add_argument ( ' subcommand ' , choices = [ ' enable ' , ' disable ' , ' unlock ' ] , help = ' Enables, disables or unlocks the security. ' )
2022-08-24 11:53:11 +00:00
args = parser . parse_args ( )
return args
def get_pki_data ( url , data = None , method = ' GET ' ) :
user_agent = ' Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; '
' rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7 '
method = ' GET '
if ( data is not None ) :
method = ' POST '
2022-10-09 20:04:30 +00:00
req = urllib . request . Request ( f " https://www.henarejos.me/pico/pico-hsm/ { url } / " ,
2022-08-24 11:53:11 +00:00
method = method ,
data = data ,
headers = { ' User-Agent ' : user_agent , } )
response = urllib . request . urlopen ( req )
resp = response . read ( ) . decode ( ' utf-8 ' )
j = json . loads ( resp )
return j
def get_pki_certs ( certs_dir = ' certs ' , force = False ) :
certs = get_pki_data ( ' certs ' )
if ( os . path . exists ( certs_dir ) is False ) :
os . mkdir ( certs_dir )
cvcap = os . path . join ( certs_dir , certs [ ' cvca ' ] [ ' CHR ' ] )
dvcap = os . path . join ( certs_dir , certs [ ' dvca ' ] [ ' CHR ' ] )
if ( os . path . exists ( cvcap ) is False or force is True ) :
with open ( cvcap , ' wb ' ) as f :
2022-08-24 11:57:37 +00:00
f . write ( base64 . urlsafe_b64decode ( certs [ ' cvca ' ] [ ' cert ' ] ) )
2022-08-24 11:53:11 +00:00
if ( os . path . exists ( dvcap ) is False or force is True ) :
with open ( dvcap , ' wb ' ) as f :
2022-08-24 11:57:37 +00:00
f . write ( base64 . urlsafe_b64decode ( certs [ ' dvca ' ] [ ' cert ' ] ) )
2022-10-09 20:04:30 +00:00
print ( f ' All PKI certificates are stored at { certs_dir } folder ' )
2022-08-24 11:53:11 +00:00
def pki ( card , args ) :
if ( args . subcommand == ' initialize ' ) :
if ( args . default is True ) :
get_pki_certs ( certs_dir = args . certs_dir , force = args . force )
2022-10-09 20:04:30 +00:00
else :
print ( ' Error: no PKI is passed. Use --default to retrieve default PKI. ' )
2022-08-24 11:53:11 +00:00
2022-08-24 15:47:28 +00:00
def initialize ( card , args ) :
2022-08-18 18:09:23 +00:00
print ( ' ******************************** ' )
print ( ' * PLEASE READ IT CAREFULLY * ' )
print ( ' ******************************** ' )
print ( ' ' )
2022-08-18 23:44:27 +00:00
print ( ' This tool will erase and reset your device. It will delete all '
2022-08-24 11:53:11 +00:00
' private and secret keys. ' )
2022-08-18 18:09:23 +00:00
print ( ' Are you sure? ' )
_ = input ( ' [Press enter to confirm] ' )
2022-08-24 11:53:11 +00:00
2022-08-30 15:55:56 +00:00
send_apdu ( card , 0xA4 , 0x04 , 0x00 , [ 0xE8 , 0x2B , 0x06 , 0x01 , 0x04 , 0x01 , 0x81 , 0xC3 , 0x1F , 0x02 , 0x01 ] )
2022-08-24 15:47:28 +00:00
if ( args . pin ) :
pin = args . pin . encode ( )
try :
response = send_apdu ( card , 0x20 , 0x00 , 0x81 , list ( pin ) )
except APDUResponse :
pass
else :
pin = b ' 648219 '
2022-08-25 11:47:42 +00:00
if ( args . so_pin ) :
so_pin = args . so_pin . encode ( )
try :
response = send_apdu ( card , 0x20 , 0x00 , 0x82 , list ( so_pin ) )
except APDUResponse :
pass
else :
2022-08-30 15:55:56 +00:00
so_pin = b ' 57621880 '
2022-08-24 15:47:28 +00:00
pin_data = [ 0x81 , len ( pin ) ] + list ( pin )
2022-08-25 11:47:42 +00:00
so_pin_data = [ 0x82 , len ( so_pin ) ] + list ( so_pin )
reset_data = [ 0x80 , 0x02 , 0x00 , 0x01 ] + pin_data + so_pin_data + [ 0x91 , 0x01 , 0x03 ]
2022-08-24 11:53:11 +00:00
response = send_apdu ( card , [ 0x80 , 0x50 ] , 0x00 , 0x00 , reset_data )
response = send_apdu ( card , 0xB1 , 0xCE , 0x00 , [ 0x54 , 0x02 , 0x00 , 0x00 ] )
cert = bytearray ( response )
Y = CVC ( ) . decode ( cert ) . pubkey ( ) . find ( 0x86 ) . data ( )
print ( f ' Public Point: { hexlify ( Y ) . decode ( ) } ' )
pbk = base64 . urlsafe_b64encode ( Y )
data = urllib . parse . urlencode ( { ' pubkey ' : pbk } ) . encode ( )
j = get_pki_data ( ' cvc ' , data = data )
print ( ' Device name: ' + j [ ' devname ' ] )
dataef = base64 . urlsafe_b64decode (
j [ ' cvcert ' ] ) + base64 . urlsafe_b64decode ( j [ ' dvcert ' ] )
response = send_apdu ( card , 0xa4 , 0x00 , 0x00 , [ 0x2f , 0x02 ] )
response = send_apdu ( card , 0x20 , 0x00 , 0x81 , list ( pin ) )
apdu_data = [ 0x54 , 0x02 , 0x00 , 0x00 ] + \
list ( ASN1 . make_tag ( 0x53 , dataef ) )
response = send_apdu ( card , 0xd7 , 0x00 , 0x00 , apdu_data )
print ( ' Certificate uploaded successfully! ' )
print ( ' ' )
print ( ' Note that the device is initialized with a default PIN and '
' configuration. ' )
2022-10-09 22:39:32 +00:00
print ( ' Now you can initialize the device as usual with your chosen PIN '
2022-08-24 11:53:11 +00:00
' and configuration options. ' )
def attestate ( card , args ) :
kid = int ( args . key )
try :
response = send_apdu ( card , 0xB1 , 0x2F , 0x02 , [ 0x54 , 0x02 , 0x00 , 0x00 ] )
except APDUResponse as a :
print ( ' ERROR: There is an error with the device certificate. ' )
sys . exit ( 1 )
devcert = ASN1 ( ) . decode ( response ) . find ( 0x7f21 , pos = 0 ) . data ( return_tag = True )
try :
cert = send_apdu ( card , 0xB1 , 0xCE , kid , [ 0x54 , 0x02 , 0x00 , 0x00 ] )
except APDUResponse as a :
if ( a . sw1 == 0x6a and a . sw2 == 0x82 ) :
print ( ' ERROR: Key not found ' )
sys . exit ( 1 )
2022-10-09 20:04:30 +00:00
from binascii import hexlify
print ( hexlify ( bytearray ( cert ) ) )
2022-08-24 11:53:11 +00:00
print ( f ' Details of key { kid } : \n ' )
print ( f ' CAR: { ( CVC ( ) . decode ( cert ) . car ( ) ) . decode ( ) } ' )
print ( ' Public Key: ' )
puboid = CVC ( ) . decode ( cert ) . pubkey ( ) . oid ( )
print ( f ' Scheme: { oid2scheme ( puboid ) } ' )
chr = CVC ( ) . decode ( cert ) . chr ( )
car = CVC ( ) . decode ( cert ) . car ( )
if ( scheme_rsa ( puboid ) ) :
print ( f ' Modulus: { hexlify ( CVC ( ) . decode ( cert ) . pubkey ( ) . find ( 0x81 ) . data ( ) ) . decode ( ) } ' )
print ( f ' Exponent: { hexlify ( CVC ( ) . decode ( cert ) . pubkey ( ) . find ( 0x82 ) . data ( ) ) . decode ( ) } ' )
else :
print ( f ' Public Point: { hexlify ( CVC ( ) . decode ( cert ) . pubkey ( ) . find ( 0x86 ) . data ( ) ) . decode ( ) } ' )
print ( f ' CHR: { chr . decode ( ) } ' )
print ( ' Key signature: ' )
inret = CVC ( ) . decode ( cert ) . verify ( )
if ( inret ) :
print ( ' Status: VALID ' )
print ( f ' This certificate is signed with private key { kid } ' )
else :
print ( ' Status: NOT VALID ' )
print ( f ' This certificate is NOT signed with private key { kid } ' )
print ( ' Cert signature: ' )
print ( f ' Outer CAR: { CVC ( ) . decode ( cert ) . outer_car ( ) . decode ( ) } ' )
outret = CVC ( ) . decode ( cert ) . verify ( outer = True , dica = devcert , curve = ec . SECP256R1 ( ) )
if ( outret ) :
print ( ' Status: VALID ' )
print ( ' This certificate is signed with the device key ' )
else :
print ( ' Status: NOT VALID ' )
print ( ' This certificate is NOT signed with the device key ' )
if ( inret is True and outret is True ) :
print ( f ' Key { kid } is generated by device { chr . decode ( ) } ' )
else :
print ( f ' Key { kid } is NOT generated by device { chr . decode ( ) } ' )
2022-08-25 11:38:09 +00:00
def rtc ( card , args ) :
if ( args . subcommand == ' set ' ) :
now = datetime . now ( )
_ = send_apdu ( card , [ 0x80 , 0x64 ] , 0x0A , 0x00 , list ( now . year . to_bytes ( 2 , ' big ' ) ) + [ now . month , now . day , now . weekday ( ) , now . hour , now . minute , now . second ] )
elif ( args . subcommand == ' get ' ) :
response = send_apdu ( card , [ 0x80 , 0x64 ] , 0x0A , 0x00 )
dt = datetime ( int . from_bytes ( response [ : 2 ] , ' big ' ) , response [ 2 ] , response [ 3 ] , response [ 5 ] , response [ 6 ] , response [ 7 ] )
print ( f ' Current date and time is: { dt . ctime ( ) } ' )
def opts ( card , args ) :
opt = 0x0
if ( args . opt == ' button ' ) :
opt = 0x1
elif ( args . opt == ' counter ' ) :
opt = 0x2
current = send_apdu ( card , [ 0x80 , 0x64 ] , 0x6 , 0x0 ) [ 0 ]
if ( args . subcommand == ' set ' ) :
if ( args . onoff == ' on ' ) :
newopt = current | opt
else :
newopt = current & ~ opt
send_apdu ( card , [ 0x80 , 0x64 ] , 0x6 , 0x0 , [ newopt ] )
elif ( args . subcommand == ' get ' ) :
print ( f ' Option { args . opt . upper ( ) } is { " ON " if current & opt else " OFF " } ' )
2022-10-31 14:09:54 +00:00
class SecureLock :
def __init__ ( self , card ) :
self . card = card
def mse ( self ) :
sk = ec . generate_private_key ( ec . SECP256R1 ( ) )
pn = sk . public_key ( ) . public_numbers ( )
self . __pb = sk . public_key ( ) . public_bytes ( Encoding . X962 , PublicFormat . UncompressedPoint )
ret = send_apdu ( self . card , [ 0x80 , 0x64 ] , 0x3A , 0x01 , list ( self . __pb ) )
pk = ec . EllipticCurvePublicKey . from_encoded_point ( ec . SECP256R1 ( ) , bytes ( ret ) )
shared_key = sk . exchange ( ec . ECDH ( ) , pk )
xkdf = HKDF (
algorithm = hashes . SHA256 ( ) ,
length = 12 + 32 ,
salt = None ,
info = self . __pb
)
kdf_out = xkdf . derive ( shared_key )
self . __key_enc = kdf_out [ 12 : ]
self . __iv = kdf_out [ : 12 ]
def encrypt_chacha ( self , data ) :
chacha = ChaCha20Poly1305 ( self . __key_enc )
ct = chacha . encrypt ( self . __iv , data , self . __pb )
return ct
def unlock_device ( self ) :
ct = self . get_skey ( )
send_apdu ( self . card , [ 0x80 , 0x64 ] , 0x3A , 0x03 , list ( ct ) )
def _get_key_device ( self ) :
2022-11-07 12:14:37 +00:00
if ( platform . system ( ) == ' Windows ' or platform . system ( ) == ' Linux ' ) :
2022-11-03 15:09:42 +00:00
from secure_key import windows as skey
elif ( platform . system ( ) == ' Darwin ' ) :
from secure_key import macos as skey
else :
print ( ' ERROR: platform not supported ' )
sys . exit ( - 1 )
2022-10-31 14:09:54 +00:00
return skey . get_secure_key ( )
def get_skey ( self ) :
self . mse ( )
ct = self . encrypt_chacha ( self . _get_key_device ( ) )
return ct
def enable_device_aut ( self ) :
ct = self . get_skey ( )
send_apdu ( self . card , [ 0x80 , 0x64 ] , 0x3A , 0x02 , list ( ct ) )
def disable_device_aut ( self ) :
ct = self . get_skey ( )
send_apdu ( self . card , [ 0x80 , 0x64 ] , 0x3A , 0x04 , list ( ct ) )
def secure ( card , args ) :
slck = SecureLock ( card )
if ( args . subcommand == ' enable ' ) :
slck . enable_device_aut ( )
elif ( args . subcommand == ' unlock ' ) :
slck . unlock_device ( )
elif ( args . subcommand == ' disable ' ) :
slck . disable_device_aut ( )
2022-08-24 11:53:11 +00:00
def main ( args ) :
2022-10-31 14:09:54 +00:00
print ( ' Pico HSM Tool v1.6 ' )
2022-08-24 11:53:11 +00:00
print ( ' Author: Pol Henarejos ' )
2022-08-24 15:47:28 +00:00
print ( ' Report bugs to https://github.com/polhenarejos/pico-hsm/issues ' )
2022-08-24 11:53:11 +00:00
print ( ' ' )
print ( ' ' )
2022-08-18 18:09:23 +00:00
cardtype = AnyCardType ( )
try :
# request card insertion
cardrequest = CardRequest ( timeout = 10 , cardType = cardtype )
card = cardrequest . waitforcard ( )
2022-08-18 23:44:27 +00:00
2022-08-18 18:09:23 +00:00
# connect to the card and perform a few transmits
card . connection . connect ( )
2022-08-18 23:44:27 +00:00
2022-08-18 18:09:23 +00:00
except CardRequestTimeoutException :
print ( ' time-out: no card inserted during last 10s ' )
2022-08-24 11:53:11 +00:00
# Following commands may raise APDU exception on error
if ( args . command == ' initialize ' ) :
2022-08-24 15:47:28 +00:00
initialize ( card , args )
2022-08-24 11:53:11 +00:00
elif ( args . command == ' attestate ' ) :
attestate ( card , args )
elif ( args . command == ' pki ' ) :
pki ( card , args )
2022-08-25 11:38:09 +00:00
elif ( args . command == ' datetime ' ) :
rtc ( card , args )
elif ( args . command == ' options ' ) :
opts ( card , args )
2022-10-31 14:09:54 +00:00
elif ( args . command == ' secure ' ) :
secure ( card , args )
2022-08-18 23:44:27 +00:00
2022-08-18 18:09:23 +00:00
def run ( ) :
2022-08-24 11:53:11 +00:00
args = parse_args ( )
main ( args )
2022-08-18 23:44:27 +00:00
2022-08-18 18:09:23 +00:00
if __name__ == " __main__ " :
2022-10-09 20:04:30 +00:00
run ( )