The branch, master has been updated
via d3aa479f962 docs-xml: Document samba-tool subcommand to generate
Certificate Signing Requests
via db6f50b7cff samba-tool: Add subcommand to generate Certificate
Signing Requests with SID extension
via dbdd6952b6e python: Factor out asn.1 methods into their own module
via 4c5f77ded68 samba-tool: Fix comments
via 34431b0d4fa python:tests: Fix code spelling
from 9bbdfee7f2f vfs_recycle: Make recycle:touch/touch_mtime work again
if recycle:keeptree is set
https://git.samba.org/?p=samba.git;a=shortlog;h=master
- Log -----------------------------------------------------------------
commit d3aa479f962574c56f754bc407763f9a7ec9778c
Author: Jennifer Sutton <[email protected]>
Date: Mon Nov 3 16:50:52 2025 +1300
docs-xml: Document samba-tool subcommand to generate Certificate Signing
Requests
Signed-off-by: Jennifer Sutton <[email protected]>
Reviewed-by: Douglas Bagnall <[email protected]>
Reviewed-by: Gary Lockyer <[email protected]>
Autobuild-User(master): Douglas Bagnall <[email protected]>
Autobuild-Date(master): Wed Nov 5 05:13:01 UTC 2025 on atb-devel-224
commit db6f50b7cff616b4d67ec62dab6e0208f5cbb79d
Author: Jennifer Sutton <[email protected]>
Date: Wed Oct 8 14:34:25 2025 +1300
samba-tool: Add subcommand to generate Certificate Signing Requests with
SID extension
Signed-off-by: Jennifer Sutton <[email protected]>
Reviewed-by: Douglas Bagnall <[email protected]>
Reviewed-by: Gary Lockyer <[email protected]>
commit dbdd6952b6eed586e502d7c26af0b3e233e188a9
Author: Jennifer Sutton <[email protected]>
Date: Mon Nov 3 10:45:44 2025 +1300
python: Factor out asn.1 methods into their own module
Signed-off-by: Jennifer Sutton <[email protected]>
Reviewed-by: Douglas Bagnall <[email protected]>
Reviewed-by: Gary Lockyer <[email protected]>
commit 4c5f77ded680b58ddb4078636331aba29a1c72c1
Author: Jennifer Sutton <[email protected]>
Date: Wed Oct 8 10:58:53 2025 +1300
samba-tool: Fix comments
Signed-off-by: Jennifer Sutton <[email protected]>
Reviewed-by: Douglas Bagnall <[email protected]>
Reviewed-by: Gary Lockyer <[email protected]>
commit 34431b0d4fad2ff38a0fde6b3b18e67c0d4e7342
Author: Jennifer Sutton <[email protected]>
Date: Fri Oct 24 12:25:15 2025 +1300
python:tests: Fix code spelling
Signed-off-by: Jennifer Sutton <[email protected]>
Reviewed-by: Douglas Bagnall <[email protected]>
Reviewed-by: Gary Lockyer <[email protected]>
-----------------------------------------------------------------------
Summary of changes:
docs-xml/manpages/samba-tool.8.xml | 40 ++++
python/samba/asn1.py | 103 ++++++++
python/samba/generate_csr.py | 266 +++++++++++++++++++++
python/samba/netcmd/computer.py | 2 +
python/samba/netcmd/computer_generate_csr.py | 93 +++++++
python/samba/netcmd/computer_keytrust.py | 2 +-
python/samba/netcmd/user/__init__.py | 2 +
python/samba/netcmd/user/generate_csr.py | 88 +++++++
python/samba/netcmd/user/keytrust.py | 2 +-
.../tests/krb5/pkinit_certificate_mapping_tests.py | 9 +-
python/samba/tests/krb5/pkinit_tests.py | 10 +-
python/samba/tests/krb5/raw_testcase.py | 74 +-----
python/samba/tests/samba_tool/user_generate_csr.py | 154 ++++++++++++
python/samba/tests/samba_tool/user_keytrust.py | 2 +-
source4/selftest/tests.py | 1 +
15 files changed, 766 insertions(+), 82 deletions(-)
create mode 100644 python/samba/asn1.py
create mode 100644 python/samba/generate_csr.py
create mode 100644 python/samba/netcmd/computer_generate_csr.py
create mode 100644 python/samba/netcmd/user/generate_csr.py
create mode 100644 python/samba/tests/samba_tool/user_generate_csr.py
Changeset truncated at 500 lines:
diff --git a/docs-xml/manpages/samba-tool.8.xml
b/docs-xml/manpages/samba-tool.8.xml
index b27b168f471..9f6deee342e 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -384,6 +384,26 @@ The <constant>--verbose</constant> includes more, probably
useless, information.
</variablelist>
</refsect3>
+<refsect3>
+ <title>computer generate-csr <replaceable>computername</replaceable>
+ <replaceable>subject_name</replaceable>
+ <replaceable>private_key_filename</replaceable>
+ <replaceable>output_filename</replaceable> [options]</title>
+ <para>Generate a PEM‐encoded Certificate Signing Request for a
computer.</para>
+
+ <variablelist>
+ <!--Options-->
+ <varlistentry>
+ <term>--private-key-encoding</term>
+ <listitem><para>Specify which encoding the private key uses.
Default is 'auto'.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>--private-key-pass</term>
+ <listitem><para>Provide a password to decrypt the private
key.</para></listitem>
+ </varlistentry>
+ </variablelist>
+</refsect3>
+
<refsect2>
<title>contact</title>
@@ -4482,6 +4502,26 @@ The <constant>--verbose</constant> includes more,
probably useless, information.
</variablelist>
</refsect3>
+<refsect3>
+ <title>user generate-csr <replaceable>username</replaceable>
+ <replaceable>subject_name</replaceable>
+ <replaceable>private_key_filename</replaceable>
+ <replaceable>output_filename</replaceable> [options]</title>
+ <para>Generate a PEM‐encoded Certificate Signing Request for a
user.</para>
+
+ <variablelist>
+ <!--Options-->
+ <varlistentry>
+ <term>--private-key-encoding</term>
+ <listitem><para>Specify which encoding the private key uses.
Default is 'auto'.</para></listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>--private-key-pass</term>
+ <listitem><para>Provide a password to decrypt the private
key.</para></listitem>
+ </varlistentry>
+ </variablelist>
+</refsect3>
+
<refsect2>
<title>vampire [options] <replaceable>domain</replaceable></title>
diff --git a/python/samba/asn1.py b/python/samba/asn1.py
new file mode 100644
index 00000000000..c15fa07add4
--- /dev/null
+++ b/python/samba/asn1.py
@@ -0,0 +1,103 @@
+# Unix SMB/CIFS implementation.
+# 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/>.
+#
+
+"""ASN.1 module"""
+
+import math
+from typing import Optional
+
+
+class Asn1Error(Exception):
+ pass
+
+
+def length_in_bytes(value: int) -> int:
+ """Return the length in bytes of an integer once it is encoded as
+ bytes."""
+
+ if value < 0:
+ raise Asn1Error("value must be positive")
+ if not isinstance(value, int):
+ raise Asn1Error("value must be an integer")
+
+ length_in_bits = max(1, math.log2(value + 1))
+ length_in_bytes = math.ceil(length_in_bits / 8)
+ return length_in_bytes
+
+
+def bytes_from_int(value: int, *, length: Optional[int] = None) -> bytes:
+ """Return an integer encoded big-endian into bytes of an optionally
+ specified length.
+ """
+ if length is None:
+ length = length_in_bytes(value)
+ return value.to_bytes(length, "big")
+
+
+def int_from_bytes(data: bytes) -> int:
+ """Return an integer decoded from bytes in big-endian format."""
+ return int.from_bytes(data, "big")
+
+
+def int_from_bit_string(string: str) -> int:
+ """Return an integer decoded from a bitstring."""
+ return int(string, base=2)
+
+
+def bit_string_from_int(value: int) -> str:
+ """Return a bitstring encoding of an integer."""
+
+ string = f"{value:b}"
+
+ # The bitstring must be padded to a multiple of 8 bits in length, or
+ # pyasn1 will interpret it incorrectly (as if the padding bits were
+ # present, but on the wrong end).
+ length = len(string)
+ padding_len = math.ceil(length / 8) * 8 - length
+ return "0" * padding_len + string
+
+
+def bit_string_from_bytes(data: bytes) -> str:
+ """Return a bitstring encoding of bytes in big-endian format."""
+ value = int_from_bytes(data)
+ return bit_string_from_int(value)
+
+
+def bytes_from_bit_string(string: str) -> bytes:
+ """Return big-endian format bytes encoded from a bitstring."""
+ value = int_from_bit_string(string)
+ length = math.ceil(len(string) / 8)
+ return value.to_bytes(length, "big")
+
+
+def asn1_length(data: bytes) -> bytes:
+ """Return the ASN.1 encoding of the length of some data."""
+
+ length = len(data)
+
+ if length <= 0:
+ raise Asn1Error("length must be greater than zero")
+ if length < 0x80:
+ return bytes([length])
+
+ encoding_len = length_in_bytes(length)
+ if encoding_len >= 0x80:
+ raise Asn1Error("item is too long to be ASN.1 encoded")
+
+ data = bytes_from_int(length, length=encoding_len)
+ return bytes([0x80 | encoding_len]) + data
diff --git a/python/samba/generate_csr.py b/python/samba/generate_csr.py
new file mode 100644
index 00000000000..a486e844b93
--- /dev/null
+++ b/python/samba/generate_csr.py
@@ -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 isn’t
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
+ # doesn’t 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 doesn’t 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 account’s 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 I’m unsure. This length is not just of
+ # the OID, but of the entire structure so far, as if there’s some
+ # nesting going on. So far I haven’t 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 user’s private key.
+ return builder.sign(
+ private_key=private_key,
+ algorithm=certificate_signature(),
+ backend=default_backend(),
+ )
diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index cd5389cf8ec..39f6c627039 100644
--- a/python/samba/netcmd/computer.py
+++ b/python/samba/netcmd/computer.py
@@ -36,6 +36,7 @@ from samba.samdb import SamDB
from samba.common import get_bytes
from subprocess import check_call, CalledProcessError
from . import common
+from .computer_generate_csr import cmd_computer_generate_csr
from .computer_keytrust import cmd_computer_keytrust
@@ -723,6 +724,7 @@ class cmd_computer(SuperCommand):
subcommands["create"] = cmd_computer_add()
subcommands["delete"] = cmd_computer_delete()
subcommands["edit"] = cmd_computer_edit()
+ subcommands["generate-csr"] = cmd_computer_generate_csr()
subcommands["list"] = cmd_computer_list()
subcommands["show"] = cmd_computer_show()
subcommands["move"] = cmd_computer_move()
diff --git a/python/samba/netcmd/computer_generate_csr.py
b/python/samba/netcmd/computer_generate_csr.py
new file mode 100644
index 00000000000..094002e359d
--- /dev/null
+++ b/python/samba/netcmd/computer_generate_csr.py
@@ -0,0 +1,93 @@
+# samba-tool commands to generate a Certificate Signing Request for a
computer’s
+# 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.hazmat.primitives import serialization
+import samba.getopt as options
+from samba.netcmd import Command, Option
+from samba.netcmd import exception_to_command_error
+
+from samba.domain.models import Computer
+from samba.domain.models.exceptions import ModelError
+from samba.generate_csr import generate_csr
+
+
+class cmd_computer_generate_csr(Command):
+ """Generate a PEM‐encoded Certificate Signing Request for a computer."""
+
+ synopsis = "%prog <computername> <subject_name> <private_key_filename>
<output_filename> [options]"
+
--
Samba Shared Repository