diff --git a/.gitignore b/.gitignore index 679cc2e9..dbbc3b72 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,4 @@ cscope.files cscope.out cscope.in.out cscope.po.out +__pycache__/ diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 00000000..e7ccf5fc --- /dev/null +++ b/tools/README.md @@ -0,0 +1,21 @@ +# CLI tool uri2pem.py + +Simple tool to create PEM files for PKCS#11 URI +Usage: + + python uri2pem.py --help + python uri2pem.py 'pkcs11:token=MyToken;object=MyObject;type=private' + python uri2pem.py --bypass 'someBogusURI' + # output + python uri2pem.py --out mykey.pem 'pkcs11:token=MyToken;object=MyObject;type=private' + # verification, if token is available, requires --out + python uri2pem.py --verify --out mykey.pem 'pkcs11:token=MyToken;object=MyObject;type=private' + +The tool doesn't validate the argument for a valid PKCS#11 URI + +## Tests + +If you run `make check` the tool has a test suite that will run +against NSS softoken in `../tests/tmp.softokn/tests`. + +The test suite enables `pkcs11-module-encode-provider-uri-to-pem = true`. diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/openssl-tools.cnf b/tools/openssl-tools.cnf new file mode 100644 index 00000000..2f50d796 --- /dev/null +++ b/tools/openssl-tools.cnf @@ -0,0 +1,4 @@ +.include ../tests/tmp.softokn/openssl.cnf + +[pkcs11_sect] +pkcs11-module-encode-provider-uri-to-pem = true diff --git a/tools/tests/__init__.py b/tools/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/tests/test_softoken.py b/tools/tests/test_softoken.py new file mode 100644 index 00000000..4eb774e6 --- /dev/null +++ b/tools/tests/test_softoken.py @@ -0,0 +1,81 @@ +import os +import pathlib +import subprocess +import sys +import random +import string +import re + +from asn1crypto import pem +from .. import uri2pem + +tokens = pathlib.Path("../tests/tmp.softokn/tokens/key4.db") + + +if not tokens.exists(): + print("Run 'make check' first to create a NSS softoken in tests/tmp.softokn/tokens") + raise SystemExit(1) + + +P11_TOKEN = "".join(random.choices(string.ascii_letters, k=12)) +P11_OBJECT = "".join(random.choices(string.ascii_letters, k=12)) +KEY_URI = f"pkcs11:token={P11_TOKEN};object={P11_OBJECT};type=private" +KEY_DESC = "PKCS#11 Provider URI v1.0" + + +def test_roundtrip(tmp_path): + + pem_bytes = uri2pem.uri2pem(KEY_URI) + # asn1crypto does not like '#' in PEM labels + pem_replace = pem_bytes.decode("ascii").replace("#", "0") + + # read back the object + der_bytes = pem.unarmor(pem_replace.encode("ascii"), multiple=False) + key = uri2pem.Pkcs11PrivateKey.load(der_bytes[2]) + + assert key["desc"].native == KEY_DESC + assert key["uri"].native == KEY_URI + + +def test_asn1parse(tmp_path): + pem_bytes = uri2pem.uri2pem(KEY_URI) + pem_file = pathlib.Path(tmp_path / "test_asn1parse.pem") + pathlib.Path(tmp_path / "test_asn1parse.pem").write_bytes(pem_bytes) + ret = subprocess.run( + ["openssl", "asn1parse", "-in", str(pem_file)], capture_output=True, text=True + ) + + assert ret.returncode == 0 + assert "SEQUENCE" in ret.stdout and KEY_DESC in ret.stdout and KEY_URI in ret.stdout + + +def test_storeutl(tmp_path): + ret = subprocess.run( + ["openssl", "storeutl", "-text", "pkcs11:"], + capture_output=True, + text=True, + env={"OPENSSL_CONF": "./openssl-tools.cnf"} + ) + + assert ret.returncode == 0 + + private_key = None + for line in ret.stdout.splitlines(): + if m := re.match("URI (pkcs11.*type=private)$", line): + private_key = m.group(1) + break + + assert private_key + + data = uri2pem.uri2pem(private_key) + private_key_pem = pathlib.Path(tmp_path / "private_key.pem") + private_key_pem.write_bytes(data) + + ret = subprocess.run( + ["openssl", "pkey", "-in", str(private_key_pem)], + capture_output=True, + text=True, + env={"OPENSSL_CONF": "./openssl-tools.cnf"} + ) + + assert ret.returncode == 0 diff --git a/tools/uri2pem.py b/tools/uri2pem.py new file mode 100644 index 00000000..e6dee87f --- /dev/null +++ b/tools/uri2pem.py @@ -0,0 +1,73 @@ +""" +Copyright (C) 2024 S-P Chan +SPDX-License-Identifier: Apache-2.0 +""" + +""" +CLI tool to create pkcs11-provider pem files from a key uri +Requirements: asn1crypto + +Installation: + pip install asn1crypto + dnf install python3-asn1crypto + +Usage: + python uri2pem.py 'pkcs11:URI-goes-here' +""" + +import sys +from asn1crypto.core import Sequence, VisibleString, UTF8String +from asn1crypto import pem + + +class Pkcs11PrivateKey(Sequence): + _fields = [("desc", VisibleString), ("uri", UTF8String)] + + +def uri2pem(uri: str, bypass: bool = False) -> bytes: + if not bypass: + if not (uri.startswith("pkcs11:") and "type=private" in uri): + print(f"Error: uri({uri}) not a valid PKCS#11 URI") + sys.exit(1) + if not ("object=" in uri or "id=" in uri): + print(f"Error: uri({uri}) does not specify an object by label or id") + sys.exit(1) + + data = Pkcs11PrivateKey( + { + "desc": VisibleString("PKCS#11 Provider URI v1.0"), + "uri": UTF8String(uri), + } + ) + return pem.armor("PKCS#11 PROVIDER URI", data.dump()) + + +if __name__ == "__main__": + import argparse + import pathlib + import subprocess + + parser = argparse.ArgumentParser() + parser.add_argument("--bypass", action='store_true') + parser.add_argument("--verify", action='store_true') + parser.add_argument("--out", action='store') + parser.add_argument("keyuri", action='store') + + opts = parser.parse_args() + if opts.verify and not opts.out: + print(f"{sys.argv[0]}: --verify option requires --out to be specified") + sys.exit(1) + + data = uri2pem(opts.keyuri, bypass=opts.bypass) + if opts.out: + out_file = pathlib.Path(opts.out) + out_file.write_bytes(data) + else: + print(data.decode("ascii"), end="") + + if opts.verify: + ret = subprocess.run(["openssl", "pkey", "-in", str(out_file), "-pubout"]) + if ret.returncode != 0: + print(f"{sys.argv[0]}: verification of private key PEM({str(out_file)}) failed") + else: + print(f"{sys.argv[0]}: verification of private key PEM({str(out_file)}) OK")