Add codesign bits for secure boot
authorJulien Cristau <jcristau@debian.org>
Mon, 8 May 2017 08:59:39 +0000 (10:59 +0200)
committerJulien Cristau <jcristau@debian.org>
Tue, 8 Aug 2017 20:38:29 +0000 (16:38 -0400)
modules/roles/files/signing/pesign-wrap [new file with mode: 0755]
modules/roles/files/signing/secure-boot-code-sign.py [new file with mode: 0755]
modules/roles/manifests/init.pp
modules/roles/manifests/signing.pp [new file with mode: 0644]
modules/sudo/files/sudoers

diff --git a/modules/roles/files/signing/pesign-wrap b/modules/roles/files/signing/pesign-wrap
new file mode 100755 (executable)
index 0000000..a061896
--- /dev/null
@@ -0,0 +1,41 @@
+#!/usr/bin/expect -f
+
+if {[llength $argv] != 4} {
+    puts stderr "Usage: $argv0 certdir token cert filename"
+    exit 2
+}
+
+lassign $argv certdir token cert filename
+
+set pin $::env(PESIGN_PIN)
+
+file tempfile output efi.sig
+
+log_user 0
+spawn pesign --certdir "$certdir" -t "$token" -c "$cert" --sign -d sha256 -i "$filename" --export-signature "$output" --force
+expect {
+       "Enter Password *:" {
+               send "$pin\n"
+               exp_continue
+       }
+       "Enter passphrase *:" {
+               send "$pin\n"
+               exp_continue
+       }
+       timeout {close}
+}
+lassign [wait] wait_pid spawn_id exec_rc wait_code childkilled
+# couldn't exec pesign
+if {$exec_rc != 0} {
+       file delete $output
+       exit 1
+}
+# killed by signal (e.g. timeout)
+if {$childkilled == "CHILDKILLED"} {
+       file delete $output
+       exit 1
+}
+# all good?
+if {$wait_code == 0} {system cat $output}
+file delete $output
+exit $wait_code
diff --git a/modules/roles/files/signing/secure-boot-code-sign.py b/modules/roles/files/signing/secure-boot-code-sign.py
new file mode 100755 (executable)
index 0000000..04a3344
--- /dev/null
@@ -0,0 +1,138 @@
+#!/usr/bin/python3
+
+# Copyright (C) 2017 Collabora Ltd
+# 2017 Helen Koike <helen.koike@collabora.com>
+#
+# Ported from bash to python3 by Julien Cristau <jcristau@debian.org>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+
+import argparse
+import configparser
+import os
+import subprocess
+import sys
+import tarfile
+import tempfile
+
+
+config = {}
+
+
+def sign(extract_dir, signed_dir):
+    for dirpath, dirnames, filenames in os.walk(extract_dir):
+        assert dirpath.startswith(extract_dir)
+        out_dir = signed_dir + dirpath[len(extract_dir):]
+        os.makedirs(out_dir)
+        for filename in filenames:
+            #print(os.path.join(dirpath, filename), file=sys.stderr)
+            if filename.endswith('.efi') or filename.startswith('vmlinuz-'):
+                sign_efi(os.path.join(dirpath, filename), os.path.join(out_dir, filename + '.sig'))
+            elif filename.endswith('.ko'):
+                sign_kmod(os.path.join(dirpath, filename), os.path.join(out_dir, filename + '.sig'))
+            else:
+                print("ignoring %s" % os.path.join(dirpath, filename), file=sys.stderr)
+
+
+def sign_kmod(module_path, signature_path):
+    assert 'linux_sign_file' in config
+    assert 'pkcs11_uri' in config
+    assert 'cert_path' in config
+    assert 'pin' in config
+
+    env = os.environ.copy()
+    env['KBUILD_SIGN_PIN'] = config['pin']
+    # use check_output instead of check_call as sign-file seems to send random
+    # stuff to stderr even when it succeeds
+    subprocess.check_output(
+       [config['linux_sign_file'], '-d', 'sha256', config['pkcs11_uri'],
+        config['cert_path'], module_path],
+       env=env, stderr=subprocess.STDOUT)
+    os.rename(module_path + '.p7s', signature_path)
+
+
+def sign_efi(efi_path, signature_path):
+    assert 'sign-efi' in config
+    assert 'certdir' in config
+    assert 'token' in config
+    assert 'certname' in config
+    assert 'pin' in config
+
+    env = os.environ.copy()
+    env['PESIGN_PIN'] = config['pin']
+    with open(signature_path, 'wb') as out:
+        subprocess.check_call(
+           [config['sign-efi'], config['certdir'], config['token'],
+            config['certname'], efi_path],
+           env=env, stdout=out)
+
+
+def extract(tar_file, extract_dir):
+    with tarfile.TarFile.open(fileobj=tar_file, mode="r:xz") as f:
+        f.extractall(extract_dir)
+
+
+def repack(signed_dir, fileobj):
+    def cleanup_tarinfo(tarinfo):
+        tarinfo.path = os.path.relpath('/' + tarinfo.path, signed_dir)
+        tarinfo.gid = 0
+        tarinfo.gname = 'root'
+        tarinfo.uid = 0
+        tarinfo.uname = 'root'
+        return tarinfo
+
+    with tarfile.TarFile.open(mode='w:xz', fileobj=fileobj) as f:
+        f.add(signed_dir, filter=cleanup_tarinfo)
+
+
+def main():
+    parser = argparse.ArgumentParser(
+            description='sign files in a tarball')
+    parser.add_argument('input_tar', metavar='input', type=argparse.FileType('rb'),
+            help='tarball containing files to be signed')
+    parser.add_argument('--config', '-c', type=str,
+            default='/etc/codesign.ini', help='configuration file')
+
+    args = parser.parse_args()
+
+    cp = configparser.RawConfigParser()
+    cp.read(args.config)
+
+    # path to the sign-file command from Linux
+    config['linux_sign_file'] = cp.get('commands', 'sign-kmod',
+            fallback='/usr/lib/linux-kbuild-4.9/scripts/sign-file')
+    # pkcs11 uri from `p11tool --list-token-urls`
+    config['pkcs11_uri'] = cp.get('efi', 'pkcs11_uri')
+    # path to the PEM or DER-format certificate
+    config['cert_path'] = cp.get('efi', 'cert_path')
+
+    # path to our pesign wrapper script
+    config['sign-efi'] = cp.get('commands', 'sign-efi', fallback='/usr/local/bin/pesign-wrap')
+    # path to the nss store
+    config['certdir'] = cp.get('efi', 'certdir', fallback='/srv/codesign/pki')
+    # name of the token in the nss store
+    config['token'] = cp.get('efi','token', fallback='PIV_II (PIV Card Holder pin)')
+    # name of the cert in the nss store
+    config['certname'] = cp.get('efi', 'cert', fallback='Certificate for Digital Signature')
+
+    config['pin'] = cp.get('efi', 'pin')
+
+    workdir = tempfile.TemporaryDirectory()
+    with workdir:
+        extract_dir = os.path.join(workdir.name, 'in')
+        signed_dir = os.path.join(workdir.name, 'out')
+        extract(args.input_tar, extract_dir)
+        sign(extract_dir, signed_dir)
+        repack(signed_dir, sys.stdout.buffer)
+
+
+if __name__ == '__main__':
+    sys.exit(main())
index 124c188..d486e04 100644 (file)
@@ -76,6 +76,7 @@ class roles {
        if has_role('ftp_master') {
                include roles::ftp_master
                include roles::dakmaster
+               include roles::signing
        }
        if has_role('ftp.upload.d.o') {
                include roles::ftp_upload
diff --git a/modules/roles/manifests/signing.pp b/modules/roles/manifests/signing.pp
new file mode 100644 (file)
index 0000000..a959ae3
--- /dev/null
@@ -0,0 +1,20 @@
+class roles::signing {
+       package { 'expect': ensure => installed, }
+       package { 'pesign': ensure => installed, }
+       package { 'linux-kbuild-4.9': ensure => installed, }
+       package { 'libengine-pkcs11-openssl': ensure => installed, }
+
+       file { '/usr/local/bin/pesign-wrap':
+               owner => 'root',
+               group => 'root',
+               mode => '0555',
+               source => 'puppet:///modules/roles/signing/pesign-wrap',
+       }
+
+       file { '/usr/local/bin/secure-boot-code-sign':
+               owner => 'root',
+               group => 'root',
+               mode => '0555',
+               source => 'puppet:///modules/roles/signing/secure-boot-code-sign.py',
+       }
+}
index 2432bcf..e6ff944 100644 (file)
@@ -35,6 +35,7 @@ Host_Alias    PORTERBOXES     = abel, asachi, barriere, eller, falla, fischer, harris,
 Host_Alias     PIUPARTS_SLAVE_HOSTS    = piu-slave-bm-a, piu-slave-ubc-01
 Host_Alias     MQ_HOSTS        = rainier, rapoport
 Host_Alias     JENKINSHOSTS    = jerea
+Host_Alias     SIGNINGHOSTS    = fasolo
 
 # Cmnd alias specification
 
@@ -162,6 +163,8 @@ nagios              storace=(debbackup)     NOPASSWD: /usr/lib/nagios/plugins/dsa-check-backuppg
 dak            ALL=(dak-unpriv)        NOPASSWD: ALL
 # and ftpmaster can access the role user for their web services
 %debadmin      FTPHOSTS=(dak-web)      ALL
+# the dak user gets to sign stuff
+dak            SIGNINGHOSTS=(codesign) /usr/local/bin/secure-boot-code-sign
 
 # some groups are in apachectrl on "their" hosts so they can reload apache and update their vhost
 %apachectrl    ALL=(root)      /usr/sbin/apache2-vhost-update