2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-07-31 18:19:40 +00:00

Merge pull request #7586 from VersatusHPC/fix/issue-7512-ipmi-rakp2

fix(xcat-server): retry suite 3 on zero RAKP2 HMAC
This commit is contained in:
Daniel Hilst
2026-07-17 13:26:00 -03:00
committed by GitHub
3 changed files with 339 additions and 6 deletions
+30 -6
View File
@@ -260,6 +260,9 @@ sub login {
$self->{onlogon} = $args{callback};
$self->{onlogon_args} = $args{callback_args};
$self->{logontries} = 5;
if (delete $self->{zero_rakp2_fallback}) {
$self->{attempthash} = 256;
}
$self->get_channel_auth_cap();
}
@@ -446,6 +449,17 @@ sub got_channel_auth_cap {
}
sub retry_rmcpplus_after_zero_rakp2 {
my $self = shift;
return 0 if ($self->{attempthash} != 256 or $self->{zero_rakp2_fallback});
$self->{zero_rakp2_fallback} = 1;
$self->{attempthash} = 1;
$self->{sessionestablishmentcontext} = 0;
$self->open_rmcpplus_request();
return 1;
}
sub open_rmcpplus_request {
my $self = shift;
$self->{'authtype'} = 6;
@@ -850,7 +864,7 @@ sub send_rakp1 {
sub init {
my $self = shift;
$self->{attempthash} = 256;
$self->{attempthash} = $self->{zero_rakp2_fallback} ? 1 : 256;
$self->{confalgo} = undef;
$self->{integrityalgo} = undef;
$self->{sessionestablishmentcontext} = 0;
@@ -964,12 +978,12 @@ sub got_rakp2 {
#the reason being that if an old rakp1 retry actually made it and we were just too aggressive, then a previous rakp2 is invalidated and invalid session id or the integrity check value is bad
return 9; #now's not the time for this response, ignore it
}
unless ($byte == $self->{rmcptag}) {
my $tag_matches = $byte == $self->{rmcptag};
my $sid_matches = scalar(@data) >= 7 &&
pack("C4", @data[3..6]) eq pack("C4", @{ $self->{sidm} });
unless ($tag_matches) {
return 9 unless $byte == 0;
my @sid_check = @data[3..6];
unless (pack("C4", @sid_check) eq pack("C4", @{ $self->{sidm} })) {
return 9;
}
return 9 unless $sid_matches;
}
$byte = shift @data;
unless ($byte == 0x00) {
@@ -1016,8 +1030,18 @@ sub got_rakp2 {
my $ulength = scalar @user;
my $hmacdata = pack("C*", (@{ $self->{sidm} }, @{ $self->{pendingsessionid} }, @{ $self->{randomnumber} }, @{ $self->{remoterandomnumber} }, @{ $self->{remoteguid} }, $self->{rakp_privbyte}, $ulength, @user));
my @expectedhash = (unpack("C*", $self->{hshfn}->($hmacdata, $self->{password})));
my $zero_authcode = $tag_matches && $sid_matches &&
$self->{attempthash} == 256 && scalar(@data) == 32 &&
!grep { $_ } @data;
foreach (0 .. (scalar(@expectedhash) - 1)) {
if ($expectedhash[$_] != $data[$_]) {
# Some BMCs accept suite 17 but return an invalid all-zero
# SHA256 RAKP2 code. Never accept it; retry suite 3 once.
if ($zero_authcode and
$self->{sessionestablishmentcontext} == STATE_EXPECTINGRAKP2 and
$self->retry_rmcpplus_after_zero_rakp2()) {
return 9;
}
$self->{sessionestablishmentcontext} = STATE_FAILED;
$self->{onlogon}->("ERROR: Incorrect password provided", $self->{onlogon_args});
return 9;
@@ -0,0 +1,8 @@
start:ipmi_rakp2_unit_tests
description:Run the IPMI RAKP2 Perl unit tests through xcattest
os:Linux
label:mn_only,ci_test,ipmi,unit,ipmi_unit
cmd:prove -I/opt/xcat/lib/perl -I/opt/xcat/lib/perl/xCAT /opt/xcat/share/xcat/tools/autotest/unit/ipmi_*.t
check:rc==0
check:output=~All tests successful
end
+301
View File
@@ -0,0 +1,301 @@
#!/usr/bin/env perl
use strict;
use warnings;
## no critic (Modules::RequireFilenameMatchesPackage, TestingAndDebugging::ProhibitNoStrict, TestingAndDebugging::ProhibitNoWarnings)
BEGIN {
package xCAT::SvrUtils;
sub import { }
$INC{'xCAT/SvrUtils.pm'} = __FILE__;
package Crypt::Rijndael;
sub import { }
$INC{'Crypt/Rijndael.pm'} = __FILE__;
package Crypt::CBC;
sub import { }
$INC{'Crypt/CBC.pm'} = __FILE__;
package Digest::HMAC_SHA1;
sub import {
my $caller = caller;
no strict 'refs';
*{ $caller . '::hmac_sha1' } = \&Digest::SHA::hmac_sha1;
}
$INC{'Digest/HMAC_SHA1.pm'} = __FILE__;
}
package main;
no warnings qw/once redefine/;
use Digest::SHA ();
use FindBin;
use lib "$FindBin::Bin/../../xCAT-server/lib/perl";
use Test::More;
require xCAT::IPMI;
my @console_sid = (0x15, 0x58, 0x25, 0x7a);
my @managed_sid = (0x7c, 0x5a, 0xfd, 0xc3);
sub new_session {
my (%overrides) = @_;
my $errors = delete($overrides{errors}) || [];
return bless {
attempthash => 256,
hshfn => \&xCAT::IPMI::hmac_sha256,
hshln => 16,
ipmiversion => '2.0',
localsid => 0x1558257a,
onlogon => sub { push @{$errors}, $_[0]; },
onlogon_args => undef,
password => 'correct-password',
pendingsessionid => [@managed_sid],
privlevel => 4,
rakp_privbyte => 0x14,
randomnumber => [0x10 .. 0x1f],
rmcptag => 3,
sessionestablishmentcontext => xCAT::IPMI::STATE_EXPECTINGRAKP2(),
sessionid => [0, 0, 0, 0],
sequencenumberbytes => [0, 0, 0, 0],
sidm => [@console_sid],
userid => 'test',
%overrides,
}, 'xCAT::IPMI';
}
sub rakp2_payload {
my ($session, $authcode) = @_;
my @remote_random = (0x20 .. 0x2f);
my @remote_guid = (0x30 .. 0x3f);
return (
$session->{rmcptag}, 0, 0, 0, @{$session->{sidm}},
@remote_random, @remote_guid, @{$authcode}
);
}
sub valid_rakp2_payload {
my ($session) = @_;
my @remote_random = (0x20 .. 0x2f);
my @remote_guid = (0x30 .. 0x3f);
my @user = unpack('C*', $session->{userid});
my $hmacdata = pack(
'C*',
@{$session->{sidm}}, @{$session->{pendingsessionid}},
@{$session->{randomnumber}}, @remote_random, @remote_guid,
$session->{rakp_privbyte}, scalar(@user), @user
);
my @authcode = unpack(
'C*',
$session->{hshfn}->($hmacdata, $session->{password})
);
return (
$session->{rmcptag}, 0, 0, 0, @{$session->{sidm}},
@remote_random, @remote_guid, @authcode
);
}
sub valid_rakp4_payload {
my ($session) = @_;
my @authcode = unpack(
'C*',
$session->{hshfn}->(
pack(
'C*',
@{$session->{randomnumber}},
@{$session->{pendingsessionid}},
@{$session->{remoteguid}}
),
$session->{sik}
)
);
return (
$session->{rmcptag}, 0, 0, 0, @{$session->{sidm}},
@authcode[0 .. ($session->{hshln} - 1)]
);
}
# Emulate the defective BMC firmware behavior reported in
# https://github.com/xcat2/xcat-core/issues/7512: suite 17 succeeds, but the
# RAKP2 response contains a 32-byte all-zero authentication code.
my @captured_zero_rakp2 = unpack(
'C*',
pack(
'H*',
'030000001558257a7fbac96cbf09227865a9cc62745e3d710ce54d903ff5ec11afa30000000000080000000000000000000000000000000000000000000000000000000000000000'
)
);
is(scalar(@captured_zero_rakp2), 72, 'captured XCC RAKP2 payload is 72 bytes');
subtest 'captured all-zero SHA256 RAKP2 retries once with suite 3' => sub {
my @errors;
my @sent;
my $session = new_session(errors => \@errors);
local *xCAT::IPMI::sendpayload = sub {
my ($self, %args) = @_;
push @sent, \%args;
};
is($session->got_rakp2(@captured_zero_rakp2), 9, 'invalid response is not accepted');
is(scalar(@sent), 1, 'one replacement Open Session request is sent');
is($session->{attempthash}, 1, 'retry selects SHA1');
ok($session->{zero_rakp2_fallback}, 'fallback is recorded for this login');
is(
$session->{sessionestablishmentcontext},
xCAT::IPMI::STATE_OPENSESSION(),
'session waits for the replacement Open Session response'
);
is_deeply(
[@{ $sent[0]->{payload} }[12, 20, 28]],
[1, 1, 1],
'replacement request proposes suite 3 algorithms'
);
my @suite3_response = (
$session->{rmcptag}, 0, 4, 0, @{$session->{sidm}},
0x11, 0x22, 0x33, 0x44,
0, 0, 0, 8, 1, 0, 0, 0,
1, 0, 0, 8, 1, 0, 0, 0,
2, 0, 0, 8, 1, 0, 0, 0,
);
is($session->got_rmcp_response(@suite3_response), 0, 'suite 3 response is accepted');
is(scalar(@sent), 2, 'suite 3 response advances to RAKP1');
is($session->got_rakp2(@captured_zero_rakp2), 9, 'stale SHA256 response is ignored');
is(scalar(@sent), 2, 'stale response cannot start another fallback');
my @valid_sha1_rakp2 = valid_rakp2_payload($session);
is($session->got_rakp2(@valid_sha1_rakp2), 0, 'valid suite 3 RAKP2 is accepted');
is(scalar(@sent), 3, 'valid suite 3 RAKP2 advances to RAKP3');
my $admin_level_set = 0;
local *xCAT::IPMI::set_admin_level = sub { $admin_level_set++; };
my @valid_sha1_rakp4 = valid_rakp4_payload($session);
is($session->got_rakp4(@valid_sha1_rakp4), 0, 'valid suite 3 RAKP4 is accepted');
is(
$session->{sessionestablishmentcontext},
xCAT::IPMI::STATE_ESTABLISHED(),
'suite 3 session is established'
);
is($admin_level_set, 1, 'established session advances to privilege setup');
is_deeply(\@errors, [], 'fallback does not report an incorrect password');
};
subtest 'all-zero SHA1 RAKP2 keeps the existing mismatch path' => sub {
my @errors;
my @sent;
my $session = new_session(
errors => \@errors,
attempthash => 1,
hshfn => \&xCAT::IPMI::hmac_sha1,
hshln => 12,
zero_rakp2_fallback => 1,
);
my @payload = rakp2_payload($session, [(0) x 20]);
local *xCAT::IPMI::sendpayload = sub { push @sent, 1; };
is($session->got_rakp2(@payload), 9, 'second invalid response is rejected');
is(scalar(@sent), 0, 'no second fallback is attempted');
is(
$session->{sessionestablishmentcontext},
xCAT::IPMI::STATE_FAILED(),
'session fails after the bounded retry'
);
is_deeply(
\@errors,
['ERROR: Incorrect password provided'],
'suite 3 retains the existing mismatch error'
);
};
subtest 'ordinary HMAC mismatch remains a password failure' => sub {
my @errors;
my @sent;
my $session = new_session(errors => \@errors);
my @payload = rakp2_payload($session, [0, (1) x 31]);
local *xCAT::IPMI::sendpayload = sub { push @sent, 1; };
is($session->got_rakp2(@payload), 9, 'nonzero bad HMAC is rejected');
is(scalar(@sent), 0, 'ordinary mismatch does not trigger fallback');
is_deeply(
\@errors,
['ERROR: Incorrect password provided'],
'existing wrong-password contract is preserved'
);
};
subtest 'valid SHA256 RAKP2 continues to RAKP3' => sub {
my @errors;
my $rakp3_sent = 0;
my $session = new_session(errors => \@errors);
my @payload = valid_rakp2_payload($session);
local *xCAT::IPMI::send_rakp3 = sub { $rakp3_sent++; };
is($session->got_rakp2(@payload), 0, 'valid response succeeds');
is($rakp3_sent, 1, 'RAKP3 is sent');
is(
$session->{sessionestablishmentcontext},
xCAT::IPMI::STATE_EXPECTINGRAKP4(),
'session advances to RAKP4'
);
is_deeply(\@errors, [], 'valid response emits no error');
};
subtest 'malformed zero authentication lengths do not downgrade' => sub {
foreach my $length (31, 33) {
my @errors;
my @sent;
my $session = new_session(errors => \@errors);
my @payload = rakp2_payload($session, [(0) x $length]);
local *xCAT::IPMI::sendpayload = sub { push @sent, 1; };
is($session->got_rakp2(@payload), 9, "$length-byte response is rejected");
is(scalar(@sent), 0, "$length-byte response does not trigger fallback");
is_deeply(\@errors, ['ERROR: Incorrect password provided'], 'existing error path is preserved');
}
};
subtest 'wrong console session ID cannot trigger a downgrade' => sub {
my @errors;
my @sent;
my $session = new_session(errors => \@errors);
my @payload = @captured_zero_rakp2;
$payload[4] ^= 0xff;
local *xCAT::IPMI::sendpayload = sub { push @sent, 1; };
is($session->got_rakp2(@payload), 9, 'invalid response is rejected');
is(scalar(@sent), 0, 'stale response cannot force fallback');
is_deeply(\@errors, ['ERROR: Incorrect password provided'], 'legacy mismatch path is preserved');
is(
$session->{sessionestablishmentcontext},
xCAT::IPMI::STATE_FAILED(),
'legacy mismatch path fails the exchange'
);
};
subtest 'legacy tag zero cannot trigger a downgrade' => sub {
my @errors;
my @sent;
my $session = new_session(errors => \@errors);
my @payload = @captured_zero_rakp2;
$payload[0] = 0;
local *xCAT::IPMI::sendpayload = sub { push @sent, 1; };
is($session->got_rakp2(@payload), 9, 'tag-zero response is rejected by the HMAC check');
is(scalar(@sent), 0, 'tag-zero response cannot force fallback');
is_deeply(\@errors, ['ERROR: Incorrect password provided'], 'legacy mismatch path is preserved');
};
subtest 'suite 3 selection survives internal relog initialization' => sub {
my $session = new_session(zero_rakp2_fallback => 1);
$session->init();
is($session->{attempthash}, 1, 'relog initialization keeps SHA1 selected');
local *xCAT::IPMI::get_channel_auth_cap = sub { };
$session->login(callback => sub { }, callback_args => undef);
is($session->{attempthash}, 256, 'a new external login starts with SHA256');
ok(!exists($session->{zero_rakp2_fallback}), 'a new external login clears the fallback flag');
};
done_testing();