[swugenerator] [PATCH 2/3] Add a sign subcommand to sign an existing

7 views
Skip to first unread message

INgo...@gmx.net

unread,
Oct 22, 2025, 10:56:11 AM (8 days ago) Oct 22
to swupdate
With this feature the signing of the swu file
can be done in a second call after the generation,
because signing could require a different environment.
Refactor SWUFile supporting read and write
Change SWUFile close to add_trailer as it is not closing but writing

Signed-off-by: INgo Rah <ingo...@linutronix.de>
---
README.rst | 20 +++++--
swugenerator/generator.py | 2 +-
swugenerator/main.py | 30 +++++++++--
swugenerator/signer.py | 43 +++++++++++++++
swugenerator/swu_file.py | 107 +++++++++++++++++++++++++++++++++-----
5 files changed, 180 insertions(+), 22 deletions(-)
create mode 100644 swugenerator/signer.py

diff --git a/README.rst b/README.rst
index 40d76f2..26b860b 100644
--- a/README.rst
+++ b/README.rst
@@ -8,28 +8,36 @@ A host tool to generate SWU update package for SWUpdate.
SYNOPSIS
========
-usage: SWUGenerator [-h] [-K ENCRYPTION_KEY_FILE] [-k SIGN] -s SW_DESCRIPTION
+usage: SWUGenerator [-h] [-K ENCRYPTION_KEY_FILE] [-k SIGN] [-s SW_DESCRIPTION]
[-a ARTIFACTORY] -o SWU_FILE [-c CONFIG]
command
Generator SWU Packages for SWUpdate
positional arguments:
- command command to be executed, one of : create
+ command:
+ {create,sign} command to be executed
+ create creates a SWU file
+ sign signs an existing SWU file provided by -i
optional arguments:
-h, --help show this help message and exit
-K ENCRYPTION_KEY_FILE, --encryption-key-file ENCRYPTION_KEY_FILE
AES Key to encrypt artifacts
-n, --no-compress Do not compress files
+ -e, --no-encrypt Do not encrypt files
+ -x, --no-ivt Do not generate IV when encrypting
+ -y, --no-hash Do not store sha256 hash in sw-description
-k SIGN, --sign SIGN RSA key or certificate to sign the SWU
-s SW_DESCRIPTION, --sw-description SW_DESCRIPTION
- sw-description template
+ sw-description template for the create command
-t, --encrypt-swdesc Encrypt sw-description
-a ARTIFACTORY, --artifactory ARTIFACTORY
list of directories where artifacts are searched
-o SWU_FILE, --swu-file SWU_FILE
SWU output file
+ -i, --swu-in-file SWU_IN_FILE
+ SWU input file to be signed for the sign command
-c CONFIG, --config CONFIG
configuration file
@@ -52,6 +60,12 @@ The tool signs the SWU and can encrypt the artifacts. The tool parses the libcon
- sign sw-description with one of the methods accepted by SWUpdate
- pack all artifacts into a SWU file
+It maybe run in two steps to create an unsigned swu file and then sign it later in a second call::
+
+ swugenerator -o output.swu -a . -s sw-description.in create
+ swugenerator -o signed_output.swu -i output.swu -k CMS,key.pem,ca.crt sign
+
+
Installation
============
diff --git a/swugenerator/generator.py b/swugenerator/generator.py
index dc0af31..6087a64 100644
--- a/swugenerator/generator.py
+++ b/swugenerator/generator.py
@@ -63,7 +63,7 @@ class SWUGenerator:
def close(self):
self.temp.cleanup()
- self.cpiofile.close()
+ self.cpiofile.add_trailer()
self.out.close()
def process_entry(self, entry):
diff --git a/swugenerator/main.py b/swugenerator/main.py
index fbc28f4..b5ede6b 100644
--- a/swugenerator/main.py
+++ b/swugenerator/main.py
@@ -15,7 +15,8 @@ from typing import List, Optional, Tuple, Union
import libconf
-from swugenerator import __about__, generator
+from swugenerator import __about__, generator, signer
+
from swugenerator.swu_sign import SWUSignCMS, SWUSignCustom, SWUSignPKCS11, SWUSignRSA
@@ -204,6 +205,14 @@ def create_swu(args: argparse.Namespace) -> None:
swu.process()
swu.close()
+def sign_swu(args: argparse.Namespace) -> None:
+ swu = signer.SWUSigner(
+ args.swu_in_file,
+ args.swu_file,
+ args.sign
+ )
+ swu.process()
+ swu.close()
def parse_args(args: List[str]) -> None:
"""Sets up arguments for swugenerator and parses commandline args
@@ -273,9 +282,9 @@ def parse_args(args: List[str]) -> None:
parser.add_argument(
"-s",
"--sw-description",
- required=True,
+ required=False,
type=lambda p: Path(p).resolve(),
- help="sw-description template",
+ help="sw-description template, required for the create command",
)
parser.add_argument(
@@ -338,10 +347,23 @@ def parse_args(args: List[str]) -> None:
)
create_subparser = subparsers.add_parser("create", help="creates a SWU file")
create_subparser.set_defaults(func=create_swu)
-
+ sign_subparser = subparsers.add_parser("sign", help="signs an existing SWU file provided by -i")
+ sign_subparser.set_defaults(func=sign_swu)
+ sign_subparser.add_argument(
+ "-i",
+ "--swu-in-file",
+ required=True,
+ type=Path,
+ help="SWU input file to be signed, required for the sign command",
+ )
args = parser.parse_args(args)
if hasattr(args, 'sign') and args.sign:
args.sign = parse_signing_option(args.sign, args.engine, args.keyform)
+ if args.func == create_swu and not args.sw_description:
+ parser.error(
+ "the following arguments are required: -s/--sw-description"
+ )
+
args.func(args)
diff --git a/swugenerator/signer.py b/swugenerator/signer.py
new file mode 100644
index 0000000..2b75d3a
--- /dev/null
+++ b/swugenerator/signer.py
@@ -0,0 +1,43 @@
+# Copyright (C) 2025 INgo Rah
+#
+# SPDX-License-Identifier: GPLv3
+import logging
+from tempfile import TemporaryDirectory
+from pathlib import Path
+
+from swugenerator.swu_file import SWUFile
+
+class SWUSigner:
+ def __init__(self, in_file, out_file, crypt):
+ self.signtool = crypt
+ self.fout = open(out_file, "wb")
+ self.cpio_out = SWUFile(self.fout)
+ self.fin = open(in_file, "rb")
+ self.cpio_in = SWUFile(self.fin)
+ self.temp = TemporaryDirectory()
+
+ def close(self):
+ self.temp.cleanup()
+ self.fin.close()
+ self.cpio_out.add_trailer()
+ self.fout.close()
+
+ def process(self):
+ # extract the swu file and get the file list
+ artifacts = self.cpio_in.extract(self.temp.name)
+
+ # call signing to generate the sig file
+ if self.signtool:
+ sw_desc_in = Path(self.temp.name) / artifacts[0]
+ sw_desc_out = sw_desc_in.with_suffix(".sig")
+ self.signtool.prepare_cmd(sw_desc_in, sw_desc_out)
+ self.signtool.sign()
+ if artifacts[0] + ".sig" not in artifacts:
+ artifacts.insert(1, artifacts[0] + ".sig")
+ else:
+ logging.info("resigning a signed file")
+
+ for name in artifacts:
+ path = Path(self.temp.name) / name
+ self.cpio_out.addartifacttoswu(path)
+
diff --git a/swugenerator/swu_file.py b/swugenerator/swu_file.py
index d05f694..058a413 100644
--- a/swugenerator/swu_file.py
+++ b/swugenerator/swu_file.py
@@ -2,8 +2,9 @@
#
# SPDX-License-Identifier: GPLv3
#
-# This class manages to pack the SWU file
-# it has methods to add files to the archive
+# This class manages to pack the SWU file and
+# extracts a SWU file with returning the list.
+# It has methods to add files to the archive.
import os
import stat
@@ -11,21 +12,28 @@ import stat
class CPIOException(Exception):
pass
+class MagicException(Exception):
+ pass
+
+class FormatException(Exception):
+ pass
class SWUFile:
- def __init__(self, outfile):
- self.position = 0
+ def __init__(self, file):
"""
- :type outfile: FileIO of bytes
+ :type file: FileIO of bytes
"""
- self.outfile = outfile
self.position = 0
+ self.file = file
+ self.artifacts = []
+ # for reading
+ self.header = None
def _rawwrite(self, data):
"""
:type data: bytes
"""
- self.outfile.write(data)
+ self.file.write(data)
self.position += len(data)
def _align(self):
@@ -76,6 +84,7 @@ class SWUFile:
size -= len(chunk)
if size:
raise CPIOException("File was changed while reading", cpio_filename)
+ self.artifacts.append(cpio_filename)
def write_header(self, cpio_filename):
if cpio_filename != "TRAILER!!!":
@@ -112,15 +121,85 @@ class SWUFile:
if (value > 0xFFFFFFFF) or (value < 0):
raise CPIOException("STOP: value out of range", i, value)
s = f"{value:08X}"
- self.outfile.write(bytes(s, "ascii"))
- self.position += len(s)
+ self._rawwrite(bytes(s, "ascii"))
fn = bytes(base_filename, "ascii") + b"\x00"
self._rawwrite(fn)
- def close(self):
- self.write_header("TRAILER!!!")
- offset = -(self.position % -512)
- if offset:
- self._rawwrite(b"\x00" * offset)
+ def add_trailer(self):
+ try:
+ self.write_header("TRAILER!!!")
+ offset = -(self.position % -512)
+ if offset:
+ self._rawwrite(b"\x00" * offset)
+ except PermissionError:
+ # the file may have been opened for reading
+ # flush shouldn't be called
+ logging.info("flush called on read only file")
+
self.position = 0
+
+ def _extract_file(self, dir, name):
+ c_mode = int(self.header[14:22], 16)
+ c_filesize = int(self.header[54:62], 16)
+ # Read file data
+ filedata = self.file.read(c_filesize)
+ pad = (4 - c_filesize % 4) % 4
+ self.file.read(pad)
+ # Compute output path
+ path = os.path.join(dir, name)
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ # check if its really a file
+ if c_mode & 0o170000 != 0o040000:
+ with open(path, "wb") as out:
+ out.write(filedata)
+ try:
+ # preserve mode
+ os.chmod(path, c_mode & 0o7777)
+ except Exception:
+ pass
+ # check CRC
+ outcrc = self.cpiocrc(path)
+ incrc = int(self.header[102:110], 16)
+ if outcrc != incrc:
+ raise FormatException(
+ f"Wrong file crc {incrc} vs. {outcrc}"
+ )
+
+ def _extract_next(self, dir):
+ # Read fixed-size header (110 bytes for "newc" format)
+ self.header = self.file.read(110)
+ if len(self.header) < 110:
+ raise FormatException(
+ f"Unknown or unsupported header format. "
+ )
+ # Parse fields: all are ASCII hex numbers
+ c_magic = self.header[0:6].decode()
+ if c_magic != "070702":
+ raise MagicException(
+ f"Unknown or unsupported CPIO format. Our magic: {c_magic}"
+ )
+ # Parse fields (hex strings)
+ c_namesize = int(self.header[94:102], 16)
+ # Read filename (padded to 4 bytes)
+ name = self.file.read(c_namesize)
+ name = name[:-1].decode()
+ if name == "TRAILER!!!":
+ # end of archive
+ return False
+ # Align to 4 bytes
+ pad = (4 - (110 + c_namesize) % 4) % 4
+ self.file.read(pad)
+ self.artifacts.append(name)
+ self._extract_file(dir, name)
+
+ # lets continue
+ return True
+
+ def extract(self, dir):
+ # extract all files from the swu file
+ next_file = True
+ while next_file:
+ next_file = self._extract_next(dir)
+
+ return self.artifacts
--
2.47.3


Reply all
Reply to author
Forward
0 new messages