Files
freeipa/ipatests/test_integration/test_hsm.py
Rob Crittenden 38b83c2b93 Run HSM validation as pkiuser to verify token permissions
Run all commands as pkiuser when validating that the HSM token
is available, that the token library path is correct and that
the password can read keys. This will avoid issues where the
initial validation is ok but the pkiuser is not granted read
access to some part of the token. This is very possible
when using softhsm2.

Fixes: https://pagure.io/freeipa/issue/9626

Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
2024-07-31 16:19:02 -04:00

1316 lines
45 KiB
Python

#
# Copyright (C) 2023 FreeIPA Contributors see COPYING for license
#
import os.path
import pytest
import random
import re
import string
import time
from ipalib.constants import KRA_TRACKING_REQS
from ipapython.ipaldap import realm_to_serverid
from ipatests.test_integration.base import IntegrationTest
from ipatests.test_integration.test_acme import (
prepare_acme_client,
certbot_register,
certbot_standalone_cert,
get_selinux_status,
skip_certbot_tests,
skip_mod_md_tests,
)
from ipatests.test_integration.test_caless import CALessBase
from ipatests.test_integration.test_cert import get_certmonger_fs_id
from ipatests.test_integration.test_external_ca import (
install_server_external_ca_step1,
install_server_external_ca_step2,
check_CA_flag,
verify_caentry
)
from ipatests.test_integration.test_ipa_cert_fix import (
check_status,
needs_resubmit,
get_cert_expiry
)
from ipatests.test_integration.test_ipahealthcheck import (
run_healthcheck,
set_excludes
)
from ipatests.pytest_ipa.integration import tasks
from ipatests.pytest_ipa.integration.env_config import get_global_config
from ipalib import x509 as ipa_x509
from ipaplatform.paths import paths
config = get_global_config()
hsm_lib_path = ''
if config.token_library:
hsm_lib_path = config.token_library
else:
hsm_lib_path = '/usr/lib64/pkcs11/libsofthsm2.so'
def get_hsm_token(host):
"""Helper method to get an hsm token
This method creates a softhsm token if the hsm hardware
token is not found.
"""
if host.config.token_name:
return (host.config.token_name, host.config.token_password)
token_name = ''.join(
random.choice(string.ascii_letters) for i in range(10)
)
token_passwd = ''.join(
random.choice(string.ascii_letters) for i in range(10)
)
# remove the token if already exist
host.run_command(
['softhsm2-util', '--delete-token', '--token', token_name],
raiseonerr=False
)
host.run_command(
['runuser', '-u', 'pkiuser', '--', 'softhsm2-util', '--init-token',
'--free', '--pin', token_passwd, '--so-pin', token_passwd,
'--label', token_name]
)
return (token_name, token_passwd)
def delete_hsm_token(hosts, token_name):
for host in hosts:
if host.config.token_name:
# assumption: for time being /root/cleantoken.sh is copied
# host manually. This should be removed in final iteration.
host.run_command(['sh', '/root/cleantoken.sh'])
else:
host.run_command(
['softhsm2-util', '--delete-token', '--token', token_name],
raiseonerr=False
)
def find_softhsm_token_files(host, token):
if not host.transport.file_exists(paths.PKI_TOMCAT_ALIAS_DIR):
return None, []
result = host.run_command([
paths.MODUTIL, '-list', 'libsofthsm2',
'-dbdir', paths.PKI_TOMCAT_ALIAS_DIR
])
serial = None
state = 'token_name'
for line in result.stdout_text.split('\n'):
if state == 'token_name' and 'Token Name:' in line.strip():
(_label, tokenname) = line.split(':', 1)
if tokenname.strip() == token:
state = 'serial'
elif state == 'serial' and 'Token Serial Number' in line.strip():
(_label, serial) = line.split(':', 1)
serial = serial.strip()
serial = "{}-{}".format(serial[0:4], serial[4:])
break
if serial is None:
raise RuntimeError("can't find softhsm token serial for %s"
% token)
result = host.run_command(
['ls', '-l', '/var/lib/softhsm/tokens/'])
serialdir = None
for r in result.stdout_text.split('\n'):
if serial in r:
dirname = r.split()[-1:][0]
serialdir = f'/var/lib/softhsm/tokens/{dirname}'
break
if serialdir is None:
raise RuntimeError("can't find softhsm token directory for %s"
% serial)
result = host.run_command(['ls', '-1', serialdir])
return serialdir, [
os.path.join(serialdir, file)
for file in result.stdout_text.strip().split('\n')
]
def copy_token_files(src_host, dest_host, token_name):
"""Helper method to copy the token files to replica"""
# copy the token files to replicas
if not src_host.config.token_name:
serialdir, token_files = find_softhsm_token_files(
src_host, token_name
)
if serialdir:
for host in dest_host:
tasks.copy_files(src_host, host, token_files)
host.run_command(['usermod', 'pkiuser', '-a', '-G', 'ods'])
host.run_command(
['chown', '-R', 'pkiuser:pkiuser', serialdir]
)
def check_version(host):
if tasks.get_pki_version(host) < tasks.parse_version('11.5.0'):
raise pytest.skip("PKI HSM support is not available")
class BaseHSMTest(IntegrationTest):
master_with_dns = True
master_with_kra = False
master_with_ad = False
master_extra_args = []
token_password = None
token_name = None
token_password_file = '/tmp/token_password'
random_serial = False
@classmethod
def install(cls, mh):
check_version(cls.master)
# Enable pkiuser to read softhsm tokens
cls.master.run_command(['usermod', 'pkiuser', '-a', '-G', 'ods'])
cls.token_name, cls.token_password = get_hsm_token(cls.master)
cls.master.put_file_contents(
cls.token_password_file, cls.token_password
)
tasks.install_master(
cls.master, setup_dns=cls.master_with_dns,
setup_kra=cls.master_with_kra,
setup_adtrust=cls.master_with_ad,
extra_args=(
'--token-name', cls.token_name,
'--token-library-path', hsm_lib_path,
'--token-password', cls.token_password
)
)
cls.sync_tokens(cls.master)
@classmethod
def uninstall(cls, mh):
check_version(cls.master)
super(BaseHSMTest, cls).uninstall(mh)
delete_hsm_token([cls.master] + cls.replicas, cls.token_name)
@classmethod
def sync_tokens(cls, source, token_name=None):
"""Synchronize non-networked HSM tokens between machines
source: source host for the token data
"""
if (
hsm_lib_path
and 'nfast' in hsm_lib_path
):
for host in [cls.master] + cls.replicas:
if host == source:
continue
tasks.copy_nfast_data(source, host)
for host in [cls.master] + cls.replicas:
if host == source:
continue
copy_token_files(source, [host],
token_name if token_name else cls.token_name)
class TestHSMInstall(BaseHSMTest):
num_replicas = 3
num_clients = 1
topology = 'star'
def test_hsm_install_replica0_ca_less_install(self):
check_version(self.master)
tasks.install_replica(
self.master, self.replicas[0], setup_ca=False,
setup_dns=True,
)
def test_hsm_install_replica0_ipa_ca_install(self):
check_version(self.master)
self.sync_tokens(self.master)
tasks.install_ca(
self.replicas[0],
extra_args=('--token-password', self.token_password,),
)
def test_hsm_install_replica0_ipa_kra_install(self):
check_version(self.master)
tasks.install_kra(
self.replicas[0], first_instance=True,
extra_args=('--token-password', self.token_password,)
)
self.sync_tokens(self.replicas[0])
def test_hsm_install_replica0_ipa_dns_install(self):
tasks.install_dns(self.replicas[0])
def test_hsm_install_replica1_with_ca_install(self):
check_version(self.master)
tasks.install_replica(
self.master, self.replicas[1], setup_ca=True,
extra_args=('--token-password', self.token_password,)
)
def test_hsm_install_replica1_ipa_kra_install(self):
check_version(self.master)
tasks.install_kra(
self.replicas[1],
extra_args=('--token-password', self.token_password,)
)
def test_hsm_install_replica1_ipa_dns_install(self):
check_version(self.master)
tasks.install_dns(self.replicas[1])
def test_hsm_install_replica2_with_ca_kra_dns_install(self):
check_version(self.master)
tasks.install_replica(
self.master, self.replicas[2], setup_ca=True, setup_kra=True,
setup_dns=True,
extra_args=('--token-password', self.token_password,)
)
def test_hsm_install_master_ipa_kra_install(self):
check_version(self.master)
tasks.install_kra(
self.master,
extra_args=('--token-password', self.token_password,)
)
def test_hsm_install_client(self):
check_version(self.master)
tasks.install_client(self.master, self.clients[0])
def test_hsm_install_issue_user_cert(self):
check_version(self.master)
user = 'testuser1'
csr_file = f'{user}.csr'
key_file = f'{user}.key'
cert_file = f'{user}.crt'
tasks.kinit_admin(self.master)
tasks.user_add(self.master, user)
openssl_cmd = [
'openssl', 'req', '-newkey', 'rsa:2048', '-keyout', key_file,
'-nodes', '-out', csr_file, '-subj', '/CN=' + user]
self.master.run_command(openssl_cmd)
cmd_args = ['ipa', 'cert-request', '--principal', user,
'--certificate-out', cert_file, csr_file]
self.master.run_command(cmd_args)
def test_hsm_install_healthcheck(self):
check_version(self.master)
set_excludes(self.master, "key", "DSCLE0004")
tasks.install_packages(self.master, ['*ipa-healthcheck'])
returncode, output = run_healthcheck(
self.master, output_type="human", failures_only=True
)
assert returncode == 0
assert output == "No issues found."
def test_hsm_install_server_password_file(self):
check_version(self.master)
# cleanup before fresh install with password file
for client in self.clients:
tasks.uninstall_client(client)
for replica in self.replicas:
tasks.uninstall_master(replica)
tasks.uninstall_master(self.master)
delete_hsm_token([self.master] + self.replicas, self.token_name)
self.token_name, self.token_password = get_hsm_token(self.master)
self.master.put_file_contents(self.token_password_file,
self.token_password)
self.replicas[0].put_file_contents(self.token_password_file,
self.token_password)
tasks.install_master(
self.master, setup_dns=self.master_with_dns,
setup_kra=self.master_with_kra,
setup_adtrust=self.master_with_ad,
extra_args=(
'--token-name', self.token_name,
'--token-library-path', hsm_lib_path,
'--token-password-file', self.token_password_file
)
)
self.sync_tokens(self.master, token_name=self.token_name)
def test_hsm_install_replica0_password_file(self):
check_version(self.master)
tasks.install_replica(
self.master, self.replicas[0], setup_ca=True,
extra_args=('--token-password-file', self.token_password_file,)
)
def test_hsm_install_replica0_kra_password_file(self):
check_version(self.master)
tasks.install_kra(
self.replicas[0],
extra_args=('--token-password-file', self.token_password_file,)
)
class TestHSMInstallADTrustBase(BaseHSMTest):
"""
Base test for builtin AD trust installation in combination with other
components with HSM support
"""
num_replicas = 1
master_with_dns = False
master_with_kra = True
def test_hsm_adtrust_replica0_all_components(self):
check_version(self.master)
tasks.install_replica(
self.master, self.replicas[0], setup_ca=True,
setup_adtrust=False, setup_kra=True, setup_dns=True,
nameservers='master' if self.master_with_dns else None,
extra_args=('--token-password', self.token_password,)
)
class TestADTrustInstallWithDNS_KRA_ADTrust(BaseHSMTest):
num_replicas = 1
master_with_dns = True
master_with_kra = True
master_with_ad = True
def test_hsm_adtrust_replica0(self):
check_version(self.master)
tasks.install_replica(
self.master, self.replicas[0], setup_ca=True, setup_kra=True,
extra_args=('--token-password', self.token_password,)
)
class TestHSMcertRenewal(BaseHSMTest):
master_with_kra = True
def test_certs_renewal(self):
"""
Test that the KRA subsystem certificates renew properly
"""
check_version(self.master)
CA_TRACKING_REQS = {
'ocspSigningCert cert-pki-ca': 'caocspSigningCert',
'subsystemCert cert-pki-ca': 'casubsystemCert',
'auditSigningCert cert-pki-ca': 'caauditSigningCert'
}
CA_TRACKING_REQS.update(KRA_TRACKING_REQS)
self.master.put_file_contents(self.token_password_file,
self.token_password)
for nickname in CA_TRACKING_REQS:
cert = tasks.certutil_fetch_cert(
self.master,
paths.PKI_TOMCAT_ALIAS_DIR,
self.token_password_file,
nickname,
token_name=self.token_name,
)
starting_serial = int(cert.serial_number)
cmd_arg = [
'ipa-getcert', 'resubmit', '-v', '-w',
'-d', paths.PKI_TOMCAT_ALIAS_DIR,
'-n', nickname,
]
result = self.master.run_command(cmd_arg)
request_id = re.findall(r'\d+', result.stdout_text)
status = tasks.wait_for_request(self.master, request_id[0], 120)
assert status == "MONITORING"
args = ['-L', '-h', self.token_name, '-f',
self.token_password_file,]
tasks.run_certutil(self.master, args, paths.PKI_TOMCAT_ALIAS_DIR)
cert = tasks.certutil_fetch_cert(
self.master,
paths.PKI_TOMCAT_ALIAS_DIR,
self.token_password_file,
nickname,
token_name=self.token_name,
)
assert starting_serial != int(cert.serial_number)
class TestHSMCALessToExternalToSelfSignedCA(CALessBase, BaseHSMTest):
"""Test server caless to external CA to self signed scenario"""
num_replicas = 1
@classmethod
def install(cls, mh):
check_version(cls.master)
super(TestHSMCALessToExternalToSelfSignedCA, cls).install(mh)
# Enable pkiuser to read softhsm tokens
cls.master.run_command(['usermod', 'pkiuser', '-a', '-G', 'ods'])
cls.token_name, cls.token_password = get_hsm_token(cls.master)
@classmethod
def uninstall(cls, mh):
check_version(cls.master)
super(TestHSMCALessToExternalToSelfSignedCA, cls).uninstall(mh)
delete_hsm_token([cls.master] + cls.replicas, cls.token_name)
def test_hsm_caless_server(self):
"""Install CA-less master"""
check_version(self.master)
self.create_pkcs12('ca1/server')
self.prepare_cacert('ca1')
master = self.install_server()
assert master.returncode == 0
def test_hsm_caless_to_ca_full(self):
check_version(self.master)
tasks.install_ca(
self.master,
extra_args=(
'--token-name', self.token_name,
'--token-library-path', hsm_lib_path,
'--token-password', self.token_password
),
)
self.sync_tokens(self.master)
ca_show = self.master.run_command(['ipa', 'ca-show', 'ipa'])
assert 'Subject DN: CN=Certificate Authority,O={}'.format(
self.master.domain.realm) in ca_show.stdout_text
def test_hsm_caless_selfsigned_to_external_ca_install(self):
# Install external CA on master
result = self.master.run_command([paths.IPA_CACERT_MANAGE, 'renew',
'--external-ca'])
assert result.returncode == 0
# Sign CA, transport it to the host and get ipa a root ca paths.
root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport(
self.master, paths.IPA_CA_CSR, ROOT_CA, IPA_CA)
# renew CA with externally signed one
result = self.master.run_command([paths.IPA_CACERT_MANAGE, 'renew',
'--external-cert-file={}'.
format(ipa_ca_fname),
'--external-cert-file={}'.
format(root_ca_fname)])
assert result.returncode == 0
# update IPA certificate databases
result = self.master.run_command([paths.IPA_CERTUPDATE])
assert result.returncode == 0
# Check if external CA have "C" flag after the switch
result = check_CA_flag(self.master)
assert bool(result), ('External CA does not have "C" flag')
# Check that ldap entries for the CA have been updated
remote_cacrt = self.master.get_file_contents(ipa_ca_fname)
cacrt = ipa_x509.load_pem_x509_certificate(remote_cacrt)
verify_caentry(self.master, cacrt)
def test_hsm_caless_external_to_self_signed_ca(self):
check_version(self.master)
self.master.run_command([paths.IPA_CACERT_MANAGE, 'renew',
'--self-signed'])
self.master.run_command([paths.IPA_CERTUPDATE])
def test_hsm_caless_replica0_with_ca_install(self):
check_version(self.master)
self.sync_tokens(self.master)
tasks.install_replica(
self.master, self.replicas[0], setup_ca=True,
extra_args=('--token-password', self.token_password,)
)
IPA_CA = "ipa_ca.crt"
ROOT_CA = "root_ca.crt"
class TestHSMExternalToSelfSignedCA(BaseHSMTest):
"""
Test of FreeIPA server installation with external CA then
renew it to self-signed
"""
num_replicas = 1
@classmethod
def install(cls, mh):
check_version(cls.master)
# Enable pkiuser to read softhsm tokens
cls.master.run_command(['usermod', 'pkiuser', '-a', '-G', 'ods'])
cls.token_name, cls.token_password = get_hsm_token(cls.master)
@classmethod
def uninstall(cls, mh):
check_version(cls.master)
super(TestHSMExternalToSelfSignedCA, cls).uninstall(mh)
delete_hsm_token([cls.master] + cls.replicas, cls.token_name)
def test_hsm_external_ca_install(self):
check_version(self.master)
# Step 1 of ipa-server-install.
result = install_server_external_ca_step1(
self.master,
extra_args=[
'--external-ca-type=ms-cs',
'--token-name', self.token_name,
'--token-library-path', hsm_lib_path,
'--token-password', self.token_password
]
)
assert result.returncode == 0
root_ca_fname, ipa_ca_fname = tasks.sign_ca_and_transport(
self.master, paths.ROOT_IPA_CSR, ROOT_CA, IPA_CA
)
# Step 2 of ipa-server-install.
result = install_server_external_ca_step2(
self.master, ipa_ca_fname, root_ca_fname,
extra_args=[
'--token-name', self.token_name,
'--token-library-path', hsm_lib_path,
'--token-password', self.token_password
]
)
assert result.returncode == 0
self.sync_tokens(self.master)
def test_hsm_external_kra_install(self):
check_version(self.master)
tasks.install_kra(
self.master, first_instance=True,
extra_args=('--token-password', self.token_password,)
)
self.sync_tokens(self.master)
def test_hsm_external_to_self_signed_ca(self):
check_version(self.master)
self.master.run_command([paths.IPA_CACERT_MANAGE, 'renew',
'--self-signed'])
self.master.run_command([paths.IPA_CERTUPDATE])
def test_hsm_external_ca_replica0_install(self):
check_version(self.master)
tasks.install_replica(
self.master, self.replicas[0], setup_kra=True,
extra_args=('--token-password', self.token_password,)
)
@pytest.fixture
def expire_cert_critical():
"""
Fixture to expire the certs by moving the system date using
date -s command and revert it back
"""
hosts = dict()
def _expire_cert_critical(host):
hosts['host'] = host
# move date to expire certs
tasks.move_date(host, 'stop', '+3Years+1day')
host.run_command(
['ipactl', 'restart', '--ignore-service-failures']
)
yield _expire_cert_critical
host = hosts.pop('host')
# Prior to uninstall remove all the cert tracking to prevent
# errors from certmonger trying to check the status of certs
# that don't matter because we are uninstalling.
host.run_command(['systemctl', 'stop', 'certmonger'])
# Important: run_command with a str argument is able to
# perform shell expansion but run_command with a list of
# arguments is not
host.run_command('rm -fv ' + paths.CERTMONGER_REQUESTS_DIR + '*')
tasks.uninstall_master(host)
tasks.move_date(host, 'start', '-3Years-1day')
class TestHSMcertFix(BaseHSMTest):
master_with_dns = False
def test_hsm_renew_expired_cert_on_master(self, expire_cert_critical):
check_version(self.master)
expire_cert_critical(self.master)
# wait for cert expiry
check_status(self.master, 8, "CA_UNREACHABLE")
self.master.run_command(['ipa-cert-fix', '-v'], stdin_text='yes\n')
check_status(self.master, 9, "MONITORING", timeout=1000)
# second iteration of ipa-cert-fix
result = self.master.run_command(
['ipa-cert-fix', '-v'],
stdin_text='yes\n'
)
assert "Nothing to do" in result.stdout_text
check_status(self.master, 9, "MONITORING")
class TestHSMcertFixKRA(BaseHSMTest):
master_with_dns = False
master_with_kra = True
def test_hsm_renew_expired_cert_with_kra(self, expire_cert_critical):
check_version(self.master)
expire_cert_critical(self.master)
# check if all subsystem cert expired
check_status(self.master, 11, "CA_UNREACHABLE")
self.master.run_command(['ipa-cert-fix', '-v'], stdin_text='yes\n')
self.master.run_command(['systemctl', 'restart', 'certmonger'])
check_status(self.master, 12, "MONITORING")
class TestHSMcertFixReplica(BaseHSMTest):
num_replicas = 1
master_with_dns = False
@classmethod
def install(cls, mh):
super(TestHSMcertFixReplica, cls).install(mh)
tasks.install_replica(
cls.master, cls.replicas[0], setup_ca=True,
nameservers='master' if cls.master_with_dns else None,
extra_args=('--token-password', cls.token_password,)
)
@pytest.fixture
def expire_certs(self):
# move system date to expire certs
for host in self.master, self.replicas[0]:
tasks.move_date(host, 'stop', '+3years+1days')
host.run_command(
['ipactl', 'restart', '--ignore-service-failures']
)
yield
# move date back on replica and master
for host in self.replicas[0], self.master:
tasks.uninstall_master(host)
tasks.move_date(host, 'start', '-3years-1days')
def test_hsm_renew_expired_cert_replica(self, expire_certs):
check_version(self.master)
# wait for cert expiry
self.master.run_command(['systemctl', 'restart', 'certmonger'])
check_status(self.master, 8, "CA_UNREACHABLE")
self.master.run_command(['ipa-cert-fix', '-v'], stdin_text='yes\n')
check_status(self.master, 9, "MONITORING")
# replica operations
# 'Server-Cert cert-pki-ca' cert will be in CA_UNREACHABLE state
cmd = self.replicas[0].run_command(
['getcert', 'list',
'-d', paths.PKI_TOMCAT_ALIAS_DIR,
'-n', 'Server-Cert cert-pki-ca']
)
req_id = get_certmonger_fs_id(cmd.stdout_text)
tasks.wait_for_certmonger_status(
self.replicas[0], ('CA_UNREACHABLE'), req_id, timeout=600
)
# get initial expiry date to compare later with renewed cert
initial_expiry = get_cert_expiry(
self.replicas[0],
paths.PKI_TOMCAT_ALIAS_DIR,
'Server-Cert cert-pki-ca'
)
# check that HTTP,LDAP,PKINIT are renewed and in MONITORING state
instance = realm_to_serverid(self.master.domain.realm)
dirsrv_cert = paths.ETC_DIRSRV_SLAPD_INSTANCE_TEMPLATE % instance
for cert in (paths.KDC_CERT, paths.HTTPD_CERT_FILE):
cmd = self.replicas[0].run_command(
['getcert', 'list', '-f', cert]
)
req_id = get_certmonger_fs_id(cmd.stdout_text)
tasks.wait_for_certmonger_status(
self.replicas[0], ('MONITORING'), req_id, timeout=600
)
cmd = self.replicas[0].run_command(
['getcert', 'list', '-d', dirsrv_cert]
)
req_id = get_certmonger_fs_id(cmd.stdout_text)
tasks.wait_for_certmonger_status(
self.replicas[0], ('MONITORING'), req_id, timeout=600
)
# check if replication working fine
testuser = 'testuser1'
password = 'Secret@123'
stdin = (f"{self.master.config.admin_password}\n"
f"{self.master.config.admin_password}\n"
f"{self.master.config.admin_password}\n")
self.master.run_command(['kinit', 'admin'], stdin_text=stdin)
tasks.user_add(self.master, testuser, password=password)
self.replicas[0].run_command(['kinit', 'admin'], stdin_text=stdin)
self.replicas[0].run_command(['ipa', 'user-show', testuser])
# renew shared certificates by resubmitting to certmonger
cmd = self.replicas[0].run_command(
['getcert', 'list', '-f', paths.RA_AGENT_PEM]
)
req_id = get_certmonger_fs_id(cmd.stdout_text)
if needs_resubmit(self.replicas[0], req_id):
self.replicas[0].run_command(
['getcert', 'resubmit', '-i', req_id]
)
tasks.wait_for_certmonger_status(
self.replicas[0], ('MONITORING'), req_id, timeout=600
)
for cert_nick in ('auditSigningCert cert-pki-ca',
'ocspSigningCert cert-pki-ca',
'subsystemCert cert-pki-ca'):
cmd = self.replicas[0].run_command(
['getcert', 'list',
'-d', paths.PKI_TOMCAT_ALIAS_DIR,
'-n', cert_nick]
)
req_id = get_certmonger_fs_id(cmd.stdout_text)
if needs_resubmit(self.replicas[0], req_id):
self.replicas[0].run_command(
['getcert', 'resubmit', '-i', req_id]
)
tasks.wait_for_certmonger_status(
self.replicas[0], ('MONITORING'), req_id, timeout=600
)
self.replicas[0].run_command(
['ipa-cert-fix', '-v'], stdin_text='yes\n'
)
check_status(self.replicas[0], 9, "MONITORING")
# Sometimes certmonger takes time to update the cert status
# So check in nssdb instead of relying on getcert command
renewed_expiry = get_cert_expiry(
self.replicas[0],
paths.PKI_TOMCAT_ALIAS_DIR,
'Server-Cert cert-pki-ca'
)
assert renewed_expiry > initial_expiry
class TestHSMNegative(IntegrationTest):
master_with_dns = False
token_password_file = '/tmp/token_password'
@classmethod
def install(cls, mh):
check_version(cls.master)
# Enable pkiuser to read softhsm tokens
cls.master.run_command(['usermod', 'pkiuser', '-a', '-G', 'ods'])
cls.token_name, cls.token_password = get_hsm_token(cls.master)
@classmethod
def uninstall(cls, mh):
cls.master.run_command(
['softhsm2-util', '--delete-token', '--token', cls.token_name],
raiseonerr=False
)
def test_hsm_negative_wrong_token_details(self):
check_version(self.master)
# wrong token name
result = tasks.install_master(
self.master, raiseonerr=False,
extra_args=(
'--token-name', 'random_token',
'--token-library-path', hsm_lib_path,
'--token-password', self.token_password
)
)
assert result.returncode != 0
# wrong token password
result = tasks.install_master(
self.master, raiseonerr=False,
extra_args=(
'--token-name', self.token_name,
'--token-library-path', hsm_lib_path,
'--token-password', 'token_passwd'
)
)
assert result.returncode != 0
# wrong token lib
result = tasks.install_master(
self.master, raiseonerr=False,
extra_args=(
'--token-name', self.token_name,
'--token-library-path', '/tmp/non_existing_hsm_lib_path',
'--token-password', self.token_password
)
)
assert result.returncode != 0
def test_hsm_negative_bad_token_dir_permissions(self):
"""Create an unreadable softhsm2 token and install should fail.
This is most often seen on replicas where the pkiuser is not
a member of the ods group.
"""
check_version(self.master)
token_name = 'bad_perms'
token_passwd = 'Secret123'
self.master.run_command(
['softhsm2-util', '--delete-token', '--token', token_name],
raiseonerr=False
)
self.master.run_command(
['usermod', 'pkiuser', '-a', '-G', 'ods']
)
self.master.run_command(
['softhsm2-util', '--init-token',
'--free', '--pin', token_passwd, '--so-pin', token_passwd,
'--label', token_name]
)
self.master.run_command(
['usermod', 'pkiuser', '-r', '-G', 'ods']
)
result = tasks.install_master(
self.master, raiseonerr=False,
extra_args=(
'--token-name', token_name,
'--token-library-path', hsm_lib_path,
'--token-password', token_passwd
)
)
self.master.run_command(
['usermod', 'pkiuser', '-a', '-G', 'ods']
)
self.master.run_command(
['softhsm2-util', '--delete-token', '--token', token_name],
raiseonerr=False
)
assert result.returncode != 0
assert (
f"Token named '{token_name}' was not found"
in result.stderr_text
)
def test_hsm_negative_special_char_token_name(self):
check_version(self.master)
token_name = 'hsm:token'
token_passwd = 'Secret123'
self.master.run_command(
['softhsm2-util', '--delete-token', '--token', token_name],
raiseonerr=False
)
self.master.run_command(
['runuser', '-u', 'pkiuser', '--', 'softhsm2-util', '--init-token',
'--free', '--pin', token_passwd, '--so-pin', token_passwd,
'--label', token_name]
)
# special character in token name
result = tasks.install_master(
self.master, raiseonerr=False,
extra_args=(
'--token-name', token_name,
'--token-library-path', hsm_lib_path,
'--token-password', token_passwd
)
)
assert result.returncode != 0
def test_hsm_negative_token_password_and_file(self):
"""Test token-password and token-password-file at same time
Test if command fails when --token-password and --token-password-file
provided at the same time results into command failure.
"""
check_version(self.master)
self.master.put_file_contents(
self.token_password_file, self.token_password
)
result = tasks.install_master(
self.master, raiseonerr=False,
extra_args=(
'--token-name', self.token_name,
'--token-library-path', hsm_lib_path,
'--token-password', self.token_password,
'--token-password-file', self.token_password_file
)
)
self.master.run_command(
['softhsm2-util', '--delete-token', '--token', self.token_name],
raiseonerr=False
)
# assert 'error message non existing token lib' in result.stderr_text
assert result.returncode != 0
class TestHSMACME(CALessBase):
num_clients = 1
@classmethod
def install(cls, mh):
check_version(cls.master)
super(TestHSMACME, cls).install(mh)
# install packages before client install in case of IPA DNS problems
cls.acme_server = prepare_acme_client(cls.master, cls.clients[0])
# Enable pkiuser to read softhsm tokens
cls.master.run_command(['usermod', 'pkiuser', '-a', '-G', 'ods'])
cls.token_name, cls.token_password = get_hsm_token(cls.master)
tasks.install_master(
cls.master, setup_dns=True,
extra_args=(
'--token-name', cls.token_name,
'--token-library-path', hsm_lib_path,
'--token-password', cls.token_password
)
)
tasks.install_client(cls.master, cls.clients[0])
@classmethod
def uninstall(cls, mh):
check_version(cls.master)
super(TestHSMACME, cls).uninstall(mh)
delete_hsm_token([cls.master], cls.token_name)
@pytest.mark.skipif(skip_certbot_tests, reason='certbot not available')
def test_certbot_certonly_standalone(self):
check_version(self.master)
# enable ACME on server
tasks.kinit_admin(self.master)
self.master.run_command(['ipa-acme-manage', 'enable'])
# register account to certbot
certbot_register(self.clients[0], self.acme_server)
# request ACME cert with certbot
certbot_standalone_cert(self.clients[0], self.acme_server)
@pytest.mark.skipif(skip_mod_md_tests, reason='mod_md not available')
def test_mod_md(self):
check_version(self.master)
if get_selinux_status(self.clients[0]):
# mod_md requires its own SELinux policy to grant perms to
# maintaining ACME registration and cert state.
raise pytest.skip("SELinux is enabled, this will fail")
# write config
self.clients[0].run_command(['mkdir', '-p', '/etc/httpd/conf.d'])
self.clients[0].run_command(['mkdir', '-p', '/etc/httpd/md'])
self.clients[0].put_file_contents(
'/etc/httpd/conf.d/md.conf',
'\n'.join([
f'MDCertificateAuthority {self.acme_server}',
'MDCertificateAgreement accepted',
'MDStoreDir /etc/httpd/md',
f'MDomain {self.clients[0].hostname}',
'<VirtualHost *:443>',
f' ServerName {self.clients[0].hostname}',
' SSLEngine on',
'</VirtualHost>\n',
]),
)
# To check for successful cert issuance means knowing how mod_md
# stores certificates, or looking for specific log messages.
# If the thing we are inspecting changes, the test will break.
# So I prefer a conservative sleep.
#
self.clients[0].run_command(['systemctl', 'restart', 'httpd'])
time.sleep(15)
# We expect mod_md has acquired the certificate by now.
# Perform a graceful restart to begin using the cert.
# (If mod_md ever learns to start using newly acquired
# certificates /without/ the second restart, then both
# of these sleeps can be replaced by "loop until good".)
#
self.clients[0].run_command(['systemctl', 'reload', 'httpd'])
time.sleep(3)
# HTTPS request from server to client (should succeed)
self.master.run_command(
['curl', f'https://{self.clients[0].hostname}'])
# clean-up
self.clients[0].run_command(['rm', '-rf', '/etc/httpd/md'])
self.clients[0].run_command(['rm', '-f', '/etc/httpd/conf.d/md.conf'])
class TestHSMBackupRestore(BaseHSMTest):
def test_hsm_backup_restore(self):
check_version(self.master)
backup_path = tasks.get_backup_dir(self.master)
self.master.run_command(['ipa-server-install',
'--uninstall',
'-U'])
assert not self.master.transport.file_exists(
paths.IPA_CUSTODIA_KEYS)
assert not self.master.transport.file_exists(
paths.IPA_CUSTODIA_CONF)
self.master.run_command(
['ipa-restore', backup_path],
stdin_text=f'{self.master.config.dirman_password}\nyes'
)
@pytest.fixture
def issue_and_expire_acme_cert():
"""Fixture to expire cert by moving date past expiry of acme cert"""
hosts = []
def _issue_and_expire_acme_cert(
master, client,
acme_server_url, no_of_cert=1
):
hosts.append(master)
hosts.append(client)
# enable the ACME service on master
master.run_command(['ipa-acme-manage', 'enable'])
# register the account with certbot
certbot_register(client, acme_server_url)
# request a standalone acme cert
certbot_standalone_cert(client, acme_server_url, no_of_cert)
# move system date to expire acme cert
for host in hosts:
tasks.kdestroy_all(host)
tasks.move_date(host, 'stop', '+90days+2hours')
# restart ipa services as date moved and wait to get things settle
time.sleep(10)
master.run_command(['ipactl', 'restart'])
time.sleep(10)
tasks.get_kdcinfo(master)
# Note raiseonerr=False:
# the assert is located after kdcinfo retrieval.
# run kinit command repeatedly until sssd gets settle
# after date change
tasks.run_repeatedly(
master, "KRB5_TRACE=/dev/stdout kinit admin",
stdin_text='{0}\n{0}\n{0}\n'.format(
master.config.admin_password
)
)
# Retrieve kdc.$REALM after the password change, just in case SSSD
# domain status flipped to online during the password change.
tasks.get_kdcinfo(master)
yield _issue_and_expire_acme_cert
# move back date
for host in hosts:
tasks.move_date(host, 'start', '-90days-2hours')
# restart ipa services as date moved and wait to get things settle
# if the internal fixture was not called (for instance because the test
# was skipped), hosts = [] and hosts[0] would produce an IndexError
# exception.
if hosts:
time.sleep(10)
hosts[0].run_command(['ipactl', 'restart'])
time.sleep(10)
class TestHSMACMEPrune(IntegrationTest):
"""Validate that ipa-acme-manage configures dogtag for pruning"""
num_clients = 1
@classmethod
def install(cls, mh):
check_version(cls.master)
super(TestHSMACMEPrune, cls).install(mh)
# install packages before client install in case of IPA DNS problems
cls.acme_server = prepare_acme_client(cls.master, cls.clients[0])
# Enable pkiuser to read softhsm tokens
cls.master.run_command(['usermod', 'pkiuser', '-a', '-G', 'ods'])
cls.token_name, cls.token_password = get_hsm_token(cls.master)
tasks.install_master(
cls.master, setup_dns=True,
random_serial=True,
extra_args=(
'--token-name', cls.token_name,
'--token-library-path', hsm_lib_path,
'--token-password', cls.token_password
)
)
tasks.install_client(cls.master, cls.clients[0])
@classmethod
def uninstall(cls, mh):
check_version(cls.master)
super(TestHSMACMEPrune, cls).uninstall(mh)
delete_hsm_token([cls.master], cls.token_name)
def test_hsm_prune_cert_manual(self, issue_and_expire_acme_cert):
"""Test to prune expired certificate by manual run"""
if (tasks.get_pki_version(self.master)
< tasks.parse_version('11.3.0')):
raise pytest.skip("Certificate pruning is not available")
issue_and_expire_acme_cert(
self.master, self.clients[0], self.acme_server)
# check that the certificate issued for the client
result = self.master.run_command(
['ipa', 'cert-find', '--subject', self.clients[0].hostname]
)
assert f'CN={self.clients[0].hostname}' in result.stdout_text
# We moved time forward 90 days + 2 hours. Configure it to
# prune after an hour then run it.
self.master.run_command(
['ipa-acme-manage', 'pruning', '--enable',
'--certretention=60',
'--certretentionunit=minute',]
)
self.master.run_command(['ipactl', 'restart'])
self.master.run_command(['ipa-acme-manage', 'pruning', '--run'])
# wait for cert to get prune
time.sleep(50)
# check if client cert is removed
result = self.master.run_command(
['ipa', 'cert-find', '--subject', self.clients[0].hostname],
raiseonerr=False
)
assert f'CN={self.clients[0].hostname}' not in result.stdout_text
def test_hsm_prune_cert_cron(self, issue_and_expire_acme_cert):
"""Test to prune expired certificate by cron job"""
if (tasks.get_pki_version(self.master)
< tasks.parse_version('11.3.0')):
raise pytest.skip("Certificate pruning is not available")
issue_and_expire_acme_cert(
self.master, self.clients[0], self.acme_server)
# check that the certificate issued for the client
result = self.master.run_command(
['ipa', 'cert-find', '--subject', self.clients[0].hostname]
)
assert f'CN={self.clients[0].hostname}' in result.stdout_text
# enable pruning
self.master.run_command(['ipa-acme-manage', 'pruning', '--enable'])
# cron would be set to run the next minute
cron_minute = self.master.run_command(
[
"python3",
"-c",
(
"from datetime import datetime, timedelta; "
"print(int((datetime.now() + "
"timedelta(minutes=5)).strftime('%M')))"
),
]
).stdout_text.strip()
self.master.run_command(
['ipa-acme-manage', 'pruning',
f'--cron={cron_minute} * * * *']
)
self.master.run_command(['ipactl', 'restart'])
# wait for 5 minutes to cron to execute and 20 sec for just in case
time.sleep(320)
# check if client cert is removed
result = self.master.run_command(
['ipa', 'cert-find', '--subject', self.clients[0].hostname],
raiseonerr=False
)
assert f'CN={self.clients[0].hostname}' not in result.stdout_text
class TestHSMVault(BaseHSMTest):
"""Validate that vault works properly"""
num_clients = 1
master_with_kra = True
@classmethod
def install(cls, mh):
super(TestHSMVault, cls).install(mh)
tasks.install_client(cls.master, cls.clients[0])
def test_hsm_vault_create_and_retrieve_master(self):
vault_name = "testvault"
vault_password = "password"
vault_data = "SSBsb3ZlIENJIHRlc3RzCg=="
# create vault on master
tasks.kinit_admin(self.master)
self.master.run_command([
"ipa", "vault-add", vault_name,
"--password", vault_password,
"--type", "symmetric",
])
# archive vault
self.master.run_command([
"ipa", "vault-archive", vault_name,
"--password", vault_password,
"--data", vault_data,
])
# wait after archival
time.sleep(45)
# retrieve vault on master
self.master.run_command([
"ipa", "vault-retrieve",
vault_name,
"--password", vault_password,
])
# retrieve on client
tasks.kinit_admin(self.clients[0])
self.clients[0].run_command([
"ipa", "vault-retrieve",
vault_name,
"--password", vault_password,
])