Files
pyscrlink/pyscrlink/gencert.py
Shin'ichiro Kawasaki d845d69bb8 gencert.py: Support Snap Firefox and Chromium
Recently Ubuntu provides Firefox and Chromium in the form of Snap
package, and they place NSS DB at different path from non-Snap package.
However, current gencert.py implementation supports only the NSS DB
paths for non-Snap packages. This results in HTTPS communication failure
between the browsers and scratch_link.

Support the NSS DB paths for the Snap packages. Add a new function
prep_cert_for_app() which takes application name and its NSS DB search
path. Call this function for list of browsers, covering both non-Snap
and Snap packages.

Signed-off-by: Shin'ichiro Kawasaki <kawasaki@juno.dti.ne.jp>
2023-03-19 20:59:58 +09:00

189 lines
6.1 KiB
Python
Executable File

#!/usr/bin/env python
import subprocess
import shutil
import sys
import os
import re
from OpenSSL import crypto
import logging
logLevel = logging.INFO
# for logging
logger = logging.getLogger(__name__)
formatter = logging.Formatter(fmt='%(asctime)s %(message)s')
handler = logging.StreamHandler()
handler.setLevel(logLevel)
handler.setFormatter(formatter)
logger.setLevel(logLevel)
logger.addHandler(handler)
logger.propagate = False
# Check dependent tools
DEPENDENT_TOOLS = {
"certutil": "libnss3-tools (Ubuntu) or nss (Arch)",
}
for cmd in DEPENDENT_TOOLS:
if not shutil.which(cmd):
print(f"'{cmd}' not found. Install package {DEPENDENT_TOOLS[cmd]}.")
sys.exit(1)
# The python-nss package 1.0.1 does not provide API to delete certificates to
# NSSDB. Instead, utilize certutil command.
SCRATCH_CERT_NICKNAME = "device-manager.scratch.mit.edu"
homedir = os.path.expanduser('~')
localdir = os.path.join(homedir, ".local/share/pyscrlink/")
cert_file_path = os.path.join(localdir, "scratch-device-manager.cer")
key_file_path = os.path.join(localdir, "scratch-device-manager.key")
def gen_cert(cert_path, key_path):
"""
Generate certificate and key for scratch-link
"""
os.makedirs(localdir, exist_ok=True)
if os.path.isfile(cert_path) and os.path.isfile(key_path):
if is_cert_valid(cert_path):
logger.debug(f"Alreadfy {cert_path} and {key_path} are genereated.")
return
else:
logger.info(f"Certificate {cert_path} expired. Regenerate it.")
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 2048)
cert = crypto.X509()
cert.get_subject().CN = SCRATCH_CERT_NICKNAME
cert.gmtime_adj_notBefore(9)
cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60) # ten years
cert.set_version(2) # version 3 of X.509 (0 start)
cert.set_pubkey(key)
cert.set_issuer(cert.get_subject())
cert.add_extensions([
crypto.X509Extension(b"subjectAltName", False,
b"DNS:device-manager.scratch.mit.edu")
])
cert.sign(key, 'sha256')
with open(cert_path, "wb") as cf:
cf.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
with open(key_path, "wb") as kf:
kf.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
logger.info(f"Generated certificate: {cert_path}")
logger.info(f"Generated key: {key_path}")
def certutil_db_name(dir):
prefix = "dbm" if os.path.isfile(os.path.join(dir, "key3.db")) else "sql"
return prefix + ":" + dir
def remove_cert(dir, nickname):
while True:
p = subprocess.run(["certutil", "-L", "-d", certutil_db_name(dir),
"-n", nickname], stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
if p.returncode != 0:
break
logger.info(f"Delete certificate {nickname} from {dir}")
p = subprocess.run(["certutil", "-D", "-d", certutil_db_name(dir),
"-n", nickname], stdout=subprocess.PIPE)
def is_cert_valid(cert_path):
"""
Check if the certificate at specified path is valid
"""
with open(cert_path, "rb") as cf:
cbarr = cf.read()
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cbarr)
return not cert.has_expired()
def has_cert(dir, cert, nickname):
"""
Check if given NSSDB at dir has the certificate with the nickname.
"""
# Try to list with given nick name.
p = subprocess.run(["certutil", "-L", "-d", certutil_db_name(dir),
"-n", nickname], stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
if p.returncode != 0:
# No certificate with the nick name.
return False
# Get the certificate in the NSSDB.
p = subprocess.run(["certutil", "-L", "-d", certutil_db_name(dir),
"-n", nickname, "-a"], stdout=subprocess.PIPE)
assert (p.returncode == 0), "Unexpected certutil result"
cert_in_db = p.stdout.replace(b'\r\n', b'\n')
# Get the certificate in the specified cert file.
with open(cert, 'rb') as f:
file_barr = f.read()
cert_in_file = file_barr.replace(b'\r\n', b'\n')
# Compare the two certificates.
for i in range(len(cert_in_db)):
if cert_in_db[i] != cert_in_file[i]:
logger.info(f"Old certificate is in {dir}.")
return False
logger.debug(f"NSSDB at {dir} has valid certificate.")
return True
def add_cert(dir, cert, nickname):
"""
Add certification to the NSS db in the specified directory.
"""
p = subprocess.run(["certutil", "-A", "-d", certutil_db_name(dir),
"-n", nickname,
"-t", "C,,", "-i", cert])
return p.returncode
def prep_nss_cert(dir, cert, nickname):
"""
Prepare specified certificate with specified nickname in the NSSDB at
specified directory.
"""
if has_cert(dir, cert, nickname):
return
logger.info(f"Add the new certificate to {dir}")
remove_cert(dir, nickname)
add_cert(dir, cert, nickname)
def prep_cert_for_app(cert, app, search_path):
"""
Find a NSS DB in the search_path for the app and prepare the cert in the DB.
"""
nssdb = None
for root, dirs, files in os.walk(os.path.join(homedir, search_path)):
for name in files:
if not re.match("key.*\.db", name):
continue
nssdb = root
if prep_nss_cert(nssdb, cert, SCRATCH_CERT_NICKNAME):
logger.error(f"Failed to add certificate to {app}: {nssdb}")
sys.exit(3)
else:
logger.info(f"Certificate is ready in {app} NSS DB: {nssdb}")
if not nssdb:
logger.debug(f"NSS DB for {app} not found. Do not add certificate.")
def prep_cert():
# Generate certification and key
gen_cert(cert_file_path, key_file_path)
nss_dbs = {
"FireFox": ".mozilla/firefox/",
"FireFox(Snap)": "snap/firefox/common/.mozilla/firefox/",
"Chrome": ".pki",
"Chromium(Snap)": "snap/chromium",
}
[ prep_cert_for_app(cert_file_path, k, nss_dbs[k]) for k in nss_dbs ]
if __name__ == "__main__":
prep_cert()