stages: add new stage org.osbuild.hmac
The new org.osbuild.hmac stage can be used to calculate hmac digests to be stored alongside files for verification.
This commit is contained in:
parent
7b843dc83e
commit
5b77ff6f65
3 changed files with 170 additions and 0 deletions
34
stages/org.osbuild.hmac
Executable file
34
stages/org.osbuild.hmac
Executable file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import osbuild.api
|
||||||
|
|
||||||
|
|
||||||
|
def hmac_filepath(path):
|
||||||
|
directory = os.path.dirname(path)
|
||||||
|
basename = os.path.basename(path)
|
||||||
|
return os.path.join(directory, f".{basename}.hmac")
|
||||||
|
|
||||||
|
|
||||||
|
def main(tree, options):
|
||||||
|
paths = options["paths"]
|
||||||
|
algorithm = options["algorithm"]
|
||||||
|
|
||||||
|
hmac_cmd = f"{algorithm}hmac"
|
||||||
|
for path in paths:
|
||||||
|
real_path = os.path.join(tree, path.lstrip("/"))
|
||||||
|
with open(hmac_filepath(real_path), "w", encoding="utf-8") as hmac_file:
|
||||||
|
# run from the directory of the target file to create the hmac file with just the basename
|
||||||
|
cwd = os.path.dirname(real_path)
|
||||||
|
base = os.path.basename(real_path)
|
||||||
|
subprocess.run([hmac_cmd, base], cwd=cwd, encoding="utf-8", stdout=hmac_file, check=True)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
args = osbuild.api.arguments()
|
||||||
|
r = main(args["tree"], args["options"])
|
||||||
|
sys.exit(r)
|
||||||
45
stages/org.osbuild.hmac.meta.json
Normal file
45
stages/org.osbuild.hmac.meta.json
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"summary": "Generates .hmac checksum files",
|
||||||
|
"description": [
|
||||||
|
"Generates HMAC values for given files and stores them alongside the original",
|
||||||
|
"for later verification.",
|
||||||
|
"The stage uses the <algorithm>hmac commands (e.g. sha512hmac) which use a",
|
||||||
|
"built-in key when none is provided. Future extensions of this stage may",
|
||||||
|
"add a key parameter if and when it becomes a requirement. ",
|
||||||
|
"In its current state, the stage can be used to replicate the generation of",
|
||||||
|
"kernel hmac files [1] which are used for integrity verification when booting",
|
||||||
|
"in FIPS mode.",
|
||||||
|
"Notes:",
|
||||||
|
" - Requires hmac calc in the build root (libkcapi-hmaccalc)",
|
||||||
|
"Links:",
|
||||||
|
"[1] https://gitlab.com/redhat/centos-stream/rpms/kernel/-/blob/f5b2a5f2ae8040c6072382545d302a4a936cb53c/kernel.spec?page=3#L2370"
|
||||||
|
],
|
||||||
|
"schema_2": {
|
||||||
|
"options": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"paths",
|
||||||
|
"algorithm"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"paths": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"algorithm": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"sha1",
|
||||||
|
"sha224",
|
||||||
|
"sha256",
|
||||||
|
"sha384",
|
||||||
|
"sha512"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
stages/test/test_hmac.py
Normal file
91
stages/test/test_hmac.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from osbuild import testutil
|
||||||
|
|
||||||
|
STAGE_NAME = "org.osbuild.hmac"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("test_data,expected_errs", [
|
||||||
|
# bad
|
||||||
|
({}, [r"'paths' is a required property", r"'algorithm' is a required property"]),
|
||||||
|
({"algorithm": "sha512"}, [r"'paths' is a required property"]),
|
||||||
|
({"paths": ["/somefile"]}, [r"'algorithm' is a required property"]),
|
||||||
|
({"paths": [], "algorithm": "sha1"}, [r"(\[\] is too short|\[\] should be non-empty)"]),
|
||||||
|
|
||||||
|
({"paths": ["/somefiles"], "algorithm": "md5"},
|
||||||
|
[r"'md5' is not one of \['sha1', 'sha224', 'sha256', 'sha384', 'sha512'\]"]),
|
||||||
|
|
||||||
|
# good
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"paths": [
|
||||||
|
"/greet1.txt",
|
||||||
|
"/greet2.txt"
|
||||||
|
],
|
||||||
|
"algorithm": "sha256"
|
||||||
|
},
|
||||||
|
""
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
"paths": [
|
||||||
|
"/path/to/some/file",
|
||||||
|
"/boot/efi/EFI/Linux/vmlinuz-linux"
|
||||||
|
],
|
||||||
|
"algorithm": "sha512"
|
||||||
|
},
|
||||||
|
""
|
||||||
|
),
|
||||||
|
])
|
||||||
|
def test_schema_validation_hmac(stage_schema, test_data, expected_errs):
|
||||||
|
test_input = {
|
||||||
|
"type": STAGE_NAME,
|
||||||
|
"options": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test_input["options"].update(test_data)
|
||||||
|
res = stage_schema.validate(test_input)
|
||||||
|
|
||||||
|
if not expected_errs:
|
||||||
|
assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}"
|
||||||
|
else:
|
||||||
|
assert res.valid is False
|
||||||
|
for exp_err in expected_errs:
|
||||||
|
testutil.assert_jsonschema_error_contains(res, re.compile(exp_err), expected_num_errs=len(expected_errs))
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("subprocess.run")
|
||||||
|
@pytest.mark.parametrize("options", [
|
||||||
|
({"paths": ["/a/file/path"], "algorithm": "sha512"}),
|
||||||
|
({"paths": ["/b/file/path"], "algorithm": "sha512"}),
|
||||||
|
({"paths": ["/a/file/path", "/b/file/path"], "algorithm": "sha256"}),
|
||||||
|
])
|
||||||
|
def test_hmac_cmdline(mock_run, tmp_path, stage_module, options):
|
||||||
|
algorithm = options["algorithm"]
|
||||||
|
|
||||||
|
expected_cmds = []
|
||||||
|
expected_wds = []
|
||||||
|
hmac_paths = []
|
||||||
|
for path in options["paths"]:
|
||||||
|
# create parent directories because the stage opens a file to write the output to it even when mocking sp.run()
|
||||||
|
basename = os.path.basename(path)
|
||||||
|
real_path = os.path.join(tmp_path, path.lstrip("/"))
|
||||||
|
parent = os.path.dirname(real_path)
|
||||||
|
hmac_paths.append(os.path.join(parent, f".{basename}.hmac"))
|
||||||
|
os.makedirs(parent, exist_ok=True)
|
||||||
|
expected_cmds.append([f"{algorithm}hmac", basename])
|
||||||
|
expected_wds.append(parent)
|
||||||
|
|
||||||
|
stage_module.main(tmp_path, options)
|
||||||
|
for exp, cwd, actual in zip(expected_cmds, expected_wds, mock_run.call_args_list):
|
||||||
|
assert exp == actual[0][0]
|
||||||
|
assert cwd == actual[1]["cwd"]
|
||||||
|
|
||||||
|
# the file should have been created by the open() call, but should be empty because we mocked sp.run()
|
||||||
|
for path in hmac_paths:
|
||||||
|
info = os.stat(path)
|
||||||
|
assert info.st_size == 0
|
||||||
Loading…
Add table
Add a link
Reference in a new issue