mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-08-01 02:29:41 +00:00
fix(xcat-server): retry suite 3 on zero RAKP2 HMAC
This commit is contained in:
@@ -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
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user