1
0
mirror of https://github.com/samba-team/samba.git synced 2025-12-16 00:23:52 +03:00

samba-tool: Add subcommand to generate Certificate Signing Requests with SID extension

Signed-off-by: Jennifer Sutton <jennifersutton@catalyst.net.nz>
Reviewed-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Reviewed-by: Gary Lockyer <gary@catalyst.net.nz>
This commit is contained in:
Jennifer Sutton
2025-10-08 14:34:25 +13:00
committed by Douglas Bagnall
parent dbdd6952b6
commit db6f50b7cf
7 changed files with 606 additions and 0 deletions

View File

@@ -0,0 +1,266 @@
# Generate a Certificate Signing Request for a certificate
#
# Copyright (C) Catalyst.Net Ltd 2025
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import Optional
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.hazmat.primitives.serialization import (
load_der_private_key,
load_pem_private_key,
)
from cryptography.x509.base import CertificateSigningRequest
from samba import asn1, ldb
from samba.samdb import SamDB
from samba.domain.models import User
ID_PKINIT_MS_SAN = x509.ObjectIdentifier("1.3.6.1.4.1.311.20.2.3")
szOID_NTDS_CA_SECURITY_EXT = x509.ObjectIdentifier("1.3.6.1.4.1.311.25.2")
# As the version of python3-cryptography used in CI is too old to include the
# method x509.Name.from_rfc4514_string(), we must implement it ourselves.
def x509_name_from_rfc4514_string(rfc4514_string: str) -> x509.Name:
# Derived from https://datatracker.ietf.org/doc/html/rfc4514#page-7
name_oid_map = {
"CN": x509.NameOID.COMMON_NAME,
"L": x509.NameOID.LOCALITY_NAME,
"ST": x509.NameOID.STATE_OR_PROVINCE_NAME,
"O": x509.NameOID.ORGANIZATION_NAME,
"OU": x509.NameOID.ORGANIZATIONAL_UNIT_NAME,
"C": x509.NameOID.COUNTRY_NAME,
"STREET": x509.NameOID.STREET_ADDRESS,
"DC": x509.NameOID.DOMAIN_COMPONENT,
"UID": x509.NameOID.USER_ID,
}
def name_to_name_oid(name: str) -> x509.ObjectIdentifier:
try:
return name_oid_map[name]
except KeyError:
raise ValueError(f"Unknown component {name} in RFC4514 string")
try:
dn = ldb.Dn(ldb.Ldb(), rfc4514_string)
except ValueError:
raise ValueError("Unable to parse RFC4514 string as DN")
return x509.Name([
x509.RelativeDistinguishedName([
x509.NameAttribute(
name_to_name_oid(dn.get_component_name(i)), dn.get_component_value(i)
)
])
for i in reversed(range(len(dn)))
])
def get_private_key(
data: bytes, encoding: Optional[str] = None, password: Optional[str] = None
) -> RSAPrivateKey:
"""decode a key in PEM or DER format.
So far only RSA keys are supported.
"""
encoded_password = None
if password is not None:
encoded_password = password.encode("utf-8")
if encoding is None:
if data[:11] == b"-----BEGIN ":
encoding = "PEM"
else:
encoding = "DER"
encoding = encoding.upper()
# The cryptography module also supports ssh keys, PKCS1, and other formats,
# as well as non-RSA keys. It might not be wise to tolerate all of this, but
# we can do it by adding to key_fns here.
if encoding == "PEM":
key_fns = [load_pem_private_key]
elif encoding == "DER":
key_fns = [load_der_private_key]
else:
raise ValueError(
f"Private key encoding '{encoding}' not supported (try 'PEM' or 'DER')"
)
key = None
for fn in key_fns:
try:
key = fn(data, encoded_password)
break
except ValueError:
continue
except TypeError:
if password is None:
raise ValueError("No password supplied to decrypt private key")
else:
raise ValueError("Password supplied but private key isnt encrypted")
if key is None:
raise ValueError("could not decode private key")
if not isinstance(key, RSAPrivateKey):
raise ValueError(f"Currently only RSA Private Keys are supported (not '{key}')")
return key
def generate_csr(
samdb: SamDB,
user: User,
subject_name: str,
private_key_filename: str,
*,
private_key_encoding: Optional[str] = "auto",
private_key_pass: Optional[str] = None,
) -> CertificateSigningRequest:
if private_key_encoding == "auto":
private_key_encoding = None
certificate_signature = hashes.SHA256
account_name = user.account_name
if user.user_principal_name is not None:
account_upn = user.user_principal_name
else:
realm = samdb.domain_dns_name()
account_upn = f"{account_name}@{realm.lower()}"
builder = x509.CertificateSigningRequestBuilder()
# Add the subject name.
builder = builder.subject_name(x509_name_from_rfc4514_string(subject_name))
with open(private_key_filename, "rb") as private_key_file:
private_key_bytes = private_key_file.read()
private_key = get_private_key(
private_key_bytes, encoding=private_key_encoding, password=private_key_pass
)
public_key = private_key.public_key()
# Add the SubjectAlternativeName. Windows uses this to map the account
# to the certificate.
encoded_upn = account_upn.encode("utf-8")
encoded_upn = bytes([0x0C]) + asn1.asn1_length(encoded_upn) + encoded_upn
ms_upn_san = x509.OtherName(ID_PKINIT_MS_SAN, encoded_upn)
alt_names = [ms_upn_san]
builder = builder.add_extension(
x509.SubjectAlternativeName(alt_names),
critical=False,
)
builder = builder.add_extension(
x509.BasicConstraints(ca=False, path_length=None),
critical=True,
)
# The key identifier is used to identify the certificate.
subject_key_id = x509.SubjectKeyIdentifier.from_public_key(public_key)
builder = builder.add_extension(
subject_key_id,
critical=True,
)
# Add the key usages for which this certificate is valid. Windows
# doesnt actually require this extension to be present.
builder = builder.add_extension(
# Heimdal requires that the certificate be valid for digital
# signatures.
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False,
),
critical=True,
)
# Windows doesnt require this extension to be present either; but if
# it is, Windows will not accept the certificate unless either client
# authentication or smartcard logon is specified, returning
# KDC_ERR_INCONSISTENT_KEY_PURPOSE otherwise.
builder = builder.add_extension(
x509.ExtendedKeyUsage([
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH,
]),
critical=False,
)
# If the certificate predates (as ours does) the existence of the
# account that presents it Windows will refuse to accept it unless
# there exists a strong mapping from one to the other. This strong
# mapping will in this case take the form of a certificate extension
# described in [MS-WCCE] 2.2.2.7.7.4 (szOID_NTDS_CA_SECURITY_EXT) and
# containing the accounts SID.
# Encode this structure manually until we are able to produce the same
# ASN.1 encoding that Windows does.
encoded_sid = user.object_sid.encode("utf-8")
# The OCTET STRING tag, followed by length and encoded SID…
security_ext = bytes([0x04]) + asn1.asn1_length(encoded_sid) + (encoded_sid)
# …enclosed in a construct tagged with the application-specific value
# 0…
security_ext = bytes([0xA0]) + asn1.asn1_length(security_ext) + (security_ext)
# …preceded by the extension OID…
encoded_oid = bytes.fromhex("060a2b060104018237190201")
security_ext = encoded_oid + security_ext
# …and another application-specific tag 0…
# (This is the part about which Im unsure. This length is not just of
# the OID, but of the entire structure so far, as if theres some
# nesting going on. So far I havent been able to replicate this with
# pyasn1.)
security_ext = bytes([0xA0]) + asn1.asn1_length(security_ext) + (security_ext)
# …all enclosed in a structure with a SEQUENCE tag.
security_ext = bytes([0x30]) + asn1.asn1_length(security_ext) + (security_ext)
# Add the security extension to the certificate.
builder = builder.add_extension(
x509.UnrecognizedExtension(
szOID_NTDS_CA_SECURITY_EXT,
security_ext,
),
critical=False,
)
# Sign the certificate with the users private key.
return builder.sign(
private_key=private_key,
algorithm=certificate_signature(),
backend=default_backend(),
)