Package: ftp.debian.org
Severity: important
Tags: patch security

While studying some details of
https://salsa.debian.org/ftp-team/code-signing/-/blob/master/secure-boot-code-sign.py,
I noticed that the "Check that files.json doesn't use absolute paths" in
check_template doesn't seem quite sufficient.  The current code is as
follows:

    # Check that files.json doesn't use absolute paths
    for pkg, metadata in job.bin_pkgs.items():
        assert(re.match(r'[a-z0-9][a-z0-9.+-]+\Z', pkg))
        for file in metadata['files']:
            assert(any(file["file"].startswith(prefix) for prefix in ("boot/", 
"lib/", "usr/")))
            assert(".." not in file["file"])
            assert(not os.path.isabs(file["file"]))

Consider packages that look like this:

  foo:
    ./usr/lib -> ../../../../../etc

  foo-signed-template:
    ./usr/share/code-signing/foo-signed-template/files.json:
      {"foo": {"files": [{"sig_type": "efi", "file": "usr/lib/passwd"}]}}

This will pass, because the file name starts with "usr/", doesn't
contain "..", and isn't absolute.  sign_and_log_files will then go on to
resolve those file names relative to the extracted binary package being
signed, following symlinks, and might be able to read any data
accessible to the user running secure-boot-code-sign.py.

(Incidentally, the check for ".." there is implemented weirdly.  It
should check that none of the individual segments are "..", not that the
substring ".." appears nowhere in the path.)

Obviously this would never pass any kind of attentive manual check, and
there are probably limited ways in which you could actually use this to
extract information, so I can't really justify RC severity here; but
since you have these checks in place I'm assuming that you think it's
worth them being effective.

The following is a mostly untested patch (though I've tested some of the
individual bits of code in isolation).  I generally find it easier to
get this stuff right using pathlib nowadays, so I converted the relevant
bits of code to that, on the assumption that you have at least Python
3.9 for Path.is_relative_to.

I considered filing this as a merge request, but since nobody's replied
to either of my fairly straightforward MRs in
https://salsa.debian.org/ftp-team/code-signing/-/merge_requests, I
suspect that the right people might not be subscribed to those.

diff --git a/secure-boot-code-sign.py b/secure-boot-code-sign.py
index 97f86c1..81ed844 100755
--- a/secure-boot-code-sign.py
+++ b/secure-boot-code-sign.py
@@ -24,6 +24,7 @@ import fcntl
 import hashlib
 import json
 import os
+import pathlib
 import re
 import subprocess
 import sys
@@ -160,7 +161,7 @@ def hash_file(f):
     return m.hexdigest()
 
 
-def sign_kmod(key_config: Dict[str, str], module_path: str, signature_path: 
str) -> str:
+def sign_kmod(key_config: Dict[str, str], module_path: pathlib.Path, 
signature_path: pathlib.Path) -> str:
     env = os.environ.copy()
     if config['efi']['pin'] is not None:
         env['KBUILD_SIGN_PIN'] = config['efi']['pin']
@@ -175,7 +176,7 @@ def sign_kmod(key_config: Dict[str, str], module_path: str, 
signature_path: str)
         stderr=subprocess.STDOUT
     )
 
-    os.rename(module_path + '.p7s', signature_path)
+    pathlib.Path(str(module_path) + '.p7s').rename(signature_path)
 
     with open(signature_path, 'rb') as f:
         fhash = hash_file(f)
@@ -183,7 +184,7 @@ def sign_kmod(key_config: Dict[str, str], module_path: str, 
signature_path: str)
     return fhash
 
 
-def sign_efi(key_config: Dict[str, str], efi_path: str, signature_path: str) 
-> str:
+def sign_efi(key_config: Dict[str, str], efi_path: pathlib.Path, 
signature_path: pathlib.Path) -> str:
     env = os.environ.copy()
     if config['efi']['pin'] is not None:
         env['PESIGN_PIN'] = str(config['efi']['pin'])
@@ -309,12 +310,14 @@ def check_template(job):
     # Check signatures folder doesn't exist
     assert(not os.path.exists(os.path.join(job.template_source_dir, 'debian', 
'signatures')))
     # Check that files.json doesn't use absolute paths
+    # (sign_and_log_files has additional symlink checks)
     for pkg, metadata in job.bin_pkgs.items():
         assert(re.match(r'[a-z0-9][a-z0-9.+-]+\Z', pkg))
         for file in metadata['files']:
-            assert(any(file["file"].startswith(prefix) for prefix in ("boot/", 
"lib/", "usr/")))
-            assert(".." not in file["file"])
-            assert(not os.path.isabs(file["file"]))
+            file_path = pathlib.PurePath(file["file"])
+            assert file_path.parts[0] in {"boot", "lib", "usr"}
+            assert ".." not in file_path.parts
+            assert not file_path.is_absolute()
     # Check that all trusted certificates are whitelisted
     trusted_certs = config['keys']['trusted_certs']
     if '*' not in trusted_certs:
@@ -367,25 +370,26 @@ def sign_and_log_files(job):
     key_config = config['signing-keys'][key_name]
     for pkg, metadata in job.bin_pkgs.items():
         maybe_interactive_print('Processing', pkg)
+        pkg_path = pathlib.Path(job.tempdir, build_pkg_full_name(job, 
pkg)).resolve(strict=True)
         for n, file in enumerate(metadata['files'], start=1):
-            relative_file_path = os.path.join(build_pkg_full_name(job, pkg), 
file["file"])
-            file_path = os.path.join(job.tempdir, build_pkg_full_name(job, 
pkg), file["file"])
-            sig_path = os.path.join(job.template_source_dir, 'debian', 
'signatures', pkg, file["file"] + ".sig")
+            relative_file_path = pathlib.PurePath(build_pkg_full_name(job, 
pkg), file["file"])
+            file_path = pkg_path / file["file"]
+            sig_path = pathlib.Path(job.template_source_dir, 'debian', 
'signatures', pkg, file["file"] + ".sig")
+            assert file_path.resolve(strict=True).is_relative_to(pkg_path)
 
             with open(file_path, 'rb') as f:
                 fhash = hash_file(f)
 
-            presign_id = log_presign(relative_file_path, fhash, pkg, 
job.version, job.architecture)
+            presign_id = log_presign(str(relative_file_path), fhash, pkg, 
job.version, job.architecture)
 
-            if not os.path.exists(os.path.dirname(sig_path)):
-                os.makedirs(os.path.dirname(sig_path))
+            sig_path.parent.mkdir(parents=True, exist_ok=True)
             if file["sig_type"] == "efi":
                 sig_hash = sign_efi(key_config, file_path, sig_path)
             elif file["sig_type"] == "linux-module":
                 sig_hash = sign_kmod(key_config, file_path, sig_path)
             else:
                 raise ValueError("File Type Unknown")
-            log_signature(relative_file_path, sig_hash, pkg, job.version, 
presign_id, job.architecture, key_name)
+            log_signature(str(relative_file_path), sig_hash, pkg, job.version, 
presign_id, job.architecture, key_name)
             maybe_interactive_print('\rSigned', n, 'of', 
len(metadata['files']), 'files', end='')
         maybe_interactive_print()
 

Thanks,

-- 
Colin Watson (he/him)                              [cjwat...@debian.org]

Reply via email to