2
0
mirror of https://github.com/xcat2/xcat-dep.git synced 2026-09-30 14:55:17 +00:00
Files
xcat-dep/lib/XCAT/NativeInputs.pm
T
2026-09-21 18:23:51 -03:00

292 lines
13 KiB
Perl

package XCAT::NativeInputs;
use strict;
use warnings;
use Cwd qw(abs_path);
use Digest::SHA ();
use Exporter qw(import);
use File::Basename qw(basename);
use File::Copy qw(copy);
use File::Path qw(make_path);
use File::Temp qw(tempdir);
use JSON::PP;
our @EXPORT_OK = qw(load_inputs stage_inputs verify_input rpm_identity validate_outputs publisher_trust);
sub read_file {
my ($path) = @_;
open my $fh, '<', $path or die "Cannot read $path: $!\n";
local $/;
my $data = <$fh>;
close $fh or die "Cannot close $path: $!\n";
return $data;
}
sub sha256 {
my ($path) = @_;
open my $fh, '<', $path or die "Cannot read $path: $!\n";
binmode $fh;
my $sha = Digest::SHA->new(256)->addfile($fh)->hexdigest;
close $fh or die "Cannot close $path: $!\n";
return $sha;
}
sub capture {
my (@args) = @_;
open my $fh, '-|', @args or die "Cannot execute $args[0]: $!\n";
local $/;
my $out = <$fh> // '';
close $fh or die "Command failed: @args\n";
$out =~ s/\s+\z//;
return $out;
}
sub run {
my (@args) = @_;
system(@args) == 0 or die "Command failed: @args\n";
}
sub pinned_file {
my ($root, $entry) = @_;
die "Invalid pinned path\n" unless ($entry->{path} // '') =~ m{\A[\w./-]+\z}
&& $entry->{path} !~ m{(?:\A|/)\.\.(?:/|\z)|\A/};
die "Missing input $entry->{path}\n" unless -f "$root/$entry->{path}";
my $path = abs_path("$root/$entry->{path}") // die "Missing input $entry->{path}\n";
die "Input escapes repository: $entry->{path}\n" unless index($path, "$root/") == 0;
die "Input SHA256 mismatch: $entry->{path}\n" unless sha256($path) eq ($entry->{sha256} // '');
return $path;
}
sub load_inputs {
my ($root, $required) = @_;
$root = abs_path($root) // die "Missing repository\n";
my $catalog_path = "$root/openeuler/24.03-ppc64le.inputs.json";
my $catalog = JSON::PP->new->decode(read_file($catalog_path));
die "Unsupported native input catalog\n" unless ($catalog->{version} // 0) == 1
&& ($catalog->{target} // '') eq 'openeuler-24.03-ppc64le';
my $key = $catalog->{publisher_key};
die "Invalid publisher fingerprint\n" unless ($key->{fingerprint} // '') =~ /\A[0-9A-F]{40}\z/;
my $key_path = pinned_file($root, $key);
my (%nodes, %outputs);
for my $node (@{$catalog->{inputs}}) {
my $name = $node->{name} // '';
die "Invalid native input name '$name'\n" unless $name =~ /\A[\w+.-]+\z/;
die "Duplicate native input '$name'\n" if $nodes{$name};
my $type = $node->{type} // '';
die "Invalid native input type '$type'\n" unless $type =~ /\A(?:srpm|publisher|owner)\z/;
if ($type eq 'owner') {
die "Unsupported native build owner '$name'\n" unless grep { $_ eq $name }
qw(goconserver grub2-xcat ipmitool-xcat syslinux-xcat xnba-undi xCAT-genesis-base
perl-Crypt-Rijndael perl-Crypt-SSLeay perl-HTTP-Async perl-IO-Stty perl-Net-HTTPS-NB perl-Net-Telnet);
}
if ($type ne 'publisher') {
my $uid = $type eq 'owner' && ($name eq 'xnba-undi' || $name eq 'xCAT-genesis-base') ? 0 : 1000;
die "Invalid native build UID for $name\n" unless ($node->{build_uid} // -1) == $uid;
}
if ($type ne 'owner') {
die "Invalid pinned native URL for $name\n" unless ($node->{url} // '') =~
m{\Ahttps://repo\.openeuler\.org/openEuler-24\.03-LTS(?:-SP3)?/[A-Za-z0-9_./+-]+\.rpm\z};
die "Invalid native SHA256 for $name\n" unless ($node->{sha256} // '') =~ /\A[0-9a-f]{64}\z/;
die "Publisher binary must be exact GA: $name\n" if $type eq 'publisher'
&& $node->{url} !~ m{/openEuler-24\.03-LTS/.*\.noarch\.rpm\z};
}
die "Missing output ownership for $name\n" unless ref($node->{outputs}) eq 'ARRAY' && @{$node->{outputs}};
for my $output (@{$node->{outputs}}) {
die "Invalid native output name\n" unless $output =~ /\A[\w+.-]+\z/;
die "Conflicting output ownership: $output\n" if $outputs{$output};
$outputs{$output} = $name;
}
die "Invalid publisher outputs for $name\n" if $type eq 'publisher'
&& (@{$node->{outputs}} != 1 || $node->{outputs}[0] ne $name);
for my $patch (@{$node->{patches} // []}) {
die "Only source inputs accept patches\n" unless $type eq 'srpm';
$patch->{absolute_path} = pinned_file($root, $patch);
}
for my $define (@{$node->{defines} // []}) {
die "Invalid native spec definition for $name\n" unless $type eq 'srpm'
&& $define =~ /\A(?:llvmjit|external_libpq|runselftest|test) [01]\z/;
}
$nodes{$name} = $node;
}
for my $name (sort keys %nodes) {
my $type = $nodes{$name}{type};
for my $dependency (@{$nodes{$name}{needs} // []}) {
die "Missing native dependency '$dependency'\n" unless $nodes{$dependency};
die "Unsupported native execution edge: $name -> $dependency\n"
if $type eq 'publisher' || $nodes{$dependency}{type} eq 'owner';
}
}
my (%mark, @order);
my $visit;
$visit = sub {
my ($name) = @_;
die "Missing native dependency '$name'\n" unless $nodes{$name};
die "Cyclic native dependency at '$name'\n" if ($mark{$name} // '') eq 'visiting';
return if $mark{$name};
$mark{$name} = 'visiting';
$visit->($_) for @{$nodes{$name}{needs} // []};
$mark{$name} = 'done';
push @order, $name;
};
$visit->($_) for sort keys %nodes;
my %selected;
my $select;
$select = sub {
my ($name) = @_;
die "Missing native dependency '$name'\n" unless $nodes{$name};
return if $selected{$name}++;
$select->($_) for @{$nodes{$name}{needs} // []};
};
for my $output (sort keys %$required) {
my $owner = $outputs{$output} // ($nodes{$output} ? $output : undef);
die "No native output owner for '$output'\n" unless $owner;
$select->($owner);
}
$select->($_) for @{$catalog->{build_inputs} // []};
return {catalog => $catalog, nodes => \%nodes, outputs => \%outputs,
order => [grep { $selected{$_} } @order], selected => \%selected,
publisher_key => $key_path, publisher_fingerprint => $key->{fingerprint},
catalog_sha256 => sha256($catalog_path)};
}
sub rpm_identity {
my ($path) = @_;
my @fields = split /\n/, capture('rpm', '-qp', '--qf',
'%{NAME}\n%{ARCH}\n%{SOURCEPACKAGE}\n%{RELEASE}\n', $path);
die "Invalid RPM header: $path\n" unless @fields == 4;
return {name => $fields[0], arch => $fields[1], source => $fields[2] eq '1', release => $fields[3]};
}
sub read_exact {
my ($fh, $size) = @_;
my $data = '';
while (length($data) < $size) {
my $got = read($fh, $data, $size - length($data), length($data));
die "Truncated RPM payload\n" unless defined($got) && $got > 0;
}
return $data;
}
sub reject_elf_payload {
my ($path) = @_;
open my $fh, '-|', 'rpm2cpio', $path or die "Cannot read RPM payload: $!\n";
binmode $fh;
my $error;
eval {
while (1) {
my $header = read_exact($fh, 110);
die "Invalid RPM cpio header\n" unless $header =~ /\A07070[12][0-9A-Fa-f]{104}\z/;
my @fields = map { hex($_) } $header =~ /\A.{6}(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})(.{8})\z/s;
my ($size, $namesize) = @fields[6, 11];
die "Invalid RPM cpio filename\n" unless $namesize > 0 && $namesize <= 1048576;
my $name = read_exact($fh, $namesize);
die "Invalid RPM cpio filename terminator\n" unless $name =~ s/\0\z//;
read_exact($fh, (4 - (110 + $namesize) % 4) % 4);
last if $name eq 'TRAILER!!!' && $size == 0;
my $prefix = read_exact($fh, $size < 4 ? $size : 4);
die "ELF payload in publisher noarch RPM: $name\n" if $prefix eq "\x7fELF";
$size -= length($prefix);
while ($size) { my $count = $size < 65536 ? $size : 65536; read_exact($fh, $count); $size -= $count; }
read_exact($fh, (4 - $fields[6] % 4) % 4);
}
my $tail;
while (read($fh, $tail, 65536)) { die "Unexpected data after RPM cpio trailer\n" if $tail =~ /[^\0]/; }
1;
} or $error = $@;
my $closed = close($fh);
die $error if $error;
die "rpm2cpio failed: $path\n" unless $closed;
}
sub verify_input {
my ($plan, $node, $path, $db) = @_;
die "Native input SHA256 mismatch: $path\n" unless sha256($path) eq $node->{sha256};
my $out = capture('rpmkeys', '--dbpath', $db, '--checksig', '--verbose', $path);
die "Publisher signature missing or invalid: $path\n" unless $out =~ /Signature.*: OK/i
&& $out !~ /NOKEY|NOT OK|BAD|UNSIGNED/i;
my $id = rpm_identity($path);
die "Native input NAME mismatch: $path\n" unless $id->{name} eq $node->{name};
if ($node->{type} eq 'publisher') {
die "Publisher input is not a noarch binary: $path\n" if $id->{source} || $id->{arch} ne 'noarch';
die "Publisher input is not exact GA: $path\n" unless $id->{release} =~ /\.oe2403\z/;
reject_elf_payload($path);
} else {
die "Native input is not a source RPM: $path\n" unless $id->{source};
}
die "Native input changed during verification: $path\n" unless sha256($path) eq $node->{sha256};
return $id;
}
sub publisher_trust {
my ($plan, $work) = @_;
make_path("$work/trust", "$work/gnupg");
chmod 0700, "$work/gnupg";
my $listing = capture('gpg', '--homedir', "$work/gnupg", '--batch', '--with-colons', '--show-keys', $plan->{publisher_key});
my @primary;
my $pub;
for my $line (split /\n/, $listing) {
my @fields = split /:/, $line;
$pub = 1 if $fields[0] eq 'pub';
if ($pub && $fields[0] eq 'fpr') { push @primary, $fields[9]; $pub = 0; }
}
die "Publisher public key fingerprint mismatch\n" unless @primary == 1 && $primary[0] eq $plan->{publisher_fingerprint};
run('rpmkeys', '--dbpath', "$work/trust", '--import', $plan->{publisher_key});
return "$work/trust";
}
sub stage_inputs {
my ($plan, $work) = @_;
die "Native input staging already exists: $work\n" if -e $work;
make_path($work);
my $db = publisher_trust($plan, $work);
my @ledger;
for my $name (@{$plan->{order}}) {
my $node = $plan->{nodes}{$name};
next if $node->{type} eq 'owner';
make_path("$work/$name");
for my $patch (@{$node->{patches} // []}) {
make_path("$work/$name/patches");
my $staged = "$work/$name/patches/" . basename($patch->{path});
die "Conflicting staged patch: $staged\n" if -e $staged;
copy($patch->{absolute_path}, $staged) or die "Cannot stage native patch: $!\n";
die "Staged patch SHA256 mismatch: $staged\n" unless sha256($staged) eq $patch->{sha256};
chmod 0444, $staged or die "Cannot protect native patch: $!\n";
$patch->{staged} = $staged;
}
my $dest = "$work/$name/" . basename($node->{url});
run('wget', '--https-only', '--tries=3', '--timeout=60', '-O', "$dest.part", $node->{url});
verify_input($plan, $node, "$dest.part", $db);
rename("$dest.part", $dest) or die "Cannot preserve native input: $!\n";
chmod 0444, $dest or die "Cannot protect native input: $!\n";
$node->{staged} = $dest;
push @ledger, {name => $name, type => $node->{type}, url => $node->{url},
sha256 => $node->{sha256}, path => $dest, publisher => $plan->{publisher_fingerprint},
defines => $node->{defines} // [],
patches => [map { {path => $_->{path}, sha256 => $_->{sha256}, staged => $_->{staged}} } @{$node->{patches} // []}]};
}
open my $fh, '>', "$work/inputs.json" or die "Cannot record native input ledger: $!\n";
print {$fh} JSON::PP->new->canonical->pretty->encode({catalog_sha256 => $plan->{catalog_sha256}, inputs => \@ledger});
close $fh or die "Cannot close native input ledger: $!\n";
$plan->{trust_db} = $db;
}
sub validate_outputs {
my ($node, $paths, $require_all) = @_;
my %allowed = map { $_ => 1 } @{$node->{outputs}};
my %seen;
for my $path (@$paths) {
my $id = rpm_identity($path);
next if $id->{source};
die "Unexpected output from $node->{name}: $id->{name}\n" unless $allowed{$id->{name}};
die "Duplicate output from $node->{name}: $id->{name}\n" if $seen{$id->{name}}++;
die "Foreign output architecture: $id->{arch}\n" unless $id->{arch} eq 'ppc64le' || $id->{arch} eq 'noarch';
}
if ($require_all) {
die "Missing output from $node->{name}: $_\n" for grep { !$seen{$_} } sort keys %allowed;
}
return \%seen;
}
1;