mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-08-03 07:57:00 +00:00
302 lines
10 KiB
Perl
302 lines
10 KiB
Perl
#!/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();
|