diff --git a/README.md b/README.md index 3e354fb..b0a9e13 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,37 @@ Python script to replace [MargTools](https://businessconnect.margis.si/output/#o ## Usage -Create the configuration file `~/.margfools` with the paths to your TLS private key and certificate in PEM format: + +### Configure certificates and sites + +Create the configuration file `~/.margfools`. The contents are described in the sections below. + +#### Certificates in files +If you are using certificate files, add the paths to your TLS private key and certificate in PEM format: [https://gcsign.example.com/BCSign/] user-key = user-cert = +#### Certificates on smartcards +If you have your certificate on a PIV-II smart card (e.g. Yubikey), first determine the slot on your card which contains the certificate you wish to use: + + pkcs11-tool -O + +Look for "ID:" in the output. + +Assuming the ID of your certificate was 07, specify the engine and certificate slot in your config file: + + + [https://gcsign.example.com/BCSign/] + engine=pkcs11 + user-key = 07 + + +You will be asked for your pin during signing. + +### Add URL schema + Section name is the percent-decoded value of `baseURL` in bc-digsign://sign?accessToken=…&baseUrl=https%3a%2f%2fgcsign.example.com%2fBCSign%2f&…' diff --git a/margfools b/margfools index fed38bf..5df99a2 100755 --- a/margfools +++ b/margfools @@ -10,14 +10,33 @@ import subprocess import sys import traceback import urllib.parse +import getpass # use requests instead of urllib.request for keep-alive connection import requests -def sign(data, keyfile): +def sign(data, keyfile, unlock_key=None, engine=None): + # pkcs11-tool --id 02 -s -p $PIN -m RSA-PKCS + if engine is None: + cmd = ['openssl', 'pkeyutl', '-sign', '-inkey', keyfile, '-pkeyopt', 'digest:sha256'] + raw_data = base64.b64decode(data) + env = None + elif engine == 'pkcs11': + env = {'PIN': unlock_key} + """magic_prefix is ASN.1 DER for + DigestInfo ::= SEQUENCE { + digestAlgorithm DigestAlgorithm, + digest OCTET STRING + } + """ + magic_prefix = bytes.fromhex("3031300d060960864801650304020105000420") + raw_data = magic_prefix + base64.b64decode(data) + cmd = ['pkcs11-tool', '--id', keyfile, '-s', '-m', 'RSA-PKCS', '-p', 'env:PIN'] + # cmd = ['openssl', 'pkeyutl', '-sign', '-pkeyopt', 'digest:sha256', '-engine', 'pkcs11', '-keyform', 'engine', '-inkey', keyfile] p = subprocess.run( - ['openssl', 'pkeyutl', '-sign', '-inkey', keyfile, '-pkeyopt', 'digest:sha256'], - input=base64.b64decode(data), + cmd, + env=env, + input=raw_data, capture_output=True) return base64.b64encode(p.stdout).decode() @@ -26,6 +45,7 @@ if __name__ == '__main__': parser.add_argument('url', type=urllib.parse.urlparse, help='bc-digsign:// url') parser.add_argument('-k', '--user-key', type=pathlib.Path, help='key file') parser.add_argument('-c', '--user-cert', type=pathlib.Path, help='certificate file') + parser.add_argument('-e', '--engine', type=str, help='"pkcs11" for smart card') args = parser.parse_args() try: @@ -40,14 +60,24 @@ if __name__ == '__main__': if not args.user_key: args.user_key = config.get(url, 'user-key') if not args.user_cert: - args.user_cert = config.get(url, 'user-cert') - if not args.user_key or not args.user_cert: - print('user key and/or certificate not specified', file=sys.stderr) + args.user_cert = config.get(url, 'user-cert', fallback=None) + if not args.user_key: + print('user key not specified', file=sys.stderr) sys.exit(1) - + if not args.engine: + args.engine = config.get(url, 'engine') + engine = args.engine user_keyfile = args.user_key - user_cert = ''.join(line.strip() for line in open(args.user_cert) if not line.startswith('-----')) - + # base64.b64encode + unlock_key = None + if engine is None: + if not args.user_cert: + print('user cert not specified', file=sys.stderr) + sys.exit(1) + user_cert = ''.join(line.strip() for line in open(args.user_cert) if not line.startswith('-----')) + elif engine == 'pkcs11': + user_cert = base64.b64encode(subprocess.run(["pkcs11-tool", "--read-object", "--type", "cert", "--id", user_keyfile], capture_output=True).stdout).decode() + unlock_key = getpass.getpass("PIN:") session = requests.Session() headers={'Authorization': f'Bearer {token}'} @@ -66,19 +96,20 @@ if __name__ == '__main__': request = json.loads(r.text) request['AuthenticationToken'] = token request['CertificatePublicKey'] = user_cert - # keep signing whatever they send us while True: for name in ('AttachmentHashes', 'XmlHashes'): if request.get(name) is not None: - request[f'Signed{name}'] = [sign(e, user_keyfile) for e in request[name]] - + request[f'Signed{name}'] = [sign(e, user_keyfile, unlock_key, engine=engine) for e in request[name]] + d = json.dumps(request) + d = d.encode() r = session.put(f'{url}signatures/{request["SignatureRequestId"]}', headers=headers | {'Content-Type': 'application/json; charset=utf-8'}, data=json.dumps(request).encode()) if not r.text: break request |= json.loads(r.text) - except: traceback.print_exc() + # Don't close terminal immediately on fail + input()