Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions otamaker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ options:
-C DIR, --directory DIR
the directory payload paths are relative to (default:
the current directory)
-w, --writesum also write the sha256sum to <outfile>.sha256sum (and
for a transitional artefact, the manifest hash to
<outfile>.manifest.sha256sum)
-w, --writesum also write the manifest hash to
<outfile>.manifest.sha256sum, and (for a legacy or
transitional artefact) the hash of the whole artefact
to <outfile>.sha256sum
-t, --transitional build a transitional artefact, with a manifest and a
legacy contents file for older applicators (the
default)
Expand Down Expand Up @@ -138,9 +139,9 @@ otamaker can build three kinds of artefact, chosen with one of `-t`, `-m` and `-

With `-m` or `-t`, otamaker adds `size` and `sha256` fields to each payload in the manifest. Payloads larger than the chunk size (`-c`, default 4M, up to 64M) are instead hashed one chunk at a time, with the per-chunk digests in an extra `<name>.hashes` payload placed before them, so that the applicator can check a large image as it is written. The manifest gets a `format` field, currently always 2; a `format` in the contents file asks for at least that version, and `-f` (`--force`) allows an unsupported one, e.g. to test applicators. Don't put `size`, `sha256` or `chunk_size` in the contents file yourself.

`-w` (`--writesum`) writes the published hash to `<outfile>.sha256sum`. For a transitional artefact it writes the whole-archive hash there, and the manifest hash to `<outfile>.manifest.sha256sum`.
`-w` (`--writesum`) writes the whole-archive hash to `<outfile>.sha256sum` in legacy and transitional modes, and the manifest hash to `<outfile>.manifest.sha256sum` in manifest and transitional modes.

Note that the manifest hash doesn't change between builds from the same contents and payloads, unlike the whole-archive hash, which depends on timestamps.
Note that the manifest hash doesn't change between builds from the same contents and payloads, unlike the whole-archive hash, which depends on timestamps. However, if SOURCE_DATE_EPOCH is set in the environment, the archive members are given that mtime, resulting in consistent whole-archive hashes when run by the same user provided the payloads don't change.

**Signing**

Expand Down
54 changes: 46 additions & 8 deletions otamaker/otamaker
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Usage: otamaker [<options>] <contents file> [<outfile>]
Each payload is read from the file named by its 'source:' key, or by a comment
on its 'name:' line (the older form), or failing both by its name. Paths are
relative to the current directory, or to -C <dir>. 'source:'
is dropped from the copy of the contents in the artefact.
is dropped from the copy of the contents in the artefact. If SOURCE_DATE_EPOCH
is set in the environment, the archive members are given that mtime.

-t builds a transitional artefact, which applicators can install either as a
manifest (format 2) artefact or, if they predate manifests, as a legacy
Expand Down Expand Up @@ -99,6 +100,16 @@ PAYLOAD_TYPES = {
# individually-compressed payload (e.g. 'boot.sparse.zst' instead of
# 'boot.sparse') is almost always a mistake.
COMPRESSED_SUFFIXES = ('.zst', '.gz', '.bz2', '.xz', '.lz4', '.tgz', '.tzst')
MAX_NAME_LEN = 100 # The tar header name field, as the applicator limits it

def is_simple_name(name):
"""A payload name identifies the archive member (and perhaps a slot), so
must be a simple name rather than a path: no '/', not '.' or '..', no
control characters, and short enough for a tar header. The file it is
built from ('source:', a comment, or failing both the name) is separate."""
if not name or len(name) > MAX_NAME_LEN or name in ('.', '..'):
return False
return not any(c == '/' or ord(c) < 0x20 or ord(c) == 0x7f for c in name)

def parse_size(text):
m = re.match(r'^(\d+)([KkMm]?)$', text)
Expand Down Expand Up @@ -197,6 +208,9 @@ def build_manifest(contents, payloads_idx, groups, tmpdir, chunk_size, manifest_
if hashlist in names:
print(f'* error: hashlist name {hashlist} clashes with a payload')
sys.exit(1)
if len(hashlist) > MAX_NAME_LEN:
print(f'* error: hashlist name {hashlist} is too long')
sys.exit(1)
digests = chunk_digests(path, chunk_size)
with open(os.path.join(tmpdir, hashlist), 'wb') as fh:
fh.write(digests)
Expand Down Expand Up @@ -256,8 +270,9 @@ def main():
'(default: the current directory)',
metavar='DIR')
parser.add_argument('-w', '--writesum',
help='also write the sha256sum to <outfile>.sha256sum (and for a transitional '
'artefact, the manifest hash to <outfile>.manifest.sha256sum)',
help='also write the manifest hash to <outfile>.manifest.sha256sum, and (for '
'a legacy or transitional artefact) the hash of the whole artefact to '
'<outfile>.sha256sum',
action='store_true')
kind = parser.add_mutually_exclusive_group()
kind.add_argument('-t', '--transitional',
Expand Down Expand Up @@ -401,12 +416,22 @@ def main():
groups[-1]['name'] = name_m.group(1).strip()
groups[-1]['comment'] = comment

seen_names = set()
for g in groups:
name = g['name']
if name is None:
print("* error: payload has no name")
errors += 1
continue
if not is_simple_name(name):
print(f"* error: payload name '{name}' is not a simple name (no '/', '.' or '..')")
errors += 1
continue
if name in seen_names:
print(f"* error: duplicate payload name '{name}'")
errors += 1
continue
seen_names.add(name)
if g['source'] is not None:
target = g['source']
elif g['comment'] is not None:
Expand Down Expand Up @@ -558,7 +583,16 @@ def main():

tar_cmd = [
'tar', '--zstd', '-h', '--hard-dereference', '-cvf', outfile, '-C', tmpdir
] + payloads
]
# Honour the reproducible-builds convention: with SOURCE_DATE_EPOCH set,
# every member is given that mtime instead of the time it was copied
sde = os.environ.get('SOURCE_DATE_EPOCH')
if sde is not None:
if not re.fullmatch(r'\d+', sde.strip()):
print(f"* error: SOURCE_DATE_EPOCH '{sde}' is not an integer")
sys.exit(1)
tar_cmd.append(f'--mtime=@{sde.strip()}')
tar_cmd += payloads
r = subprocess.run(tar_cmd, capture_output=True)
if r.returncode != 0:
sys.exit(r.returncode)
Expand All @@ -569,13 +603,17 @@ def main():
sum = file_sha256(contents_path if args.manifest else outfile)
legacy_sum = file_sha256(outfile) if args.transitional else None
if args.writesum:
if legacy_sum:
# The manifest hash always goes to <outfile>.manifest.sha256sum,
# whichever kind of artefact it is in, and the hash of the whole
# artefact to <outfile>.sha256sum.
if args.legacy:
with open(f'{outfile}.sha256sum', 'w') as sf:
print(legacy_sum, file=sf)
with open(f'{outfile}.manifest.sha256sum', 'w') as sf:
print(sum, file=sf)
else:
with open(f'{outfile}.sha256sum', 'w') as sf:
if legacy_sum:
with open(f'{outfile}.sha256sum', 'w') as sf:
print(legacy_sum, file=sf)
with open(f'{outfile}.manifest.sha256sum', 'w') as sf:
print(sum, file=sf)

print('Contents:')
Expand Down
Loading