2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-08-26 16:46:41 +00:00

test(genesis): cover signed extensions

This commit is contained in:
Vinícius Ferrão
2026-08-20 13:13:12 -03:00
parent 4b3b917409
commit f6acf56ef2
@@ -0,0 +1,234 @@
#!/usr/bin/env perl
use strict;
use warnings;
use File::Copy qw(copy);
use File::Path qw(make_path);
use File::Spec;
use File::Temp qw(tempdir);
use FindBin;
use JSON::PP;
use Test::More;
my $repo_root = File::Spec->catdir( $FindBin::Bin, '..', '..' );
my $loader = File::Spec->catfile(
$repo_root,
qw(xCAT-genesis-builder oe meta-xcat-genesis recipes-core xcat-genesis-extensions files genesis-sysext)
);
my $signer = File::Spec->catfile(
$repo_root, qw(xCAT-genesis-builder oe scripts sign-extension)
);
sub write_file {
my ( $path, $contents, $mode ) = @_;
open( my $fh, '>', $path ) or die "Unable to write $path: $!";
print {$fh} $contents;
close($fh);
chmod( $mode, $path ) if defined($mode);
}
sub shell_quote {
my ($value) = @_;
$value =~ s/'/'"'"'/g;
return "'$value'";
}
sub run_command {
my (@command) = @_;
my $shell_command = join( ' ', map { shell_quote($_) } @command );
my $output = qx{$shell_command 2>&1};
return ( $? >> 8, $output );
}
sub write_manifest {
my ( $path, $hash, $changes ) = @_;
my $manifest = {
architecture => 'x86_64',
capabilities => ['diagnostic.smoke'],
genesis_release => '0.1',
kernel_modules => JSON::PP::false,
kernel_release => undef,
key_id => 'xcat-release',
license_class => 'open',
name => 'xcat-smoke',
pci_ids => [],
schema => 1,
sha256 => $hash,
version => '1.0',
};
@{$manifest}{ keys %{$changes} } = values %{$changes};
write_file( $path, JSON::PP->new->canonical->pretty->encode($manifest) );
}
my $root = tempdir( CLEANUP => 1 );
my $bin = File::Spec->catdir( $root, 'bin' );
my $keys = File::Spec->catdir( $root, 'keys' );
my $run_dir = File::Spec->catdir( $root, 'run' );
my $image = File::Spec->catfile( $root, 'xcat-smoke.raw' );
my $manifest = File::Spec->catfile( $root, 'xcat-smoke.manifest.json' );
my $signature = File::Spec->catfile( $root, 'xcat-smoke.sig' );
my $private_key = File::Spec->catfile( $root, 'private.pem' );
my $public_key = File::Spec->catfile( $keys, 'xcat-release.pem' );
my $os_release = File::Spec->catfile( $root, 'os-release' );
my $command_log = File::Spec->catfile( $root, 'commands.log' );
my $status_log = File::Spec->catfile( $root, 'status.log' );
make_path( $bin, $keys, $run_dir );
write_file( $image, "extension payload\n" );
write_file( $os_release, "ID=xcat-genesis\nVERSION_ID=0.1\n" );
write_file( $command_log, '' );
write_file( $status_log, '' );
write_file(
File::Spec->catfile( $bin, 'systemd-sysext' ),
"#!/bin/sh\nprintf 'systemd-sysext %s\\n' \"\$*\" >>\"\$XCAT_TEST_LOG\"\n",
0755
);
write_file(
File::Spec->catfile( $bin, 'genesis-status' ),
"#!/bin/sh\nprintf '%s\\n' \"\$*\" >>\"\$XCAT_STATUS_LOG\"\n",
0755
);
is( system( 'openssl', 'genpkey', '-algorithm', 'ED25519', '-out', $private_key ) >> 8,
0, 'test private key is generated' );
is( system( 'openssl', 'pkey', '-in', $private_key, '-pubout', '-out', $public_key ) >> 8,
0, 'test public key is generated' );
my ( $hash_status, $hash_output ) = run_command( 'sha256sum', '--', $image );
is( $hash_status, 0, 'extension digest is generated' );
my ($hash) = $hash_output =~ /^([0-9a-f]{64})/;
write_manifest( $manifest, $hash, {} );
my ( $sign_status, $sign_output ) =
run_command( $signer, $manifest, $private_key, $signature );
is( $sign_status, 0, 'extension manifest is signed' ) or diag($sign_output);
local %ENV = (
%ENV,
PATH => "$bin:$ENV{PATH}",
XCAT_GENESIS_EXTENSION_KEY_DIR => $keys,
XCAT_GENESIS_EXTENSION_RUN_DIR => $run_dir,
XCAT_GENESIS_OS_RELEASE => $os_release,
XCAT_GENESIS_STATUS_COMMAND => File::Spec->catfile( $bin, 'genesis-status' ),
XCAT_GENESIS_UNAME_M => 'x86_64',
XCAT_GENESIS_UNAME_R => '6.18.24-test',
XCAT_TEST_LOG => $command_log,
XCAT_STATUS_LOG => $status_log,
);
my ( $verify_status, $verify_output ) =
run_command( '/bin/bash', $loader, 'verify', $manifest, $image, $signature );
is( $verify_status, 0, 'valid extension is accepted' ) or diag($verify_output);
{
local $ENV{XCAT_GENESIS_UNAME_M} = 'i586';
my $x86_manifest = File::Spec->catfile( $root, 'xcat-smoke-x86.json' );
my $x86_signature = File::Spec->catfile( $root, 'xcat-smoke-x86.sig' );
write_manifest( $x86_manifest, $hash, { architecture => 'x86' } );
my ( $x86_sign_status, $x86_sign_output ) =
run_command( $signer, $x86_manifest, $private_key, $x86_signature );
is( $x86_sign_status, 0, 'x86 extension manifest is signed' )
or diag($x86_sign_output);
my ( $x86_status, $x86_output ) = run_command(
'/bin/bash', $loader, 'verify', $x86_manifest, $image, $x86_signature
);
is( $x86_status, 0, 'i586 runtime uses the x86 extension identity' )
or diag($x86_output);
}
{
local $ENV{XCAT_GENESIS_UNAME_M} = 'armv7l';
my $arm_manifest = File::Spec->catfile( $root, 'xcat-smoke-armv7hf.json' );
my $arm_signature = File::Spec->catfile( $root, 'xcat-smoke-armv7hf.sig' );
write_manifest( $arm_manifest, $hash, { architecture => 'armv7hf' } );
my ( $arm_sign_status, $arm_sign_output ) =
run_command( $signer, $arm_manifest, $private_key, $arm_signature );
is( $arm_sign_status, 0, 'armv7hf extension manifest is signed' )
or diag($arm_sign_output);
my ( $arm_status, $arm_output ) = run_command(
'/bin/bash', $loader, 'verify', $arm_manifest, $image, $arm_signature
);
is( $arm_status, 0, 'armv7l runtime uses the armv7hf extension identity' )
or diag($arm_output);
}
my ( $install_status, $install_output ) =
run_command( '/bin/bash', $loader, 'install', $manifest, $image, $signature );
is( $install_status, 0, 'valid extension is installed' ) or diag($install_output);
ok( -f File::Spec->catfile( $run_dir, 'xcat-smoke.raw' ),
'installed image uses the manifest name' );
open( my $log_fh, '<', $command_log ) or die "Unable to read $command_log: $!";
my $log = do { local $/; <$log_fh> };
close($log_fh);
like( $log, qr/^systemd-sysext refresh$/m,
'installation refreshes system extensions' );
my ( $load_status, $load_output ) =
run_command( '/bin/bash', $loader, 'load-all', $root );
is( $load_status, 0, 'valid extension directory is loaded' )
or diag($load_output);
open( my $status_fh, '<', $status_log )
or die "Unable to read $status_log: $!";
my $status_events = do { local $/; <$status_fh> };
close($status_fh);
like( $status_events,
qr/^extensions RUNNING Verifying Genesis extensions$/m,
'extension loading publishes its active state' );
like( $status_events,
qr/^extensions READY Genesis extensions loaded$/m,
'extension loading publishes its ready state' );
my $bad_signature = File::Spec->catfile( $root, 'bad.sig' );
write_file( $bad_signature, 'x' x 64 );
my ( $bad_signature_status, undef ) =
run_command( '/bin/bash', $loader, 'verify', $manifest, $image, $bad_signature );
isnt( $bad_signature_status, 0, 'invalid signature is rejected' );
for my $case (
[ architecture => 'riscv64', 'wrong architecture is rejected' ],
[ genesis_release => '9.9', 'wrong release is rejected' ],
[ sha256 => ( '0' x 64 ), 'wrong digest is rejected' ],
[ kernel_modules => JSON::PP::true, 'wrong kernel ABI is rejected',
kernel_release => '0.0-wrong' ],
)
{
my ( $field, $value, $label, @extra ) = @{$case};
my %changes = ( $field => $value, @extra );
my $case_manifest = File::Spec->catfile( $root, "$field.json" );
my $case_signature = File::Spec->catfile( $root, "$field.sig" );
write_manifest( $case_manifest, $hash, \%changes );
my ( $case_sign_status, $case_sign_output ) =
run_command( $signer, $case_manifest, $private_key, $case_signature );
is( $case_sign_status, 0, "$field manifest is signed" )
or diag($case_sign_output);
my ( $case_status, undef ) =
run_command( '/bin/bash', $loader, 'verify', $case_manifest, $image,
$case_signature );
isnt( $case_status, 0, $label );
}
my $linked_manifest = File::Spec->catfile( $root, 'linked.json' );
symlink( $manifest, $linked_manifest ) or die "Unable to link $linked_manifest: $!";
my ( $linked_status, undef ) =
run_command( '/bin/bash', $loader, 'verify', $linked_manifest, $image,
$signature );
isnt( $linked_status, 0, 'linked manifest is rejected' );
my $empty_dir = File::Spec->catdir( $root, 'empty' );
make_path($empty_dir);
write_file( $status_log, '' );
my ( $empty_status, undef ) =
run_command( '/bin/bash', $loader, 'load-all', $empty_dir );
isnt( $empty_status, 0, 'empty extension directory is rejected' );
open( $status_fh, '<', $status_log )
or die "Unable to read $status_log: $!";
$status_events = do { local $/; <$status_fh> };
close($status_fh);
like( $status_events,
qr/^extensions FAILED no extension manifests found in:/m,
'extension loading publishes verification failures' );
like( $status_events,
qr/CODE=EXTENSION_VERIFICATION_FAILED .*RECOVERY=Check extension images, manifests, signatures, and trusted keys/m,
'extension failures include structured recovery data' );
done_testing();