diff --git a/xCAT-test/unit/ddns_omapi_policy.t b/xCAT-test/unit/ddns_omapi_policy.t index 1797c37af..0568f3bff 100644 --- a/xCAT-test/unit/ddns_omapi_policy.t +++ b/xCAT-test/unit/ddns_omapi_policy.t @@ -3,25 +3,21 @@ use strict; use warnings; use FindBin; +use lib "$FindBin::Bin/../lib"; use lib "$FindBin::Bin/../../xCAT-server/lib"; use lib "$FindBin::Bin/../../xCAT-server/lib/perl"; use lib "$FindBin::Bin/../../perl-xCAT"; -use File::Temp qw(tempfile); +use File::Slurper qw(read_text); +use File::Temp qw(tempfile tempdir); use Test::More; +use XCAT::Test::File qw(repo_path); $ENV{XCATCFG} ||= 'SQLite:/tmp'; -$ENV{XCATROOT} ||= "$FindBin::Bin/../../xCAT-server"; +$ENV{XCATROOT} ||= repo_path('xCAT-server'); -my $ddns_plugin_path = - "$FindBin::Bin/../../xCAT-server/lib/xcat/plugins/ddns.pm"; -if ( -f $ddns_plugin_path ) { - require $ddns_plugin_path; -} -else { - require xCAT_plugin::ddns; - $ddns_plugin_path = $INC{'xCAT_plugin/ddns.pm'}; -} +my $ddns_plugin_path = repo_path('xCAT-server/lib/xcat/plugins/ddns.pm'); +require $ddns_plugin_path; sub omapi_settings { my (%overrides) = @_; @@ -65,11 +61,131 @@ is( 'default DDNS key remains xcat_key with hmac-md5' ); +my $fips_defaults = xCAT::DHCP::OmapiPolicy->settings( + site_values => { + dhcpomapialgorithm => undef, + dhcpomapikeyname => undef, + dhcpomshellpath => undef, + }, + fips_mode => 1, +); +{ + no warnings qw(redefine once); + local *xCAT::Utils::isFIPS = sub { return 1; }; + my $detected_fips = xCAT::DHCP::OmapiPolicy->settings( + site_values => { + dhcpomapialgorithm => undef, + dhcpomapikeyname => undef, + dhcpomshellpath => undef, + }, + ); + is( $detected_fips->{algorithm}, 'hmac-sha256', + 'runtime FIPS detection selects the SHA-256 default' ); +} +is( + xCAT_plugin::ddns::ddns_tsig_algorithm( + { omapi_settings => $fips_defaults } + ), + 'hmac-sha256', + 'FIPS DDNS defaults to the same algorithm as OMAPI' +); +is( + xCAT_plugin::ddns::ddns_key_contents( + { + omapi_settings => $fips_defaults, + privkey => 'fips-secret', + } + ), +"key \"xcat_key\" {\n\talgorithm hmac-sha256;\n\tsecret \"fips-secret\";\n};\n\n", + 'FIPS DDNS key uses hmac-sha256' +); +is_deeply( + xCAT_plugin::ddns::ddns_reconcile_key_algorithm( + $fips_defaults, ' HMAC-SHA512 ' + ), + { algorithm => 'hmac-sha256', replace => 1 }, + 'FIPS mode aligns an implicit existing key with the OMAPI default' +); +is_deeply( + xCAT_plugin::ddns::ddns_reconcile_key_algorithm( + $fips_defaults, 'hmac-md5' + ), + { algorithm => 'hmac-sha256', replace => 1 }, + 'FIPS mode replaces an existing MD5 key' +); +is_deeply( + xCAT_plugin::ddns::ddns_reconcile_key_algorithm( + $fips_defaults, 'hmac-unknown' + ), + { algorithm => 'hmac-sha256', replace => 1 }, + 'FIPS mode replaces an unsupported key algorithm' +); +is_deeply( + xCAT_plugin::ddns::ddns_reconcile_key_algorithm( + $fips_defaults, undef + ), + { algorithm => 'hmac-sha256', replace => 1 }, + 'FIPS mode repairs a key block without an algorithm' +); +is( + xCAT_plugin::ddns::ddns_tsig_algorithm( + { + omapi_settings => $fips_defaults, + tsig_algorithm => 'hmac-sha512', + } + ), + 'hmac-sha256', + 'FIPS DDNS signs with the algorithm OMAPI uses' +); + my $sha512 = omapi_settings( dhcpomapialgorithm => 'hmac-sha512', dhcpomapikeyname => 'provider.key', ); +is_deeply( + xCAT_plugin::ddns::ddns_reconcile_key_algorithm( + $sha512, 'hmac-sha256' + ), + { algorithm => 'hmac-sha512', replace => 1 }, + 'an explicit site algorithm replaces a different existing algorithm' +); +is_deeply( + xCAT_plugin::ddns::ddns_reconcile_key_algorithm( + $defaults, 'hmac-sha512' + ), + { algorithm => 'hmac-sha512', replace => 0 }, + 'the existing algorithm is preserved outside FIPS mode' +); + +foreach my $case ( + [ $defaults, ' HMAC-SHA512 ', 'hmac-sha512', + 'implicit policy preserves a normalized existing algorithm' ], + [ $fips_defaults, "\tHMAC-SHA256 ", 'hmac-sha256', + 'FIPS policy does not replace a matching normalized algorithm' ], + [ $sha512, " HMAC-SHA512\t", 'hmac-sha512', + 'explicit policy does not replace a matching normalized algorithm' ], +) { + my ( $settings, $current, $algorithm, $description ) = @{$case}; + is_deeply( + xCAT_plugin::ddns::ddns_reconcile_key_algorithm($settings, $current), + { algorithm => $algorithm, replace => 0 }, + $description + ); +} + +is( + xCAT_plugin::ddns::ddns_key_contents( + { + omapi_settings => $defaults, + tsig_algorithm => ' HMAC-SHA512 ', + privkey => 'legacy-secret', + } + ), +"key \"xcat_key\" {\n\talgorithm hmac-sha512;\n\tsecret \"legacy-secret\";\n};\n\n", + 'DDNS renders the normalized existing algorithm outside FIPS mode' +); + is( xCAT_plugin::ddns::ddns_tsig_algorithm( { @@ -166,6 +282,55 @@ subtest 'Net::DNS threshold controls named key reconciliation' => sub { "Net::DNS $version leaves named alone" ); } + + my ( $harvested_key, $harvested_restart ) = + reconcile_named_key( '1.35', privkey => undef ); + like( + $harvested_key, + qr/^\s*algorithm\s+hmac-sha256\s*;/m, + 'a harvested key keeps its algorithm on old Net::DNS' + ); + ok( !$harvested_restart, + 'harvesting an unchanged key does not restart named' ); +}; + +subtest 'FIPS named key migration and signing agree' => sub { + foreach my $case ( + ['hmac-md5', 'hmac-sha256', 1, 163], + ['hmac-sha512', 'hmac-sha256', 1, 163], + ) { + my ($current, $expected, $restart, $rr_type) = @{$case}; + foreach my $version ('1.35', '1.47') { + foreach my $harvest (0, 1) { + my $key_dir = tempdir(CLEANUP => 1); + local $xCAT_plugin::ddns::ddns_key_path = "$key_dir/ddns.key"; + my ($named, $restarted, $ctx) = reconcile_named_key( + $version, fips_mode => 1, current_algorithm => $current, + privkey => $harvest ? undef : 'legacy-secret', + ); + like($named, qr/^\s*algorithm\s+\Q$expected\E\s*;/m, + "FIPS reconciles $current on Net::DNS $version, harvest=$harvest"); + is($restarted ? 1 : 0, $restart, + 'named restarts only when its algorithm changes'); + is($ctx->{privkey}, 'legacy-secret', 'migration preserves the shared secret'); + my $update = Local::DDNS::Update->new(); + with_net_dns_version($version, sub { + xCAT_plugin::ddns::ddns_sign_update($ctx, $update); + }); + my $arguments = $update->{sign_tsig_calls}->[0]; + if ($version eq '1.35') { + is($arguments->[0]->algorithm, $rr_type, + 'old Net::DNS signs with the reconciled algorithm'); + } else { + is_deeply($arguments, ["$key_dir/ddns.key"], + 'new Net::DNS uses the reconciled key file'); + like(read_text($arguments->[0]), + qr/^\s*algorithm\s+\Q$expected\E\s*;/m, + 'the signing key file agrees with named and OMAPI'); + } + } + } + } }; done_testing(); @@ -178,20 +343,32 @@ sub with_net_dns_version { } sub reconcile_named_key { - my ($version) = @_; + my ( $version, %args ) = @_; + my $current = $args{current_algorithm} || 'hmac-sha256'; + my $key_dir = tempdir(CLEANUP => 1); + local $xCAT_plugin::ddns::ddns_key_path = + $xCAT_plugin::ddns::ddns_key_path eq '/etc/xcat/ddns.key' + ? "$key_dir/ddns.key" : $xCAT_plugin::ddns::ddns_key_path; my ( $named_fh, $named_path ) = tempfile(UNLINK => 1); print {$named_fh} "options {\n};\n" . "key \"xcat_key\" {\n" - . "\talgorithm hmac-sha256;\n" + . "\talgorithm $current;\n" . "\tsecret \"legacy-secret\";\n" . "};\n"; close($named_fh) or die "Unable to close $named_path: $!"; my $ctx = { - omapi_settings => omapi_settings(), - privkey => 'legacy-secret', + omapi_settings => xCAT::DHCP::OmapiPolicy->settings( + site_values => { + dhcpomapialgorithm => undef, + dhcpomapikeyname => undef, + dhcpomshellpath => undef, + }, + fips_mode => $args{fips_mode} || 0, + ), + privkey => exists $args{privkey} ? $args{privkey} : 'legacy-secret', zonesdir => '/tmp', dbdir => '/tmp', zonestotouch => {}, @@ -203,7 +380,6 @@ sub reconcile_named_key { no warnings qw(redefine once); local *xCAT_plugin::ddns::get_conf = sub { return $named_path; }; - local *xCAT_plugin::ddns::ensure_ddns_key_file = sub { return; }; local *xCAT::TableUtils::get_site_attribute = sub { return; }; local *xCAT::Utils::runcmd = sub { return (); }; local *xCAT::Utils::isAIX = sub { return 0; }; @@ -217,13 +393,7 @@ sub reconcile_named_key { sub { xCAT_plugin::ddns::update_namedconf( $ctx, 0 ); } ); - open( my $result_fh, '<', $named_path ) - or die "Unable to read $named_path: $!"; - local $/; - my $contents = <$result_fh>; - close($result_fh) or die "Unable to close $named_path: $!"; - - return ( $contents, $ctx->{restartneeded} ); + return ( read_text($named_path), $ctx->{restartneeded}, $ctx ); } { diff --git a/xCAT-test/unit/dhcp_omapi_policy.t b/xCAT-test/unit/dhcp_omapi_policy.t index f91cb7bcd..f4c71d540 100644 --- a/xCAT-test/unit/dhcp_omapi_policy.t +++ b/xCAT-test/unit/dhcp_omapi_policy.t @@ -53,6 +53,41 @@ is( 'default omshell preamble keeps legacy key command without key-algorithm' ); +my $fips_defaults = xCAT::DHCP::OmapiPolicy->settings( + site_values => { + dhcpomapialgorithm => undef, + dhcpomapikeyname => undef, + dhcpomshellpath => undef, + }, + fips_mode => 1, +); +is( $fips_defaults->{algorithm}, 'hmac-sha256', + 'FIPS mode defaults OMAPI to hmac-sha256' ); +ok( !$fips_defaults->{algorithm_explicit}, + 'FIPS default remains distinct from an administrator override' ); +ok( $fips_defaults->{needs_omshell_key_algorithm}, + 'FIPS default emits the omshell key algorithm' ); +is( + xCAT::DHCP::OmapiPolicy->omshell_preamble( + $fips_defaults, secret => 'fips-secret' + ), + "key-algorithm hmac-sha256\nkey xcat_key \"fips-secret\"\n", + 'FIPS omshell preamble selects hmac-sha256' +); + +like( + xCAT::DHCP::OmapiPolicy->settings( + site_values => { + dhcpomapialgorithm => 'hmac-md5', + dhcpomapikeyname => undef, + dhcpomshellpath => undef, + }, + fips_mode => 1, + )->{error}, + qr/hmac-md5 is not allowed while FIPS mode is enabled/, + 'FIPS mode rejects an explicit hmac-md5 override' +); + is( xCAT::DHCP::OmapiPolicy->new_install_default_algorithm( is_new_install => 1, @@ -69,6 +104,23 @@ is( undef, 'new EL8 installations retain the implicit MD5 default' ); +foreach my $platform ('el8', 'el9', 'sles15', 'ubuntu') { + my $algorithm = xCAT::DHCP::OmapiPolicy->new_install_default_algorithm( + is_new_install => 1, platform => $platform, fips_mode => 1, + ); + is($algorithm, 'hmac-sha256', "new FIPS $platform sites save SHA-256"); + foreach my $fips_mode (0, 1) { + my $settings = xCAT::DHCP::OmapiPolicy->settings( + site_values => { dhcpomapialgorithm => $algorithm }, + fips_mode => $fips_mode, + ); + is($settings->{algorithm}, 'hmac-sha256', + "the shared $platform site selects SHA-256 on a host with FIPS=$fips_mode"); + } + is(xCAT::DHCP::OmapiPolicy->new_install_default_algorithm( + is_new_install => 0, platform => $platform, fips_mode => 1, + ), undef, "existing $platform sites are not overwritten"); +} is( xCAT::DHCP::OmapiPolicy->new_install_default_algorithm( is_new_install => 1,