summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpolz <polz@fri.uni-lj.si>2023-12-06 14:49:17 +0100
committerTimotej Lazar <timotej.lazar@araneo.si>2024-01-16 21:51:14 +0100
commit7d04aa3d86b9564f01242a347e2a861afae7c05d (patch)
treef790acd5b8eed6bf405a9d86119d105d27621ac6
parent89eabe9f87673c23954b547408cb7dfbdcf61916 (diff)
Add support for smartcards
-rw-r--r--README.md27
-rwxr-xr-xmargfools57
2 files changed, 70 insertions, 14 deletions
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 = <path/to/key.pem>
user-cert = <path/to/cert.pem>
+#### 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()