extend test script to cover upn testing
This commit is intended to extend existing `generate_auth_certificate`
python script that automatically test the auth functionality of bmcweb.
We extend the script by adding test for UserPrincipalName (UPN) feature.
This feature[1] allow us to use SubjectAlternativeName (SAN) extension
on X509 certificate and enable us to use the different name as username.
Previously we can only use CommonName in the certificate as username
By adding this changes, we can test the UPN feature easily using the
script. We add a new flag that enable the user to test using UPN feature
by specifying the UPN name to be tested.
UPN has OID that is specified by Microsoft[2]. The full OID path:
1 ISO
1.3 identified-organization (ISO/IEC 6523),
1.3.6 DoD,
1.3.6.1 internet,
1.3.6.1.4 private,
1.3.6.1.4.1 enterprise,
1.3.6.1.4.1.311 Microsoft,
1.3.6.1.4.1.311.20 Microsoft enrollment infrastructure,
1.3.6.1.4.1.311.20.2 Certificate Type Extension,
1.3.6.1.4.1.311.20.2.3 UserPrincipalName
Tested:
- Regress test on CommonName by running without `--upn` flag
- Test using correct UPN name
- There are two requirements for the UPN name (`username@domain`)
- `username` must exist in the BMC device accounts
- `domain` must match the domain forest of the device
- eg: malik@fb.com match macbmc1.abc.fb.com
- Test using incorrect UPN name
- Violate one of the requirements and the test should fail
[1] Patch feature: https://gerrit.openbmc.org/c/openbmc/bmcweb/+/78519
[2] OID of UPN: https://oidref.com/1.3.6.1.4.1.311.20.2.3
Change-Id: I997bea9a6662fa41c3824fde71ea4f20b606ca9c
Signed-off-by: Malik Akbar Hashemi Rafsanjani <malikrafsan@meta.com>
diff --git a/scripts/generate_auth_certificates.py b/scripts/generate_auth_certificates.py
index ebbad18..e20103b 100755
--- a/scripts/generate_auth_certificates.py
+++ b/scripts/generate_auth_certificates.py
@@ -12,7 +12,9 @@
import os
import socket
import time
+from typing import Optional
+import asn1
import httpx
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
@@ -24,6 +26,8 @@
from cryptography.x509.oid import NameOID
replaceCertPath = "/redfish/v1/CertificateService/Actions/CertificateService.ReplaceCertificate"
+# https://oidref.com/1.3.6.1.4.1.311.20.2.3
+upnObjectIdentifier = "1.3.6.1.4.1.311.20.2.3"
class RedfishSessionContext:
@@ -56,6 +60,29 @@
r.raise_for_status()
+def get_hostname(redfish_session, username, password, url):
+ service_root = redfish_session.get("/redfish/v1/")
+ service_root.raise_for_status()
+
+ manager_uri = service_root.json()["Links"]["ManagerProvidingService"][
+ "@odata.id"
+ ]
+
+ manager_response = redfish_session.get(manager_uri)
+ manager_response.raise_for_status()
+
+ network_protocol_uri = manager_response.json()["NetworkProtocol"][
+ "@odata.id"
+ ]
+
+ network_protocol_response = redfish_session.get(network_protocol_uri)
+ network_protocol_response.raise_for_status()
+
+ hostname = network_protocol_response.json()["HostName"]
+
+ return hostname
+
+
def generateCA():
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
@@ -112,25 +139,27 @@
return
-def generate_client_key_and_cert(commonName, ca_cert, ca_key):
+def generate_client_key_and_cert(
+ ca_cert,
+ ca_key,
+ common_name: Optional[str] = None,
+ upn: Optional[str] = None,
+):
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
builder = x509.CertificateBuilder()
- builder = builder.subject_name(
- x509.Name(
- [
- x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
- x509.NameAttribute(
- NameOID.STATE_OR_PROVINCE_NAME, "California"
- ),
- x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
- x509.NameAttribute(NameOID.ORGANIZATION_NAME, "OpenBMC"),
- x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "bmcweb"),
- x509.NameAttribute(NameOID.COMMON_NAME, commonName),
- ]
- )
- )
+ cert_names = [
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
+ x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
+ x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "OpenBMC"),
+ x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "bmcweb"),
+ ]
+ if common_name is not None:
+ cert_names.append(x509.NameAttribute(NameOID.COMMON_NAME, common_name))
+
+ builder = builder.subject_name(x509.Name(cert_names))
builder = builder.issuer_name(ca_cert.subject)
builder = builder.public_key(public_key)
@@ -161,6 +190,23 @@
auth_key = x509.AuthorityKeyIdentifier.from_issuer_public_key(public_key)
builder = builder.add_extension(auth_key, critical=False)
+ if upn is not None:
+ encoder = asn1.Encoder()
+ encoder.start()
+ encoder.write(upn, asn1.Numbers.UTF8String)
+
+ builder = builder.add_extension(
+ x509.SubjectAlternativeName(
+ [
+ x509.OtherName(
+ x509.ObjectIdentifier(upnObjectIdentifier),
+ encoder.output(),
+ )
+ ]
+ ),
+ critical=False,
+ )
+
signed = builder.sign(private_key=ca_key, algorithm=hashes.SHA256())
return private_key, signed
@@ -337,15 +383,61 @@
def test_mtls_auth(url, certs_dir):
- response = httpx.get(
- f"https://{url}/redfish/v1/SessionService/Sessions",
+ with httpx.Client(
+ base_url=f"https://{url}",
verify=os.path.join(certs_dir, "CA-cert.cer"),
cert=(
os.path.join(certs_dir, "client-cert.pem"),
os.path.join(certs_dir, "client-key.pem"),
),
- )
- response.raise_for_status()
+ ) as client:
+ print("Testing mTLS auth with CommonName")
+ response = client.get(
+ "/redfish/v1/SessionService/Sessions",
+ )
+ response.raise_for_status()
+
+ print("Changing CertificateMappingAttribute to use UPN")
+ patch_json = {
+ "MultiFactorAuth": {
+ "ClientCertificate": {
+ "CertificateMappingAttribute": "UserPrincipalName"
+ }
+ }
+ }
+ response = client.patch(
+ "/redfish/v1/AccountService",
+ json=patch_json,
+ )
+ response.raise_for_status()
+
+ with httpx.Client(
+ base_url=f"https://{url}",
+ verify=os.path.join(certs_dir, "CA-cert.cer"),
+ cert=(
+ os.path.join(certs_dir, "upn-client-cert.pem"),
+ os.path.join(certs_dir, "upn-client-key.pem"),
+ ),
+ ) as client:
+ print("Retesting mTLS auth with UPN")
+ response = client.get(
+ "/redfish/v1/SessionService/Sessions",
+ )
+ response.raise_for_status()
+
+ print("Changing CertificateMappingAttribute to use CommonName")
+ patch_json = {
+ "MultiFactorAuth": {
+ "ClientCertificate": {
+ "CertificateMappingAttribute": "CommonName"
+ }
+ }
+ }
+ response = client.patch(
+ "/redfish/v1/AccountService",
+ json=patch_json,
+ )
+ response.raise_for_status()
def setup_server_cert(
@@ -389,6 +481,17 @@
install_server_cert(redfish_session, manager_uri, server_cert_dump)
+ print("Make sure setting CertificateMappingAttribute to CommonName")
+ patch_json = {
+ "MultiFactorAuth": {
+ "ClientCertificate": {"CertificateMappingAttribute": "CommonName"}
+ }
+ }
+ response = redfish_session.patch(
+ "/redfish/v1/AccountService", json=patch_json
+ )
+ response.raise_for_status()
+
def generate_and_load_certs(url, username, password):
certs_dir = os.path.expanduser("~/certs")
@@ -430,7 +533,7 @@
ca_key = load_pem_private_key(ca_key_dump, None)
client_key, client_cert = generate_client_key_and_cert(
- username, ca_cert, ca_key
+ ca_cert, ca_key, common_name=username
)
client_key_dump = client_key.private_bytes(
encoding=serialization.Encoding.PEM,
@@ -457,6 +560,35 @@
redfish_session, username, password
) as rf_session:
redfish_session.headers["X-Auth-Token"] = rf_session.x_auth_token
+
+ hostname = get_hostname(redfish_session, username, password, url)
+ print(f"Hostname: {hostname}")
+
+ upn_client_key, upn_client_cert = generate_client_key_and_cert(
+ ca_cert,
+ ca_key,
+ upn=f"{username}@{hostname}",
+ )
+ upn_client_key_dump = upn_client_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption(),
+ )
+ with open(
+ os.path.join(certs_dir, "upn-client-key.pem"), "wb"
+ ) as f:
+ f.write(upn_client_key_dump)
+ print("UPN client key generated.")
+
+ upn_client_cert_dump = upn_client_cert.public_bytes(
+ encoding=serialization.Encoding.PEM
+ )
+ with open(
+ os.path.join(certs_dir, "upn-client-cert.pem"), "wb"
+ ) as f:
+ f.write(upn_client_cert_dump)
+ print("UPN client cert generated.")
+
setup_server_cert(
redfish_session,
ca_cert_dump,
@@ -470,7 +602,6 @@
)
print("Testing redfish TLS authentication with generated certs.")
-
time.sleep(2)
test_mtls_auth(url, certs_dir)
print("Redfish TLS authentication success!")