The branch, master has been updated
       via  246ce57e52e pytest:samba-tool group: remove unused imports
       via  624a8c2261c pytest: run user_keytrust tests as computer keytrust 
tests
       via  5030dd33176 pytest: adapt user_keytrust tests to be objectclass 
agnostic
       via  8ed39fa33f9 samba-tool: copy user_keytrust to computer keytrust
       via  16d670f0a52 samba-tool computer: remove unused imports
       via  2681fe5df87 samba-tool: add user keytrust command
       via  625cabf6514 samba-tool: Command.message() can print multiple lines
       via  3ca754d8f25 py:key_credential_link: filter_kcl_list helper for 
samba-tool
       via  df0cf2556f3 py:key_credential_list: add kcl_in_list function
       via  87caac906e7 py:key_credential_links: allow encoding=='auto'
       via  7c08990a455 samba-tool: add verbose flag to 
@exception_to_command_error
       via  93391259df8 py:tests: test key_credential_link module
       via  3682667439a python:key_credential_link: add descriptive methods
       via  439146c7a0f python:models: do not re-use mutable defaults
       via  2797c013e34 samba-tool: add decorator to catch exception types
       via  0ff4d9e881c man samba-tool: computer keytrust
       via  9322a71a4fd man samba-tool: user keytrust
       via  ab9988b80d7 man samba-tool: don't suggest non-existent option in 
synopsis.
      from  affb734a256 tdbtorture: Fix CID 1034816: proper calloc usage

https://git.samba.org/?p=samba.git;a=shortlog;h=master


- Log -----------------------------------------------------------------
commit 246ce57e52e76b3e4b190a6b93309b3a8b938dde
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Fri Aug 1 16:25:13 2025 +1200

    pytest:samba-tool group: remove unused imports
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>
    
    Autobuild-User(master): Douglas Bagnall <dbagn...@samba.org>
    Autobuild-Date(master): Wed Aug 20 05:35:03 UTC 2025 on atb-devel-224

commit 624a8c2261cfcb84e0080b19c2a6bb48f8c40750
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Sun Aug 17 09:59:07 2025 +0000

    pytest: run user_keytrust tests as computer keytrust tests
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 5030dd33176cf1770637814120f69699f87ab03c
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Sun Aug 17 09:58:39 2025 +0000

    pytest: adapt user_keytrust tests to be objectclass agnostic
    
    We will reuse the tests for the computer keytrust command.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 8ed39fa33f9dd917aae291ce6aad222d95654ec1
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Sun Aug 17 09:57:55 2025 +0000

    samba-tool: copy user_keytrust to computer keytrust
    
    This is exactly a copy of user/keytrust.py to computer_keytrust.py
    with a title-case-preserving `s/user/computer/`.
    
    It works. The Computer model differs from the User model in that it
    appends a '$' to the end of account names if it senses the lack,
    otherwise these commands are using the same code paths.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 16d670f0a52ae8d78188af0c741b79175d7169ff
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Sun Aug 17 20:39:38 2025 +1200

    samba-tool computer: remove unused imports
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 2681fe5df87db07b080e12fa7aeaaea0a0518546
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Wed Aug 6 14:01:14 2025 +1200

    samba-tool: add user keytrust command
    
    This allows manipulation of key credential links for users.
    
    See `man -l bin/default/docs-xml/manpages/samba-tool.8` for
    documentation.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 625cabf65140ee2c79b0a89c483edd071d58a4f4
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Wed Aug 13 16:21:14 2025 +1200

    samba-tool: Command.message() can print multiple lines
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 3ca754d8f25c0753d02859a97d7f2664a8b46462
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Sun Aug 17 08:34:57 2025 +0000

    py:key_credential_link: filter_kcl_list helper for samba-tool
    
    This will be used in `samba-tool user keytrust delete` and `samba-tool
    computer keytrust delete` and is mainly to deduplicate that code.
    
    Potentially it could also be used in `keytrust view`.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit df0cf2556f39ff5062e052ae020fb031c76fd222
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Fri Aug 15 17:36:11 2025 +1200

    py:key_credential_list: add kcl_in_list function
    
    This compares the key material and DN of a KeyCredentialLinkDn with a
    list of others, which is a different sense of equality than the
    default (which considers GUIDs and binary equality).
    
    This will be used by samba-tool to check whether a link is in fact a
    duplicate even if it seems not to be due to some insignificant field
    being non-identical.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 87caac906e733e15ba3268db99f101bd1a93d9a1
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Fri Aug 15 17:35:33 2025 +1200

    py:key_credential_links: allow encoding=='auto'
    
    'auto' is the same as None. This is helpful to samba-tool.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 7c08990a45540a83695e7759af0c72b77a90c2d5
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Thu Aug 14 10:33:00 2025 +1200

    samba-tool: add verbose flag to @exception_to_command_error
    
    Helpful in development.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 93391259df86b156b429c95f0d8748dfb0862d44
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Fri Aug 8 15:04:51 2025 +1200

    py:tests: test key_credential_link module
    
    These tests use the samba.key_credential_link module and a real samdb.
    
    The existing key_credential_link tests address the IDL generated
    structures more directly.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 3682667439a2c81b8bd08678387d9ee43ec54e18
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Wed Jun 11 16:16:54 2025 +1200

    python:key_credential_link: add descriptive methods
    
    In samba-tool we are going to want a KeyCredentialLinkDn to be able
    to describe itself. We're adding the methods here because
    `samba-tool user` and `samba-tool computer` will both want to use
    them.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 439146c7a0ff362e2247feb94f2228edccea36be
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Wed Aug 13 17:19:16 2025 +1200

    python:models: do not re-use mutable defaults
    
    This ensures that model.save works when a field has the many flag set,
    but the object has no attribute of that name, and the caller appends
    to the attribute list, like this:
    
          user.key_credential_link.append(link)
    
    When we get to save, and are doing this:
    
          value = getattr(self, attr)
          old_value = getattr(existing_obj, attr)
          if value != old_value:
             # commit the change
    
    the .append() will have added the item to both value and old_value
    because they are the same list. But not any more.
    
    This was a problem because the Field instance is attached to the
    model class, not the model instance.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 2797c013e34953739e761ed56ea30b1f3f6c817d
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Sat Aug 9 16:27:42 2025 +1200

    samba-tool: add decorator to catch exception types
    
    Often we [think we] know that all exceptions of a certain type should
    be formatted as CommandErrors (i.e., the traceback is suppressed, and
    the message is assumed intelligible). Rather than riddling .run() with
    try...except blocks to do this, we can
    
    @exception_to_command_error(ModelError)
    def run(...)
    
    which makes any ModelError into a CommandError in that samba-tool command.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 0ff4d9e881cd0698247de99082981e8d0202157d
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Mon Aug 18 21:02:57 2025 +1200

    man samba-tool: computer keytrust
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit 9322a71a4fdc781a79292158f770cadfedc60abb
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Wed Jun 11 14:20:51 2025 +1200

    man samba-tool: user keytrust
    
    This documentation anticipates changes that will occur over the next
    ~20 commits.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

commit ab9988b80d79743babcaf7afe3b9d4283005f312
Author: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
Date:   Mon Aug 18 20:56:04 2025 +1200

    man samba-tool: don't suggest non-existent option in synopsis.
    
    Signed-off-by: Douglas Bagnall <douglas.bagn...@catalyst.net.nz>
    Reviewed-by: Gary Lockyer <g...@catalyst.net.nz>

-----------------------------------------------------------------------

Summary of changes:
 docs-xml/manpages/samba-tool.8.xml                 | 184 +++++++++-
 python/samba/domain/models/fields.py               |   2 +-
 python/samba/key_credential_link.py                | 171 ++++++++-
 python/samba/netcmd/__init__.py                    |  41 ++-
 python/samba/netcmd/computer.py                    |   8 +-
 python/samba/netcmd/computer_keytrust.py           | 223 ++++++++++++
 python/samba/netcmd/user/__init__.py               |   2 +
 python/samba/netcmd/user/keytrust.py               | 223 ++++++++++++
 python/samba/tests/key_credential_link_samdb.py    | 307 +++++++++++++++++
 python/samba/tests/samba_tool/group.py             |   5 -
 python/samba/tests/samba_tool/user_keytrust.py     | 382 +++++++++++++++++++++
 source4/selftest/tests.py                          |   2 +
 .../keytrust/ca-cert-ecdsa-p256.pem                |   0
 .../keytrust/ca-cert-rsa-2048.pem                  |   0
 .../keytrust/ca-cert-rsa-4096.pem                  |   0
 .../keytrust/cert-rsa-1024.pem                     |   0
 .../keytrust/cert-rsa-2048.pem                     |   0
 .../keytrust/cert-rsa-2048b.pem                    |   0
 .../public-key-from-cert-rsa-2048-pkcs1.pem        |   8 +
 testdata/keytrust/rsa2048-pkcs1.der                | Bin 0 -> 270 bytes
 testdata/keytrust/rsa2048b-spki.pem                |   9 +
 21 files changed, 1550 insertions(+), 17 deletions(-)
 create mode 100644 python/samba/netcmd/computer_keytrust.py
 create mode 100644 python/samba/netcmd/user/keytrust.py
 create mode 100755 python/samba/tests/key_credential_link_samdb.py
 create mode 100644 python/samba/tests/samba_tool/user_keytrust.py
 copy third_party/heimdal/lib/hx509/data/secp256r1TestCA.cert.pem => 
testdata/keytrust/ca-cert-ecdsa-p256.pem (100%)
 copy third_party/heimdal/lib/hx509/data/j.pem => 
testdata/keytrust/ca-cert-rsa-2048.pem (100%)
 copy third_party/heimdal/lib/hx509/data/ca.crt => 
testdata/keytrust/ca-cert-rsa-4096.pem (100%)
 copy third_party/heimdal/lib/hx509/data/yutaka-pad-ok-cert.pem => 
testdata/keytrust/cert-rsa-1024.pem (100%)
 copy third_party/heimdal/lib/hx509/data/tcg-ek-cp.pem => 
testdata/keytrust/cert-rsa-2048.pem (100%)
 copy third_party/heimdal/lib/hx509/data/tcg-devid.pem => 
testdata/keytrust/cert-rsa-2048b.pem (100%)
 create mode 100644 testdata/keytrust/public-key-from-cert-rsa-2048-pkcs1.pem
 create mode 100644 testdata/keytrust/rsa2048-pkcs1.der
 create mode 100644 testdata/keytrust/rsa2048b-spki.pem


Changeset truncated at 500 lines:

diff --git a/docs-xml/manpages/samba-tool.8.xml 
b/docs-xml/manpages/samba-tool.8.xml
index d0bbc30c9e6..b27b168f471 100644
--- a/docs-xml/manpages/samba-tool.8.xml
+++ b/docs-xml/manpages/samba-tool.8.xml
@@ -24,7 +24,7 @@
                <arg choice="opt">-W myworkgroup</arg>
                <arg choice="opt">-U user</arg>
                <arg choice="opt">-d debuglevel</arg>
-               <arg choice="opt">--v</arg>
+               <arg choice="opt">-V</arg>
        </cmdsynopsis>
 </refsynopsisdiv>
 
@@ -295,6 +295,96 @@
        </variablelist>
 </refsect3>
 
+<refsect2>
+       <title>computer keytrust</title>
+       <para>Manage Key Credential Links for a computer.</para>
+       <para>This can populate, describe or delete  msDS-KeyCredentialLink 
attributes.</para>
+</refsect2>
+
+
+<refsect3>
+<title>computer keytrust add <replaceable>computername</replaceable>  
<replaceable>public-key-or-certificate</replaceable>[options]</title>
+<para>Add a key-credential-link, which is a linked attribute that holds a 
public key in a binary field.
+</para>
+<para>
+  The second argument is a filename that should refer to a 2048 bit RSA key 
(or a certificate containing that key) in PEM or DER format. By default the 
encoding format will be detected automatically, but you can attempt to override 
this with <constant>--encoding</constant> option. Other types of public key are 
not supported, though the <constant>--force</constant> option can be used to 
add a non-2048 bit key.
+</para>
+
+<variablelist>
+<!--Options-->
+         <varlistentry>
+           <term>--link-target=DN</term>
+           <listitem><para>link to this DN (default: the computer's 
DN)</para></listitem>
+         </varlistentry>
+         <varlistentry>
+           <term>--encoding=ENCODING</term>
+           <listitem><para>Key format, either <constant>pem</constant>,  
<constant>der</constant>, or  <constant>auto</constant>. The default is 
<constant>auto</constant>, which is likely to detect the correct format in all 
circumstances.</para></listitem>
+         </varlistentry>
+         <varlistentry>
+           <term>--force</term>
+           <listitem><para>proceed with operations that seems 
ill-fated</para></listitem>
+         </varlistentry>
+</variablelist>
+</refsect3>
+
+<refsect3>
+
+<title>computer keytrust delete <replaceable>computername</replaceable> 
[options]</title>
+<para>Delete a key-credential-link.
+</para>
+<para>The link to be deleted can be selected in a number of ways. 
<constant>--all</constant> will delete all key credential links for the 
computer (often there will only be one). The  
<constant>--link-target</constant> option selects a key credential link based 
on the DN targeted by the link. The <constant>--fingerprint</constant> option 
selects a link to delete based on the key fingerprint. This is the SHA256 of 
the DER-encoded  key material, expressed as hex-pairs separated by colons. See 
<constant>computer keytrust view</constant> to get a list of links and their 
fingerprints.
+</para>
+
+<para>If more than one of <constant>--link-target</constant>, 
<constant>--fingerprint</constant>, and <constant>--all</constant> are used, 
links matched by any of them will be deleted.
+</para>
+
+<para>The <constant>--dry-run</constant> option will prevent links from being 
deleted, and instead indicate what would happen if it was omitted.
+</para>
+
+<variablelist>
+<!--Options-->
+         <varlistentry>
+           <term>--link-target=DN</term>
+           <listitem><para>Delete this key credential link (a 
DN)</para></listitem>
+         </varlistentry>
+         <varlistentry>
+           <term>--fingerprint=HH:HH:..</term>
+           <listitem><para>Delete the key credential link with this key 
fingerprint</para></listitem>
+         </varlistentry>
+         <varlistentry>
+           <term>--all</term>
+           <listitem><para>Delete all key credential links</para></listitem>
+         </varlistentry>
+         <varlistentry>
+           <term>-n, --dry-run</term>
+           <listitem><para>Do nothing but print what would 
happen</para></listitem>
+         </varlistentry>
+</variablelist>
+</refsect3>
+
+<refsect3>
+
+<title>computer keytrust view <replaceable>computername</replaceable> 
[options]</title>
+
+<para>View a computer's key credential links. This can be used to find a 
link's fingerprint and target DN for <title>computer keytrust delete</title>.
+
+The <constant>--verbose</constant> includes more, probably useless, 
information.
+</para>
+
+<variablelist>
+<!--Options-->
+         <varlistentry>
+           <term>-h, --help</term>
+           <listitem><para>show this help message and exit</para></listitem>
+         </varlistentry>
+         <varlistentry>
+           <term>-v, --verbose</term>
+           <listitem><para>Be verbose</para></listitem>
+         </varlistentry>
+</variablelist>
+</refsect3>
+
+
 <refsect2>
        <title>contact</title>
        <para>Manage contacts.</para>
@@ -621,7 +711,7 @@
        return code will not indicate error. </para></listitem>
        </varlistentry>
        </variablelist>
-       
+
 </refsect3>
 
 
@@ -4303,6 +4393,96 @@ in use.</para>
        <para>View the assigned authentication silo for user.</para>
 </refsect3>
 
+<refsect2>
+       <title>user keytrust</title>
+       <para>Manage Key Credential Links for a user.</para>
+       <para>This can populate, describe or delete  msDS-KeyCredentialLink 
attributes.</para>
+</refsect2>
+
+
+<refsect3>
+<title>user keytrust add <replaceable>username</replaceable>  
<replaceable>public-key-or-certificate</replaceable>[options]</title>
+<para>Add a key-credential-link, which is a linked attribute that holds a 
public key in a binary field.
+</para>
+<para>
+  The second argument is a filename that should refer to a 2048 bit RSA key 
(or a certificate containing that key) in PEM or DER format. By default the 
encoding format will be detected automatically, but you can attempt to override 
this with <constant>--encoding</constant> option. Other types of public key are 
not supported, though the <constant>--force</constant> option can be used to 
add a non-2048 bit key.
+</para>
+
+<variablelist>
+<!--Options-->
+         <varlistentry>
+           <term>--link-target=DN</term>
+           <listitem><para>link to this DN (default: the user's 
DN)</para></listitem>
+         </varlistentry>
+         <varlistentry>
+           <term>--encoding=ENCODING</term>
+           <listitem><para>Key format, either <constant>pem</constant>,  
<constant>der</constant>, or  <constant>auto</constant>. The default is  
<constant>auto</constant>, which is likely to detect the correct format in all 
circumstances.</para></listitem>
+         </varlistentry>
+         <varlistentry>
+           <term>--force</term>
+           <listitem><para>proceed with operations that seems 
ill-fated</para></listitem>
+         </varlistentry>
+</variablelist>
+</refsect3>
+
+<refsect3>
+
+<title>user keytrust delete <replaceable>username</replaceable> 
[options]</title>
+<para>Delete a key-credential-link.
+</para>
+<para>The link to be deleted can be selected in a number of ways. 
<constant>--all</constant> will delete all key credential links for the user 
(often there will only be one). The  <constant>--link-target</constant> option 
selects a key credential link based on the DN targeted by the link. The 
<constant>--fingerprint</constant> option selects a link to delete based on the 
key fingerprint. This is the SHA256 of the DER-encoded  key material, expressed 
as hex-pairs separated by colons. See <constant>user keytrust view</constant> 
to get a list of links and their fingerprints.
+</para>
+
+<para>If more than one of <constant>--link-target</constant>, 
<constant>--fingerprint</constant>, and <constant>--all</constant> are used, 
links matched by any of them will be deleted.
+</para>
+
+<para>The <constant>--dry-run</constant> option will prevent links from being 
deleted, and instead indicate what would happen if it was omitted.
+</para>
+
+<variablelist>
+<!--Options-->
+         <varlistentry>
+           <term>--link-target=DN</term>
+           <listitem><para>Delete this key credential link (a 
DN)</para></listitem>
+         </varlistentry>
+         <varlistentry>
+           <term>--fingerprint=HH:HH:..</term>
+           <listitem><para>Delete the key credential link with this key 
fingerprint</para></listitem>
+         </varlistentry>
+         <varlistentry>
+           <term>--all</term>
+           <listitem><para>Delete all key credential links</para></listitem>
+         </varlistentry>
+         <varlistentry>
+           <term>-n, --dry-run</term>
+           <listitem><para>Do nothing but print what would 
happen</para></listitem>
+         </varlistentry>
+</variablelist>
+
+</refsect3>
+<refsect3>
+
+<title>user keytrust view <replaceable>username</replaceable> [options]</title>
+
+<para>View a user's key credential links. This can be used to find a link's 
fingerprint and target DN for <title>user keytrust delete</title>.
+
+The <constant>--verbose</constant> includes more, probably useless, 
information.
+</para>
+
+<variablelist>
+<!--Options-->
+         <varlistentry>
+           <term>-h, --help</term>
+           <listitem><para>show this help message and exit</para></listitem>
+         </varlistentry>
+         <varlistentry>
+           <term>-v, --verbose</term>
+           <listitem><para>Be verbose</para></listitem>
+         </varlistentry>
+</variablelist>
+</refsect3>
+
+
 <refsect2>
        <title>vampire [options] <replaceable>domain</replaceable></title>
        <para>Join and synchronise a remote AD domain to the local server.
diff --git a/python/samba/domain/models/fields.py 
b/python/samba/domain/models/fields.py
index cff11661e73..959f80faf5c 100644
--- a/python/samba/domain/models/fields.py
+++ b/python/samba/domain/models/fields.py
@@ -64,7 +64,7 @@ class Field(metaclass=ABCMeta):
         # This ensures that fields with many=True are always lists.
         # If this is inconsistent anywhere, it isn't so great to use.
         if self.many and default is None:
-            self.default = []
+            self.default = lambda x: list()
         else:
             self.default = default
 
diff --git a/python/samba/key_credential_link.py 
b/python/samba/key_credential_link.py
index 2ff17da44da..f8c82e1a82b 100644
--- a/python/samba/key_credential_link.py
+++ b/python/samba/key_credential_link.py
@@ -18,9 +18,11 @@
 
 """Functions for processing key_credential_link"""
 
+import base64
 from hashlib import sha256
 import struct
-from typing import Optional, Union
+import time
+from typing import Optional, Union, Iterable
 
 from cryptography.hazmat.primitives.serialization import (
     load_der_public_key,
@@ -35,10 +37,31 @@ from cryptography.x509 import (
     load_der_x509_certificate)
 
 
+from samba import nttime2unix
 from samba.samdb import SamDB, BinaryDn
 from samba.ndr import ndr_unpack, ndr_pack
 from ldb import Dn
-from samba.dcerpc import keycredlink
+from samba.dcerpc import keycredlink, misc
+
+
+class KeyCredLinkError(Exception):
+    """The key credential link is inconsistent."""
+    # For bad values handed in, we use ValueError. For internal bad
+    # values, we use this.
+
+
+def key_usage_string(i):
+    # there must be a better way.
+    for s in ('KEY_USAGE_NGC', 'KEY_USAGE_FIDO', 'KEY_USAGE_FEK',):
+        if i == getattr(keycredlink, s):
+            return s
+    return "unknown"
+
+
+def nttime_as_date(nt):
+    secs = nttime2unix(nt)
+    ts = time.gmtime(secs)
+    return time.strftime('%Y-%m-%d %H:%M:%S', ts)
 
 
 class KeyCredentialLinkDn(BinaryDn):
@@ -72,8 +95,107 @@ class KeyCredentialLinkDn(BinaryDn):
             raise ValueError("Could not parse value as KEYCREDENTIALLINK_BLOB "
                              f" (internal error: {e})")
 
+    def get_entry(self, entry_id):
+        if self.blob is None:
+            raise KeyCredLinkError("no key material")
+
+        for entry in self.blob.entries:
+            if entry.identifier == entry_id:
+                return entry.value
+
+        raise KeyCredLinkError(f"Key information entry {entry_id} not found")
+
+    def fingerprint(self) -> str:
+        """The SHA256 of the key material in DER encoding, formatted
+        as hex pairs separated by colons ("hh:hh:...")"""
+        # A competing format is '2048 SHA256:<base64bytes>' (ssh style).
+
+        # This sha256 value should also be stored in the KeyID field.
+        data = self.get_entry(keycredlink.KeyMaterial)
+        hash = sha256(data).digest()
+        # Python 3.8+ will do this with hash.hex(':')
+        return ':'.join(f'{_:02X}' for _ in hash)
+
+    def description(self, verbosity=2) -> str:
+        """Text describing key credential link characteristics.
+
+        verbosity is adjustable between 1 and 3.
+        """
+        out = []
+
+        def write(msg, verbose_level=0):
+            if verbosity > verbose_level:
+                out.append(msg)
+
+        write(f'Link target: {self.dn}', 1)
+        write(f'Binary Dn: {self}', 2)
+        write(f'Key Credential Link Blob version: {self.blob.version}', 2)
+        write(f'Number of key entries:            {self.blob.count}', 1)
+
+        write('Key entries:')
+        entries = []
+        longest = 0
+        for description, verbose_level, fn, attr in [
+                ("key material fingerprint", 0,
+                 lambda x: ':'.join(f"{_:02X}" for _ in x),
+                 'KeyID'),
+                ("key parameters fingerprint", 2,
+                 lambda x: ':'.join(f"{_:02X}" for _ in x),
+                 'KeyHash'),
+                ("key usage", 1, key_usage_string, 'KeyUsage'),
+                ("Device GUID", 1, misc.GUID, 'DeviceId'),
+                ("last logon", 0, nttime_as_date,
+                 'KeyApproximateLastLogonTimeStamp'),
+                ("creation time", 0, nttime_as_date, 'KeyCreationTime'),
+                # for now we are ignoring KeySource and CustomKeyInformation
+                # KeyMaterial is decoded separately
+                ]:
+
+            if verbosity > 1:
+                description = f"{description} ({attr})"
+
+            i = getattr(keycredlink, attr)
+
+            try:
+                entry = self.get_entry(i)
+                value = fn(entry)
+            except KeyCredLinkError:
+                value = "not found"
+
+            if verbosity > verbose_level:
+                entries.append((description, value))
+                longest = max(longest, len(description))
+
+        for desc, val in entries:
+            write(f"  {desc + ':':{longest + 1}} {val}")
+
+        data = self.get_entry(keycredlink.KeyMaterial)
+        key = get_public_key(data, 'der')
 
-def get_public_key(data:bytes, encoding:str):
+        write("RSA public key properties:", 1)
+        write(f"  key size: {key.key_size}", 1)
+        write(f"  fingerprint: {self.fingerprint()}", 1)
+
+        return '\n'.join(out)
+
+    def key_material(self) -> bytes:
+        return self.get_entry(keycredlink.KeyMaterial)
+
+    def as_pem(self) -> str:
+        """Get the key out of the keycredlink blob, and return it in
+        PEM format as a string.
+
+        PEM is the ASCII format that starts '-----BEGIN PUBLIC KEY-----'.
+        """
+        # The key is in DER format in an entry in the blob.
+        data = self.key_material()
+        key = get_public_key(data, 'der')
+        pem =  key.public_bytes(Encoding.PEM,
+                                PublicFormat.SubjectPublicKeyInfo)
+        return pem.decode()
+
+
+def get_public_key(data:bytes, encoding:Optional[str] = None) -> RSAPublicKey:
     """decode a key in PEM or DER format.
 
     If it turns out to be a certificate or something, we try to get
@@ -146,6 +268,9 @@ def create_key_credential_link(samdb: SamDB,
         if len(res) == 0:
             raise ValueError(f"link target {target} does not exist")
 
+    if encoding == 'auto':
+        encoding = None
+
     key = get_public_key(data, encoding)
 
     if key.key_size != 2048:
@@ -214,3 +339,43 @@ def create_key_credential_link(samdb: SamDB,
 
     k = KeyCredentialLinkDn.from_bytes_and_dn(samdb, kcl_bytes, target)
     return k
+
+def kcl_in_list(kcl: KeyCredentialLinkDn, others: 
Iterable[KeyCredentialLinkDn]):
+    """True if kcl is in the list, otherwise False, disregarding
+    everything except key material and DN for the comparison.
+    """
+    # this helps us avoid duplicate key credential links, which are
+    # otherwise disallowed only if all fields are identical, but which
+    # are generally useless.
+    km = kcl.key_material()
+    for other in others:
+        if km == other.key_material() and kcl.dn == other.dn:
+            return True
+    return False
+
+
+def filter_kcl_list(samdb: SamDB,
+                    keycredlinks: Iterable[KeyCredentialLinkDn],
+                    link_target: Optional[str] = None,
+                    fingerprint: Optional[str] = None) -> list:
+    """Select only the input links that match at least one of the
+    criteria.
+    """
+    # used in samba-tool X keytrust delete
+    selected = []
+    filters = []
+    if link_target is not None:
+        target_dn = Dn(samdb, link_target)
+        filters.append(lambda x: x.dn == target_dn)
+
+    if fingerprint is not None:
+        fingerprint = fingerprint.upper()
+        filters.append(lambda x: x.fingerprint() == fingerprint)
+
+    for x in keycredlinks:
+        for fn in filters:
+            if fn(x):
+                selected.append(x)
+                break
+
+    return selected
diff --git a/python/samba/netcmd/__init__.py b/python/samba/netcmd/__init__.py
index 9aa4664418a..fc8bf96f19c 100644
--- a/python/samba/netcmd/__init__.py
+++ b/python/samba/netcmd/__init__.py
@@ -300,8 +300,9 @@ class Command(object):
 
         return parser, optiongroups
 
-    def message(self, text):
-        self.outf.write(text + "\n")
+    def message(self, *text):
+        for t in text:
+            print(t, file=self.outf)
 
     def _resolve(self, path, *argv, outf=None, errf=None):
         """This is a leaf node, the command that will actually run."""
@@ -500,3 +501,39 @@ class CommandError(Exception):
 
     def __repr__(self):
         return "CommandError(%s)" % self.message
+
+
+def exception_to_command_error(*exceptions, verbose=False):
+    """If you think all instances of a particular exceptions can be
+    turning to a CommandError, do this:
+
+    @exception_to_command_error(ValueError, LdbError):
+    def run(self, username, ...):
+        # continue as normal
+
+    Add the verbose=True flag during development if it is doing your
+    head in.
+    """
+    def wrap2(f):
+        def wrap(*args, **kwargs):
+            try:
+                return f(*args, **kwargs)
+            except exceptions as e:
+                if verbose:
+                    print(colour.c_DARK_RED("↓" * 20 + "DEBUG" + "↓" * 20),
+                          file=sys.stderr)
+                    print(f"converting «e» raised in {f} "
+                          "to CommandError\n",
+                          file=sys.stderr)
+                    traceback.print_exc()
+                    print("\nThis message is here because "
+                          "exception_to_command_error() has the verbose=True"
+                          " debugging option set.",
+                          file=sys.stderr)
+                    print("Below this is what you would normally see:",
+                          file=sys.stderr)
+                    print(colour.c_DARK_RED("↑" * 20 + "DEBUG" + "↑" * 20),
+                          file=sys.stderr)
+                raise CommandError(e)
+        return wrap
+    return wrap2
diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py
index 1413803cf8a..cd5389cf8ec 100644


-- 
Samba Shared Repository

Reply via email to