From 04e7a5d9d3ed7fbb3de5fe1cb8957aec32f7d18c Mon Sep 17 00:00:00 2001 From: S-P Chan Date: Wed, 20 Mar 2024 10:20:05 +0800 Subject: [PATCH] Add uri2pem.py tool to create pkcs11-provider PEM key files Signed-off-by: S-P Chan --- .gitignore | 1 + tools/README.md | 17 ++++++++ tools/__init__.py | 0 tools/openssl-tools.cnf | 4 ++ tools/tests/__init__.py | 0 tools/tests/test_softoken.py | 81 ++++++++++++++++++++++++++++++++++++ tools/uri2pem.py | 54 ++++++++++++++++++++++++ 7 files changed, 157 insertions(+) create mode 100644 tools/README.md create mode 100644 tools/__init__.py create mode 100644 tools/openssl-tools.cnf create mode 100644 tools/tests/__init__.py create mode 100644 tools/tests/test_softoken.py create mode 100644 tools/uri2pem.py 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..fbf48ac6 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,17 @@ +# 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' + +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..0dd3f1f3 --- /dev/null +++ b/tools/uri2pem.py @@ -0,0 +1,54 @@ +""" +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 + + parser = argparse.ArgumentParser() + parser.add_argument("--bypass", action='store_true') + parser.add_argument("keyuri", action='store') + + opts = parser.parse_args(); + + print(uri2pem(opts.keyuri, bypass=opts.bypass).decode("ascii"), end="")