From 82c99a0a66f80c5189078004e39b6f1506eb183d Mon Sep 17 00:00:00 2001 From: Jeremy Cline Date: Apr 18 2025 16:46:23 +0000 Subject: [PATCH 1/2] sign_pe: Add the ability to sign PE applications via PKCS#11 This adds a new key type, PKCS#11 (yes, not _really_ a type, but there's already a gnupg key type) and adds a check for the key type in the sign_pe command. Additionally, there's a helper script to add a row into the keys table for each PKCS#11-backed key you wish to sign with. Signed-off-by: Jeremy Cline --- diff --git a/src/server.py b/src/server.py index df4f2ff..bf73fcb 100644 --- a/src/server.py +++ b/src/server.py @@ -1624,6 +1624,8 @@ def cmd_delete_key(db, conn): key = key_by_name(db, conn) if key.keytype == KeyTypeEnum.gnupg: server_common.gpg_delete_key(conn.config, key.fingerprint) + elif key.keytype == KeyTypeEnum.PKCS11: + pass else: remove_non_gnupg_key(conn.config, key.fingerprint) for a in key.key_accesses: @@ -2049,9 +2051,97 @@ class TempNSSDb(object): @request_handler(payload_storage=RequestHandler.PAYLOAD_FILE) def cmd_sign_pe(db, conn): (access, key_passphrase) = conn.authenticate_user(db) - if not access.key.keytype.supports_pe(): + if access.key.keytype == KeyTypeEnum.RSA: + sign_pe(conn, access, key_passphrase) + elif access.key.keytype == KeyTypeEnum.PKCS11: + sign_pe_pkcs11(conn, access, key_passphrase) + else: conn.send_error(errors.UNSUPPORTED_KEYTYPE) + +def sign_pe_pkcs11(conn, access, key_passphrase): + """ + Sign a PE application using PKCS#11. + + For this to work properly, the following things must be true: + + - The PKCS#11 module that contains the token the key is in must be + configured via p11-kit. + + - The sigul user must be able to access the token; this may vary by + module. For example, the sigul user needs to own the softhsm2 directory + if you use that module (which you should not do in production). + + - The certificate for the key pair must also be in the token, and it must + have a label applied to it that matches the cert name provided here. It + must also have an id that matches the private key. + + At this time, there are no remote commands to manage PKCS#11 keys or certificates, + and the keys and certificates must be set up using other tools (pkcs11-tool, for + example). + + To make Sigul aware of the signing key and the PIN to access it, the + `server_add_pkcs11_token.py` script is provided. + """ + # This corresponds to the label applied to the certificate in the token + cert_name = conn.safe_outer_field("cert-name") + logging.info("sign_pe_pkcs11: signing request using '%s' received", cert_name) + + token_name = access.key.pkcs11_token_name() + logging.info("sign_pe_pkcs11: using key '%s' in '%s' token", access.key.fingerprint, token_name) + + # We don't actually load anything into the NSS database, but pesign talks to + # the PKCS11 token through it. + nssdir = TempNSSDb() + pw_r = nssdir.prepare_secretfile(key_passphrase.encode('utf-8') + b'\n') + signature_file = tempfile.TemporaryFile() + try: + with tempfile.TemporaryFile() as signature_file: + subprocess.run( + [ + "/usr/bin/pesign", + "--sign", + "--in", conn.payload_file.name, + "--out", f"/dev/fd/{signature_file.fileno()}", + "--force", + "--certdir", nssdir.db_dir, + "--certificate", cert_name, + "--nofork", + "--pinfile", f"/dev/fd/{pw_r.fileno()}", + "--token", token_name, + ], + check=True, + stdin=subprocess.DEVNULL, + pass_fds=( + pw_r.fileno(), + signature_file.fileno(), + ), + timeout=60, + capture_output=True, + ) + logging.info( + "Signed PE file with key %s, certificate %s", + access.key.name, + cert_name, + ) + + conn.send_reply_header(errors.OK, {}) + conn.send_reply_payload_from_file(signature_file) + except subprocess.TimeoutExpired: + logging.error("pesign command timed out") + conn.send_reply_header(errors.UNKNOWN_ERROR, {}) + except subprocess.CalledProcessError as err: + logging.error( + "Command failed: %s returned %d (stderr=%s, stdout=%s)", + err.cmd, + err.returncode, + err.stderr, + err.stdout + ) + conn.send_reply_header(errors.UNKNOWN_ERROR, {}) + + +def sign_pe(conn, access, key_passphrase): privkey = non_gnupg_private_key( conn.config, access.key.fingerprint, diff --git a/src/server_add_pkcs11_token.py b/src/server_add_pkcs11_token.py new file mode 100644 index 0000000..c1b714e --- /dev/null +++ b/src/server_add_pkcs11_token.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2025 Microsoft Corporation. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +from server import ServerConfiguration +import server_common +import utils + + +def main(): + """ + Register a PKCS#11 token to use when signing PE applications. + + Adds a new private key to the Sigul database using the PKCS11 key type. + The key added is a reference to the key that resides in the token. + """ + cli_parser = utils.create_basic_parser( + "Register a PKCS#11 token to use when signing PE applications.", + "~/.sigul/server.conf", + ) + cli_parser.add_option("-a", "--initial-key-admin", help="The key admin's username") + cli_parser.add_option("-k", "--key-uri", help="The key's PKCS#11 URI") + cli_parser.add_option("-n", "--key-name", help="The key name; used by the sigul client") + cli_parser.add_option( + "-t", + "--token-pin-file", + help="File containing PIN required to log into the PKCS#11 token", + ) + cli_parser.add_option( + "-p", + "--passphrase-file", + help="File containing the user passphrase to access the key", + ) + + options = utils.optparse_parse_options_only(cli_parser) + config = ServerConfiguration(options.config_file) + db = server_common.db_open(config) + + token_pin = open(options.token_pin_file, "rt").readline().strip() + user_passphrase = open(options.passphrase_file, "rt").readline().strip() + + # The fingerprint is used to ensure the file is unique, but since the keys are stored in hardware, we use + # this field to store the key URI. + admin = ( + db.query(server_common.User).filter_by(name=options.initial_key_admin).first() + ) + key = server_common.Key(options.key_name, "PKCS11", options.key_uri) + db.add(key) + access = server_common.KeyAccess(key, admin, key_admin=True) + access.set_passphrase( + config, + key_passphrase=token_pin, + user_passphrase=user_passphrase, + bind_params=None, + ) + db.add(access) + db.commit() + + +if __name__ == "__main__": + main() diff --git a/src/server_common.py b/src/server_common.py index 1f6ced2..e069ecd 100644 --- a/src/server_common.py +++ b/src/server_common.py @@ -25,6 +25,7 @@ from six import StringIO import shutil import subprocess import tempfile +from urllib import parse from cryptography import x509 import cryptography.hazmat.primitives.asymmetric.ec @@ -119,6 +120,14 @@ class Key(object): self.keytype = keytype self.fingerprint = fingerprint + def pkcs11_token_name(self): + if self.keytype != KeyTypeEnum.PKCS11: + raise ValueError("The key type must be PKCS11") + token_name = [ + param for param in self.fingerprint.split(";") if param.startswith("token=") + ].pop() + return parse.unquote(token_name.split("=", maxsplit=1)[1]) + class KeyAccess(object): @@ -162,12 +171,15 @@ class KeyTypeEnum(enum.Enum): gnupg = 1 ECC = 2 RSA = 3 + #: Technically this is more about _where_ it's stored, but so is gnupg. + #: We assume the user wants to do pesigning with it. + PKCS11 = 4 def supports_ca(self): return self != KeyTypeEnum.gnupg def supports_pe(self): - return self == KeyTypeEnum.RSA + return self in (KeyTypeEnum.RSA, KeyTypeEnum.PKCS11) sa = sqlalchemy From e85dbeae2f28222402b41bdf3aa5174506c80acf Mon Sep 17 00:00:00 2001 From: Jeremy Cline Date: Apr 18 2025 16:46:24 +0000 Subject: [PATCH 2/2] db: Add a database migration for pkcs11 key type The v11 keys table introduces a constraint on the keytype to be one of 'gnupg' or 'ECC'. There is, however, an additional key type of 'RSA' which is required when using pesign, and the HSM support introduces a further key type of 'PKCS11' (yes, not really a type). Introduce the new 'PKCS11' type to the keytype column constraint, as well as add in the forgotten 'RSA' type. Signed-off-by: Jeremy Cline --- diff --git a/dbschema_v11_to_v12.sql b/dbschema_v11_to_v12.sql new file mode 100644 index 0000000..d24243c --- /dev/null +++ b/dbschema_v11_to_v12.sql @@ -0,0 +1,23 @@ +PRAGMA writable_schema=on; + +CREATE TABLE keys_v12 ( + id INTEGER NOT NULL, + name TEXT NOT NULL, + keytype VARCHAR(5) NOT NULL, + fingerprint TEXT NOT NULL, + PRIMARY KEY (id), + UNIQUE (name), + CONSTRAINT keytypeenum CHECK (keytype IN ('gnupg', 'ECC', 'RSA', 'PKCS11')), + UNIQUE (fingerprint) +); + +INSERT INTO keys_v12 SELECT id,name,keytype,fingerprint FROM keys; + +ALTER TABLE keys RENAME TO keys_v11; +ALTER TABLE keys_v12 RENAME TO keys; + +PRAGMA writable_schema=off; + +PRAGMA foreign_key_check; +PRAGMA integrity_check; +