]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
samba-tool: add user keytrust command
authorDouglas Bagnall <douglas.bagnall@catalyst.net.nz>
Wed, 6 Aug 2025 02:01:14 +0000 (14:01 +1200)
committerDouglas Bagnall <dbagnall@samba.org>
Wed, 20 Aug 2025 04:34:37 +0000 (04:34 +0000)
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.bagnall@catalyst.net.nz>
Reviewed-by: Gary Lockyer <gary@catalyst.net.nz>
13 files changed:
python/samba/netcmd/user/__init__.py
python/samba/netcmd/user/keytrust.py [new file with mode: 0644]
python/samba/tests/samba_tool/user_keytrust.py [new file with mode: 0644]
source4/selftest/tests.py
testdata/keytrust/ca-cert-ecdsa-p256.pem [new file with mode: 0644]
testdata/keytrust/ca-cert-rsa-2048.pem [new file with mode: 0644]
testdata/keytrust/ca-cert-rsa-4096.pem [new file with mode: 0644]
testdata/keytrust/cert-rsa-1024.pem [new file with mode: 0644]
testdata/keytrust/cert-rsa-2048.pem [new file with mode: 0644]
testdata/keytrust/cert-rsa-2048b.pem [new file with mode: 0644]
testdata/keytrust/public-key-from-cert-rsa-2048-pkcs1.pem [new file with mode: 0644]
testdata/keytrust/rsa2048-pkcs1.der [new file with mode: 0644]
testdata/keytrust/rsa2048b-spki.pem [new file with mode: 0644]

index fab657c2278fbcbeca7954edd99adcb24da2a4d5..e73f2d323e0350aa16e1413e7d742c3aa7770dc6 100644 (file)
@@ -27,6 +27,7 @@ from .disable import cmd_user_disable
 from .edit import cmd_user_edit
 from .enable import cmd_user_enable
 from .getgroups import cmd_user_getgroups
+from .keytrust import cmd_user_keytrust
 from .list import cmd_user_list
 from .move import cmd_user_move
 from .password import cmd_user_password
@@ -52,6 +53,7 @@ class cmd_user(SuperCommand):
     subcommands["delete"] = cmd_user_delete()
     subcommands["disable"] = cmd_user_disable()
     subcommands["enable"] = cmd_user_enable()
+    subcommands["keytrust"] = cmd_user_keytrust()
     subcommands["list"] = cmd_user_list()
     subcommands["setexpiry"] = cmd_user_setexpiry()
     subcommands["password"] = cmd_user_password()
diff --git a/python/samba/netcmd/user/keytrust.py b/python/samba/netcmd/user/keytrust.py
new file mode 100644 (file)
index 0000000..c72e304
--- /dev/null
@@ -0,0 +1,223 @@
+# samba-tool commands to manager Key Credential Links on a user
+#
+# Copyright © Douglas Bagnall <dbagnall@samba.org> 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 <http://www.gnu.org/licenses/>.
+
+import ldb
+import samba.getopt as options
+from samba.domain.models import User
+from samba.domain.models.exceptions import ModelError
+from samba.netcmd import Command, CommandError, Option, SuperCommand
+from samba.netcmd import exception_to_command_error
+from samba.key_credential_link import (create_key_credential_link,
+                                       kcl_in_list,
+                                       filter_kcl_list)
+
+
+class cmd_user_keycredentiallink_add(Command):
+    """Add a key-credential-link."""
+
+    synopsis = "%prog <username> [options] <pubkey>"
+
+    takes_args = ["username", "pubkey"]
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "hostopts": options.HostOptions,
+    }
+
+    takes_options = [
+        Option("--link-target", metavar="DN",
+               help="link to this DN (default: this user's DN)"),
+        Option("--encoding", default='auto', choices=('pem', 'der', 'auto'),
+               help="Key format (optional)"),
+        Option("--force", default=False, action='store_true',
+               help="proceed with operations that seems ill-fated"),
+    ]
+
+    @exception_to_command_error(ValueError, ModelError, FileNotFoundError)
+    def run(self, username, pubkey,
+            hostopts=None, sambaopts=None, credopts=None,
+            link_target=None, encoding='auto', force=False):
+
+        samdb = self.ldb_connect(hostopts, sambaopts, credopts)
+        user = User.find(samdb, username)
+
+        if link_target is None:
+            link_target = user.dn
+
+        with open(pubkey, 'rb') as f:
+            data = f.read()
+
+        try:
+            link = create_key_credential_link(samdb,
+                                              link_target,
+                                              data,
+                                              encoding=encoding,
+                                              force=force)
+        except ldb.LdbError as e:
+            # with --force, we will end up with CONSTRAINT_VIOLATION
+            # at user.save(), rather than NO_SUCH_OBJECT now.
+            if e.args[0] == ldb.ERR_NO_SUCH_OBJECT:
+                raise CommandError(f"Link target '{link_target}' does not exist")
+            raise
+
+        if not force and kcl_in_list(link, user.key_credential_link):
+            # It is not allowed to have duplicate linked attributes,
+            # which in the case of key credential links means having
+            # the same key blob and the same DN target.
+            #
+            # It is still possible to have the same key material and
+            # DN target if other fields (e.g. creation date) in the
+            # blob differ. The creation date is set with one second
+            # resolution in create_key_credential_link() just above,
+            # which puts us in the awkward position of creating a race
+            # if people are running samba-tool in a script.
+            #
+            # While the uniqueness invariant is a feature of AD/DSDB,
+            # not of key credential links, duplicates are not going to
+            # be useful, so we try to avoid this by checking first
+            # unless --force is used.
+            #
+            # if --force is used to add a key for the second time in
+            # the same second, user.save() below will raise an
+            # ERR_ATTRIBUTE_OR_VALUE_EXISTS LdbError.
+            raise CommandError(f"User {username} "
+                               "already has this key credential link")
+
+        user.key_credential_link.append(link)
+        user.save(samdb)
+
+
+class cmd_user_keycredentiallink_delete(Command):
+    """Delete a key-credential-link."""
+
+    synopsis = "%prog <username> [options]"
+
+    takes_args = ["username"]
+
+    takes_options = [
+        Option("--link-target", metavar="DN",
+               help="Delete this key credential link (a DN)"),
+        Option("--fingerprint", metavar="HH:HH:..",
+               help="Delete the key credential link with this key fingerprint"),
+        Option("--all", action='store_true',
+               help="Delete all key credential links"),
+        Option("-n", "--dry-run", action='store_true',
+               help="Do nothing but print what would happen"),
+    ]
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "hostopts": options.HostOptions,
+    }
+
+    @exception_to_command_error(ValueError, ModelError)
+    def run(self, username, hostopts=None, sambaopts=None, credopts=None,
+            link_target=None, fingerprint=None, all=False, dry_run=False):
+
+        samdb = self.ldb_connect(hostopts, sambaopts, credopts)
+        user = User.find(samdb, username)
+
+        keycredlinks = user.key_credential_link
+
+        if all:
+            goners = keycredlinks
+        else:
+            goners = filter_kcl_list(samdb,
+                                     keycredlinks,
+                                     link_target=link_target,
+                                     fingerprint=fingerprint)
+
+        keepers = [x for x in keycredlinks if x not in goners]
+        nk = len(keepers)
+
+        if dry_run:
+            self.message("Without --dry-run, this would happen:")
+            if not goners:
+                self.message("NO key credential links are deleted")
+            for x in goners:
+                self.message(f"DELETE {x} (fingerprint {x.fingerprint()})")
+            self.message('')
+            for x in keepers:
+                self.message(f"KEEP {x} (fingerprint {x.fingerprint()})")
+
+            self.message(f"{username} would now have {nk} key credential link"
+                         f"{'' if nk == 1 else 's'}")
+            return
+
+        if not goners:
+            # fail without traceback if the filter matches no links
+            raise CommandError("no key credential links deleted")
+
+        user.key_credential_link = keepers
+        user.save(samdb)
+
+        for x in goners:
+            self.message(f"Deleted {x} (fingerprint {x.fingerprint()})")
+        self.message('')
+        for x in keepers:
+            self.message(f"Keeping {x} (fingerprint {x.fingerprint()})")
+
+        self.message(f"{username} now has {nk} key credential link"
+                     f"{'' if nk == 1 else 's'}")
+
+
+class cmd_user_keycredentiallink_view(Command):
+    """View a user's key credential links."""
+    synopsis = "%prog <username> [options]"
+
+    takes_args = ["username"]
+
+    takes_options = [
+        Option("-v", "--verbose", help="Be verbose", action="store_true"),
+    ]
+
+    takes_optiongroups = {
+        "sambaopts": options.SambaOptions,
+        "credopts": options.CredentialsOptions,
+        "hostopts": options.HostOptions,
+    }
+
+    @exception_to_command_error(ValueError, ModelError)
+    def run(self, username, hostopts=None, sambaopts=None, credopts=None,
+            verbose=False):
+
+        samdb = self.ldb_connect(hostopts, sambaopts, credopts)
+        user = User.find(samdb, username)
+
+        if verbose:
+            verbosity = 3
+        else:
+            verbosity = 2
+
+        n = len(user.key_credential_link)
+        self.message(f"{username} has {n} key credential link"
+                     f"{'' if n == 1 else 's'}\n")
+
+        for kcl in user.key_credential_link:
+            self.message(kcl.description(verbosity), '')
+
+
+class cmd_user_keytrust(SuperCommand):
+    """Manage key-credential links on a user."""
+
+    subcommands = {
+        "add": cmd_user_keycredentiallink_add(),
+        "delete": cmd_user_keycredentiallink_delete(),
+        "view": cmd_user_keycredentiallink_view(),
+    }
diff --git a/python/samba/tests/samba_tool/user_keytrust.py b/python/samba/tests/samba_tool/user_keytrust.py
new file mode 100644 (file)
index 0000000..bcc587b
--- /dev/null
@@ -0,0 +1,360 @@
+# Unix SMB/CIFS implementation.
+#
+# Tests for `samba-tool user keytrust`
+#
+# Copyright © Douglas Bagnall <dbagnall@samba.org> 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 <http://www.gnu.org/licenses/>.
+#
+
+import os
+from pathlib import Path
+
+from samba.domain.models import User
+from samba.tests.samba_tool.base import SambaToolCmdTest
+from samba import key_credential_link as kcl
+
+
+HOST = "ldap://{DC_SERVER}".format(**os.environ)
+CREDS = "-U{DC_USERNAME}%{DC_PASSWORD}".format(**os.environ)
+
+ROOT = (Path(__file__) / '../../../../../').resolve()
+TESTDATA = ROOT / 'testdata' / 'keytrust'
+
+GOOD_CERTS = [
+    str(TESTDATA / 'cert-rsa-2048.pem'),
+    str(TESTDATA / 'ca-cert-rsa-2048.pem'),
+]
+
+WRONG_SIZE_CERTS = [
+    str(TESTDATA / 'cert-rsa-1024.pem'),
+    str(TESTDATA / 'ca-cert-rsa-4096.pem'),
+]
+
+NON_RSA_CERTS = [
+    str(TESTDATA / 'ca-cert-ecdsa-p256.pem'),
+]
+
+GOOD_KEYS = [
+    str(TESTDATA / 'rsa2048-pkcs1.der'),
+    str(TESTDATA / 'rsa2048b-spki.pem'),
+]
+
+DUPLICATE_KEYS = [
+    str(TESTDATA / 'cert-rsa-2048.pem'),
+    str(TESTDATA / 'public-key-from-cert-rsa-2048-pkcs1.pem'),
+]
+
+
+class SambaToolUserKeyTrustTest(SambaToolCmdTest):
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.samdb = cls.getSamDB("-H", HOST, CREDS)
+        cls.runcmd("user", "key-trust", "delete",
+                   "-H", HOST, CREDS,
+                   'joe', '--all')
+        cls.runcmd("user", "key-trust", "delete",
+                   "-H", HOST, CREDS,
+                   'alice', '--all')
+
+    def get_links(self, username):
+        result = self.samdb.search(expression=f'sAMAccountName={username}',
+                                   attrs=['msDS-KeyCredentialLink'])
+        self.assertEqual(len(result), 1)
+        links = result[0].get('msDS-KeyCredentialLink', [])
+        return [kcl.KeyCredentialLinkDn(self.samdb, v) for v in links]
+
+    def test_add_good_cert(self):
+        """These ones should just succeed."""
+        links = self.get_links('joe')
+        n = len(links)
+        for f in GOOD_CERTS:
+            result, out, err = self.runcmd("user", "key-trust", "add",
+                                           "-H", HOST, CREDS,
+                                           'joe', f)
+            self.assertCmdSuccess(result, out, err)
+
+            n += 1
+            links = self.get_links('joe')
+            self.assertEqual(len(links), n)
+
+        result, out, err = self.runcmd("user", "key-trust", "delete",
+                                       "-H", HOST, CREDS,
+                                       'joe', '--all')
+        self.assertCmdSuccess(result, out, err)
+
+        for link in links:
+            self.assertIn(f"Deleted {link}", out)
+
+        links = self.get_links('joe')
+        self.assertEqual(links, [])
+
+    def test_add_and_delete_good_keys(self):
+        """Add known good keys, and also check the view and delete commands."""
+        links = self.get_links('alice')
+        self.assertEqual(links, [])
+
+        result, out, err = self.runcmd("user", "key-trust", "add",
+                                       "-H", HOST, CREDS,
+                                       'alice', GOOD_KEYS[0])
+        self.assertCmdSuccess(result, out, err)
+        links = self.get_links('alice')
+        self.assertEqual(len(links), 1)
+
+        result, out, err = self.runcmd("user", "key-trust", "view",
+                                       "-H", HOST, CREDS,
+                                       'alice')
+        self.assertCmdSuccess(result, out, err)
+        self.assertIn('alice has 1 key credential link\n', out)
+        self.assertIn('Link target: CN=alice,CN=Users,DC=addom,DC=samba,DC=example,DC=com\n', out)
+        self.assertIn('Number of key entries:            5', out)
+
+        result, out, err = self.runcmd("user", "key-trust", "add",
+                                       "-H", HOST, CREDS,
+                                       'alice', GOOD_KEYS[1])
+        self.assertCmdSuccess(result, out, err)
+        result, out, err = self.runcmd("user", "key-trust", "view",
+                                       "-H", HOST, CREDS,
+                                       'alice', '--verbose')
+        self.assertCmdSuccess(result, out, err)
+        self.assertIn('alice has 2 key credential links\n', out)
+
+        links = self.get_links('alice')
+        fingerprints = [('16:CD:1B:C2:7A:0B:FC:C9:4B:95:11:9F:AD:97:EC:1B:'
+                         'ED:BD:64:91:42:2E:AF:CA:CB:1E:C3:EE:86:6D:F1:5A'),
+                        ('86:61:6D:B2:6A:3A:04:BD:E0:59:10:13:21:9A:2B:2C:'
+                         'C4:FD:CE:50:05:16:3C:66:1B:38:63:79:8C:B1:DA:17')]
+
+        self.assertEqual(set(x.fingerprint() for x in links),
+                         set(fingerprints))
+
+        # test delete --dry-run / -n
+        result, out, err = self.runcmd("user", "key-trust", "delete",
+                                       "-H", HOST, CREDS,
+                                       'alice', '--all', '-n')
+        self.assertCmdSuccess(result, out, err)
+        self.assertIn('Without --dry-run, this would happen:\n', out)
+        self.assertIn(f'DELETE {links[0]} (fingerprint {links[0].fingerprint()})',
+                      out)
+        self.assertIn(f'DELETE {links[1]} (fingerprint {links[1].fingerprint()})',
+                      out)
+        self.assertNotIn('KEEP', out)
+        self.assertIn('alice would now have 0 key credential links\n', out)
+
+        result, out, err = self.runcmd("user", "key-trust", "delete",
+                                       "-H", HOST, CREDS,
+                                       'alice', '--fingerprint=whatever',
+                                       '--dry-run')
+        self.assertCmdSuccess(result, out, err)
+        self.assertIn('NO key credential links are deleted\n', out)
+
+        self.assertIn(f'KEEP {links[0]} (fingerprint {links[0].fingerprint()})',
+                      out)
+        self.assertIn(f'KEEP {links[1]} (fingerprint {links[1].fingerprint()})',
+                      out)
+        self.assertIn('alice would now have 2 key credential links\n', out)
+
+        result, out, err = self.runcmd("user", "key-trust", "delete",
+                                       "-H", HOST, CREDS,
+                                       'alice',
+                                       '--fingerprint',
+                                       fingerprints[1],
+                                       '--dry-run')
+        self.assertCmdSuccess(result, out, err)
+        self.assertIn(f'DELETE {links[1]} (fingerprint {links[1].fingerprint()})',
+                      out)
+        self.assertIn(f'KEEP {links[0]} (fingerprint {links[0].fingerprint()})',
+                      out)
+        self.assertIn('alice would now have 1 key credential link\n', out)
+
+        # this time deleting for real
+        result, out, err = self.runcmd("user", "key-trust", "delete",
+                                       "-H", HOST, CREDS,
+                                       'alice', '--all')
+        self.assertCmdSuccess(result, out, err)
+        links = self.get_links('alice')
+        self.assertEqual(links, [])
+
+        result, out, err = self.runcmd("user", "key-trust", "view",
+                                       "-H", HOST, CREDS,
+                                       'alice')
+        self.assertCmdSuccess(result, out, err)
+        self.assertIn('alice has 0 key credential links\n', out)
+
+    def test_add_duplicate_keys(self):
+        """You should not be able to add the same link twice."""
+
+        self.addCleanup(self.runcmd, "user", "key-trust", "delete",
+                        "-H", HOST, CREDS,
+                        'alice', '--all')
+
+        result, out, err = self.runcmd("user", "key-trust", "add",
+                                       "-H", HOST, CREDS,
+                                       'alice', DUPLICATE_KEYS[0])
+        self.assertCmdSuccess(result, out, err)
+
+        # This source file is different, but contains the same public
+        # key. samba-tool should notice this and fail *before* it
+        # fails in the dsdb layer with ERR_ATTRIBUTE_OR_VALUE_EXISTS
+        result, out, err = self.runcmd("user", "key-trust", "add",
+                                       "-H", HOST, CREDS,
+                                       'alice', DUPLICATE_KEYS[1])
+        self.assertCmdFail(result)
+        self.assertNotIn('ATTRIBUTE_OR_VALUE_EXISTS', err)
+
+        # adding the first file again should also fail.
+        result, out, err = self.runcmd("user", "key-trust", "add",
+                                       "-H", HOST, CREDS,
+                                       'alice', DUPLICATE_KEYS[0])
+        self.assertCmdFail(result)
+
+        # adding to a different DN is OK
+        base_dn = self.samdb.domain_dn()
+        result, out, err = self.runcmd("user", "key-trust", "add",
+                                       "-H", HOST, CREDS,
+                                       "--link-target", base_dn,
+                                       'alice', DUPLICATE_KEYS[1])
+        self.assertCmdSuccess(result, out, err)
+
+        self.assertEqual(len(self.get_links('alice')), 2)
+
+    def test_add_wrong_size_keys(self):
+        """You should not be able to add the same link twice."""
+
+        self.addCleanup(self.runcmd, "user", "key-trust", "delete",
+                        "-H", HOST, CREDS,
+                        'joe', '--all')
+
+        for fn in WRONG_SIZE_CERTS:
+            result, out, err = self.runcmd("user", "key-trust", "add",
+                                           "-H", HOST, CREDS,
+                                           'joe', fn)
+            self.assertCmdFail(result)
+            self.assertIn('ERROR: 2048 bit RSA key expected, not', err)
+
+        self.assertEqual(self.get_links('joe'), [])
+
+        for fn in WRONG_SIZE_CERTS:
+            # it will work with --force
+            result, out, err = self.runcmd("user", "key-trust", "add",
+                                           "-H", HOST, CREDS,
+                                           '--force',
+                                           'joe', fn)
+
+            self.assertCmdSuccess(result, out, err)
+
+        self.assertEqual(len(self.get_links('joe')), 2)
+
+    def test_add_non_rsa_keys(self):
+        """You should not be able to add the same link twice."""
+
+        self.addCleanup(self.runcmd, "user", "key-trust", "delete",
+                        "-H", HOST, CREDS,
+                        'joe', '--all')
+
+        for fn in NON_RSA_CERTS:
+            result, out, err = self.runcmd("user", "key-trust", "add",
+                                           "-H", HOST, CREDS,
+                                           'joe', fn)
+            self.assertCmdFail(result)
+            self.assertIn('only RSA Public Keys are supported', err)
+
+        self.assertEqual(self.get_links('joe'), [])
+
+        for fn in NON_RSA_CERTS:
+            # it will NOT work with --force
+            result, out, err = self.runcmd("user", "key-trust", "add",
+                                           "-H", HOST, CREDS,
+                                           '--force',
+                                           'joe', fn)
+
+            self.assertCmdFail(result)
+            self.assertIn('only RSA Public Keys are supported', err)
+
+        self.assertEqual(self.get_links('joe'), [])
+
+    def test_add_good_cert_bad_dn(self):
+        """Fails differently with --force"""
+        links = self.get_links('joe')
+        n = len(links)
+        target = f"CN=an RDN that is not there,{self.samdb.domain_dn()}"
+        result, out, err = self.runcmd("user", "key-trust", "add",
+                                       "-H", HOST, CREDS,
+                                       '--link-target', target,
+                                       'joe', GOOD_CERTS[0])
+        self.assertCmdFail(result)
+        self.assertIn(f"ERROR: Link target '{target}' does not exist", err)
+        self.assertEqual(len(links), 0)
+
+        result, out, err = self.runcmd("user", "key-trust", "add",
+                                       "-H", HOST, CREDS,
+                                       '--link-target', target,
+                                       '--force',
+                                       'joe', GOOD_CERTS[1])
+        self.assertCmdFail(result)
+        self.assertIn("ERROR(ldb): uncaught exception", err)
+        self.assertIn("LDAP_CONSTRAINT_VIOLATION", err)
+        self.assertEqual(len(links), 0)
+
+    def test_add_good_cert_bad_encoding(self):
+        """If we use --encoding=pem with a DER file or vice versa, it
+        should fail."""
+        self.addCleanup(self.runcmd, "user", "key-trust", "delete",
+                        "-H", HOST, CREDS,
+                        'joe', '--all')
+
+        result, out, err = self.runcmd("user", "key-trust", "add",
+                                       "-H", HOST, CREDS,
+                                       '--encoding', 'der',
+                                       'joe', GOOD_CERTS[0])
+        self.assertCmdFail(result)
+        self.assertIn("ERROR: could not decode public key", err)
+        self.assertEqual(self.get_links('joe'), [])
+
+        # try to --force this one, to no avail
+        result, out, err = self.runcmd("user", "key-trust", "add",
+                                       "-H", HOST, CREDS,
+                                       '--force',
+                                       '--encoding', 'pem',
+                                       'joe', GOOD_KEYS[0])
+        self.assertCmdFail(result)
+        self.assertIn("ERROR: could not decode public key", err)
+        self.assertEqual(self.get_links('joe'), [])
+
+        with self.assertRaises(SystemExit):
+            # we can't catch result and output here because it fails
+            # in optparse which prints straight to stderr.
+            self.runcmd("user", "key-trust", "add",
+                        "-H", HOST, CREDS,
+                        '--encoding', 'pineapple',
+                        'joe', GOOD_CERTS[1])
+        self.assertCmdFail(result)
+        self.assertEqual(self.get_links('joe'), [])
+
+        # right encoding
+        result, out, err = self.runcmd("user", "key-trust", "add",
+                                       "-H", HOST, CREDS,
+                                       '--encoding', 'pem',
+                                       'joe', GOOD_CERTS[1])
+        self.assertCmdSuccess(result, out, err)
+        self.assertEqual(len(self.get_links('joe')), 1)
+
+        # 'auto' encoding
+        result, out, err = self.runcmd("user", "key-trust", "add",
+                                       "-H", HOST, CREDS,
+                                       '--encoding', 'auto',
+                                       'joe', GOOD_CERTS[0])
+        self.assertCmdSuccess(result, out, err)
+        self.assertEqual(len(self.get_links('joe')), 2)
index 57a5a66ef62623cd69b7597733184d8bd043d12e..13d868c6873c0eae16cf94c3cd25ea4646c994d1 100755 (executable)
@@ -1194,6 +1194,7 @@ planpythontestsuite("ad_dc_default:local", "samba.tests.samba_tool.processes")
 planpythontestsuite("ad_dc_ntvfs:local", "samba.tests.samba_tool.user")
 planpythontestsuite("ad_dc_default", "samba.tests.samba_tool.user_auth_policy")
 planpythontestsuite("ad_dc_default", "samba.tests.samba_tool.user_auth_silo")
+planpythontestsuite("ad_dc_default", "samba.tests.samba_tool.user_keytrust")
 for env in ["ad_dc_default:local", "ad_dc_no_ntlm:local"]:
     planpythontestsuite(env, "samba.tests.samba_tool.user_wdigest")
 for env, nt_hash in [("ad_dc:local", True),
diff --git a/testdata/keytrust/ca-cert-ecdsa-p256.pem b/testdata/keytrust/ca-cert-ecdsa-p256.pem
new file mode 100644 (file)
index 0000000..3522b09
--- /dev/null
@@ -0,0 +1,12 @@
+-----BEGIN CERTIFICATE-----
+MIIBuTCCAWCgAwIBAgIBATAKBggqhkjOPQQDAjA2MQswCQYDVQQGEwJTRTEQMA4G
+A1UECgwHSGVpbWRhbDEVMBMGA1UEAwwMQ0Egc2VjcDI1NnIxMCAXDTE5MDMyMjIy
+MjUyNVoYDzI1MTgxMTIxMjIyNTI1WjA2MQswCQYDVQQGEwJTRTEQMA4GA1UECgwH
+SGVpbWRhbDEVMBMGA1UEAwwMQ0Egc2VjcDI1NnIxMFkwEwYHKoZIzj0CAQYIKoZI
+zj0DAQcDQgAE5SuFK+KhglopQr1aMjl4ZEBaw4HYM2yVORyBOQWx3e8Pj90bFocE
+4gyS4P2V0YraxACsQgMp+s4e8/6gXPeMtqNdMFswHQYDVR0OBBYEFOtR3wCoaF9m
+8dWylzOdd5vfbwmDMB8GA1UdIwQYMBaAFOtR3wCoaF9m8dWylzOdd5vfbwmDMAwG
+A1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMAoGCCqGSM49BAMCA0cAMEQCIF/JTbEv
+iuYcuREFzWgZ/AgfLe2sRwEgSy6UcAWOYllkAiApMzA3xKjaX1/hhkDGKZnHfcTM
+tRuM0FuTdO+e15ku8w==
+-----END CERTIFICATE-----
diff --git a/testdata/keytrust/ca-cert-rsa-2048.pem b/testdata/keytrust/ca-cert-rsa-2048.pem
new file mode 100644 (file)
index 0000000..45ae8e8
--- /dev/null
@@ -0,0 +1,26 @@
+-----BEGIN CERTIFICATE-----
+MIIEajCCA1KgAwIBAgIBATANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJKUDEN
+MAsGA1UECgwESlBLSTEpMCcGA1UECwwgUHJlZmVjdHVyYWwgQXNzb2NpYXRpb24g
+Rm9yIEpQS0kxETAPBgNVBAsMCEJyaWRnZUNBMB4XDTAzMTIyNzA1MDgxNVoXDTEz
+MTIyNjE0NTk1OVowWjELMAkGA1UEBhMCSlAxDTALBgNVBAoMBEpQS0kxKTAnBgNV
+BAsMIFByZWZlY3R1cmFsIEFzc29jaWF0aW9uIEZvciBKUEtJMREwDwYDVQQLDAhC
+cmlkZ2VDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANTnUmg7K3m8
+52vd77kwkq156euwoWm5no8E8kmaTSc7x2RABPpqNTlMKdZ6ttsyYrqREeDkcvPL
+yF7yf/I8+innasNtsytcTAy8xY8Avsbd4JkCGW9dyPjk9pzzc3yLQ64Rx2fujRn2
+agcEVdPCr/XpJygX8FD5bbhkZ0CVoiASBmlHOcC3YpFlfbT1QcpOSOb7o+VdKVEi
+MMfbBuU2IlYIaSr/R1nO7RPNtkqkFWJ1/nKjKHyzZje7j70qSxb+BTGcNgTHa1YA
+UrogKB+UpBftmb4ds+XlkEJ1dvwokiSbCDaWFKD+YD4B2s0bvjCbw8xuZFYGhNyR
+/2D5XfN1s2MCAwEAAaOCATkwggE1MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E
+BTADAQH/MG0GA1UdHwRmMGQwYqBgoF6kXDBaMQswCQYDVQQGEwJKUDENMAsGA1UE
+CgwESlBLSTEpMCcGA1UECwwgUHJlZmVjdHVyYWwgQXNzb2NpYXRpb24gRm9yIEpQ
+S0kxETAPBgNVBAsMCEJyaWRnZUNBMIGDBgNVHREEfDB6pHgwdjELMAkGA1UEBhMC
+SlAxJzAlBgNVBAoMHuWFrOeahOWAi+S6uuiqjeiovOOCteODvOODk+OCuTEeMBwG
+A1UECwwV6YO96YGT5bqc55yM5Y2U6K2w5LyaMR4wHAYDVQQLDBXjg5bjg6rjg4Pj
+grjoqo3oqLzlsYAwHQYDVR0OBBYEFNQXMiCqQNkR2OaZmQgLtf8mR8p8MA0GCSqG
+SIb3DQEBBQUAA4IBAQATjJo4reTNPC5CsvAKu1RYT8PyXFVYHbKsEpGt4GR8pDCg
+HEGAiAhHSNrGh9CagZMXADvlG0gmMOnXowriQQixrtpkmx0TB8tNAlZptZWkZC+R
+8TnjOkHrk2nFAEC3ezbdK0R7MR4tJLDQCnhEWbg50rf0wZ/aF8uAaVeEtHXa6W0M
+Xq3dSe0XAcrLbX4zZHQTaWvdpLAIjl6DZ3SCieRMyoWUL+LXaLFdTP5WBCd+No58
+IounD9X4xxze2aeRVaiV/WnQ0OSPNS7n7YXy6xQdnaOU4KRW/Lne1EDf5IfWC/ih
+bVAmhZMbcrkWWcsR6aCPG+2mV3zTD6AUzuKPal8Y
+-----END CERTIFICATE-----
diff --git a/testdata/keytrust/ca-cert-rsa-4096.pem b/testdata/keytrust/ca-cert-rsa-4096.pem
new file mode 100644 (file)
index 0000000..7aa8bcf
--- /dev/null
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFczCCA1ugAwIBAgIJAI34CtjBcJHEMA0GCSqGSIb3DQEBCwUAMCoxGzAZBgNV
+BAMMEmh4NTA5IFRlc3QgUm9vdCBDQTELMAkGA1UEBhMCU0UwIBcNMTkwMzIyMjIy
+NTAxWhgPMjUxODExMjEyMjI1MDFaMCoxGzAZBgNVBAMMEmh4NTA5IFRlc3QgUm9v
+dCBDQTELMAkGA1UEBhMCU0UwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
+AQDE4gbVQ/vlPFU2W62rukqiUkJ/EIDo1HE4r+xpxO12Ke45NtqSep0d2FfSvEu8
+dhot1jWIkijF7B/bFuB0LyFryCAV/zlU9rLoadCmur5ONIgXRm7eW19wxo5YRD4C
+A1IRwvT+Axz0TC3eyquUN1C0r7ZWbiOY8uQy3Sjfar16Z3TtqlKgo4R/yF7dIPJO
+OVHaznC+xsfLsYE2r9PqbTjBF3O1pIhwV9oA3tfs23EtvcZBP3y3LSsjnKaF0b/N
+XmjLNW9hbmAfN+16TEMOlVZvBjUPO3CC/GU0PJzm1/FqyzXWeRx5FZNi7fCPKg8J
+9QDAgK5mMn+ZPazuUt70uxUFrnRLCjCia/TgC+t2d+3AqsnRlYnLYDv/MeP/QwqH
+GK3+WuAS6uqXZMtilDhY+oiMTZ4vDHvwzJ5q3UhIpWXj5cSGWAxQurKgUsjT9sta
+gGmXlBauMYSzFM5T+TXica1qE7dNXjXr2sTy9BHIp+aWJuGkX9rSx8tHwbkIkVTp
+4UCZ+QoxBRaSmiuyFPM77yg6wZBuSuRRN/BNKvAhuJaE1MdA+vobbyyNbv56MU0+
+WI4ucD+b08JmJp3k+fgM0fKXBQlEL4mp7zeUoLmC1yCy48Zk1foPZTRH9pIGB/zU
+z4B8o20NmereB9bX0IjJ6eqMDqvAZWZ99Nf16Q3X88T88wIDAQABo4GZMIGWMB0G
+A1UdDgQWBBRTuMwJxp9C6tXkdCC0Ze1o+J21BTBaBgNVHSMEUzBRgBRTuMwJxp9C
+6tXkdCC0Ze1o+J21BaEupCwwKjEbMBkGA1UEAwwSaHg1MDkgVGVzdCBSb290IENB
+MQswCQYDVQQGEwJTRYIJAI34CtjBcJHEMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQD
+AgHmMA0GCSqGSIb3DQEBCwUAA4ICAQCdea8Tx/1vuQCqn2AOmIzEO9xgmaY3opl9
+0Px82DafNuFFJu9WlPrsKeADtSqpA/MwBjL8K+T3dhL4Bxhq8jed0gGsS3C3xFTl
+/RJbnFiLuveMErPTEtxaRYpa3oibQ15eJbDq533Is1x8oeK6NnHB6St3nboST0f3
+6SeAsoeHrI16eUEJQ3UKJYJlxATEqOpeaWwdlT6jF9u0WyENz0ijD0b9FPY/zq6D
+zx47Zd5F6aisrtKNXFjB9/oHV6jOh9OGxz6WfT1z2AZ+69jEm+xE8coq4nyWcJrS
+cf7ENBIw1Rknpxk3/H1p0q+Zze5/JBYlKOtEML1dwRIRquVgfhcI/tOq7m5jUxTl
+/6dW8FCnuEFBnUvUrZ0Hv3g2jvHElpPjYkLwZyKFYyvY/G+xCZAiqaYZ2kQqRmti
+KvSfh8fJlV2Jj2aDI1I4JjACG7LYBe7WXCs7TccRrhnx/RUY7cpJwEQrIOKBq8wx
+DD58oPgvkmNPP5lZcFARjnWcY8xS9KzT+KaZWM3ZPefg3Vk//3HfwZfDE3Y7IMgu
+quuLcpeGtMDyurnm6piUdPITt8yW+MMJR8V+PeF0zLdN4dA0nuJpZWZ3fvnevmAL
+jh/ia0LuzkhVSj1R1dXXopZevazh/tmAuKU9BbeYGvwI1RFXVvyzpGRfMbgaze3Q
+tIrreKeFxg==
+-----END CERTIFICATE-----
diff --git a/testdata/keytrust/cert-rsa-1024.pem b/testdata/keytrust/cert-rsa-1024.pem
new file mode 100644 (file)
index 0000000..9a89e59
--- /dev/null
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIICzTCCAjagAwIBAgIJAOSnzE4Qx2H/MA0GCSqGSIb3DQEBBQUAMDkxCzAJBgNV
+BAYTAkpQMRQwEgYDVQQKEwtDQSBURVNUIDEtNDEUMBIGA1UEAxMLQ0EgVEVTVCAx
+LTQwHhcNMDYwOTA3MTY0MDM3WhcNMDcwOTA3MTY0MDM3WjBPMQswCQYDVQQGEwJK
+UDEOMAwGA1UECBMFVG9reW8xFjAUBgNVBAoTDVRFU1QgMiBDTElFTlQxGDAWBgNV
+BAMTD3d3dzIuZXhhbXBsZS5qcDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
+vSpZ6ig9DpeKB60h7ii1RitNuvkn4INOfEXjCjPSFwmIbGJqnyWvKTiMKzguEYkG
+6CZAbsx44t3kvsVDeUd5WZBRgMoeQd1tNJBU4BXxOA8bVzdwstzaPeeufQtZDvKf
+M4ej+fo/j9lYH9udCug1huaNybcCtijzGonkddX4JEUCAwEAAaOBxjCBwzAJBgNV
+HRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZp
+Y2F0ZTAdBgNVHQ4EFgQUK0DZtd8K1P2ij9gVKUNcHlx7uCIwaQYDVR0jBGIwYIAU
+340JbeYcg6V9zi8aozy48aIhtfihPaQ7MDkxCzAJBgNVBAYTAkpQMRQwEgYDVQQK
+EwtDQSBURVNUIDEtNDEUMBIGA1UEAxMLQ0EgVEVTVCAxLTSCCQDkp8xOEMdh/jAN
+BgkqhkiG9w0BAQUFAAOBgQCkGhwCDLRwWbDnDFReXkIZ1/9OhfiR8yL1idP9iYVU
+cSoWxSHPBWkv6LORFS03APcXCSzDPJ9pxTjFjGGFSI91fNrzkKdHU/+0WCF2uTh7
+Dz2blqtcmnJqMSn1xHxxfM/9e6M3XwFUMf7SGiKRAbDfsauPafEPTn83vSeKj1lg
+Dw==
+-----END CERTIFICATE-----
diff --git a/testdata/keytrust/cert-rsa-2048.pem b/testdata/keytrust/cert-rsa-2048.pem
new file mode 100644 (file)
index 0000000..f6631b2
--- /dev/null
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIID7zCCAtegAwIBAgIBATANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlFeGFt
+cGxlQ0EwHhcNMTQwMTE1MTU0MDUwWhcNMTUwMTE1MTU0MDUwWjAAMIIBIjANBgkq
+hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAncvm0aOBK05rdNInYXzJGV5SFteVUFpt
+XFxg4evROvlulB3BzUmFGQYFDcItVnJX2fAvf0UJLtLBVBQggb5ylL6bRpj72cS3
+oyNbs0CGmix9Z1QDjkZZFvIsD1GcKO0tvsCvsEItH8Cm0fq8WcGFijWLdRD5eulP
+55pq1bAHAvIo4+VLMJVBG71xrKGZeHPjKoq6seYjh7AGy+hk2vmFzpZ8Ghdgqv+K
+02IZ7FEdzuylHW8U3qsxBHysMut4inj6AiVf467OOs5meHiifIK9MGkovMrfY9iX
+uUVUs/KXpE1sgeoX9BLvx1BPcODosr5K+z5i71OtIXy4CXrPvcGzRwIDAQABo4IB
+XjCCAVowQAYIKwYBBQUHAQEENDAyMDAGCCsGAQUFBzAChiRodHRwOi8vd3d3LmV4
+YW1wbGUuY29tL0V4YW1wbGVDQS5jcnQwDgYDVR0PAQH/BAQDAgAgMFkGA1UdEQEB
+/wRPME2kSzBJMRYwFAYFZ4EFAgEMC2lkOjU0NDM0NzAwMRcwFQYFZ4EFAgIMDEFC
+Q0RFRjEyMzQ1NjEWMBQGBWeBBQIDDAtpZDowMDAxMDAyMzAMBgNVHRMBAf8EAjAA
+MDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly93d3cuZXhhbXBsZS5jb20vRXhhbXBs
+ZUNBLmNybDAQBgNVHSAECTAHMAUGAyoDBDAfBgNVHSMEGDAWgBQ0d2ckTESv554q
+4LJMaVeVJLM92jAQBgNVHSUECTAHBgVngQUIATAhBgNVHQkEGjAYMBYGBWeBBQIQ
+MQ0wCwwDMi4wAgEAAgFjMA0GCSqGSIb3DQEBCwUAA4IBAQAba2btJ/+4z02MWpNp
+99AFGpEu3yIaJqI6NeHvC6fxxe/9lWlHKISR+CnpAh03/MKT8TP2/cUSi0jjkQNh
+MtueUNofE79fYXtHXHU7wzzUFWNwCmhTuHDYl3jmD0fJ9yA2CuUHT6q3UV+PwXN+
+EHE1hQwC8QtNC/5A7wY1e5dBLdgwSSIgTc4lSsbNcZ9d+m7mWEWpumSYU0czTDEN
+Hmdu/VJuDN/RCOAyBb+hc19LAucGmnFYOhxWHfd9zbXZA1ldFUxrpPuVfKx+Eo8f
+rMsB2oZKMwSYUAWotqolhLe2wdBMRjdmVz44kIhuFB7y4BpQjlB1+xAzX9Hb31CG
+eoS2
+-----END CERTIFICATE-----
diff --git a/testdata/keytrust/cert-rsa-2048b.pem b/testdata/keytrust/cert-rsa-2048b.pem
new file mode 100644 (file)
index 0000000..66b769c
--- /dev/null
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEGDCCAwCgAwIBAgIBATANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlFeGFt
+cGxlQ0EwHhcNMTQwMTE1MTU0MDUwWhcNMTUwMTE1MTU0MDUwWjAAMIIBIjANBgkq
+hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAncvm0aOBK05rdNInYXzJGV5SFteVUFpt
+XFxg4evROvlulB3BzUmFGQYFDcItVnJX2fAvf0UJLtLBVBQggb5ylL6bRpj72cS3
+oyNbs0CGmix9Z1QDjkZZFvIsD1GcKO0tvsCvsEItH8Cm0fq8WcGFijWLdRD5eulP
+55pq1bAHAvIo4+VLMJVBG71xrKGZeHPjKoq6seYjh7AGy+hk2vmFzpZ8Ghdgqv+K
+02IZ7FEdzuylHW8U3qsxBHysMut4inj6AiVf467OOs5meHiifIK9MGkovMrfY9iX
+uUVUs/KXpE1sgeoX9BLvx1BPcODosr5K+z5i71OtIXy4CXrPvcGzRwIDAQABo4IB
+hzCCAYMwQAYIKwYBBQUHAQEENDAyMDAGCCsGAQUFBzAChiRodHRwOi8vd3d3LmV4
+YW1wbGUuY29tL0V4YW1wbGVDQS5jcnQwDgYDVR0PAQH/BAQDAgAgMIGBBgNVHREB
+Af8EdzB1pEswSTEWMBQGBWeBBQIBDAtpZDo1NDQzNDcwMDEXMBUGBWeBBQICDAxB
+QkNERUYxMjM0NTYxFjAUBgVngQUCAwwLaWQ6MDAwMTAwMjOgJgYIKwYBBQUHCASg
+GjAYBgVngQUBAgQPdHBtc2VyaWFsbnVtYmVyMAwGA1UdEwEB/wQCMAAwNQYDVR0f
+BC4wLDAqoCigJoYkaHR0cDovL3d3dy5leGFtcGxlLmNvbS9FeGFtcGxlQ0EuY3Js
+MBAGA1UdIAQJMAcwBQYDKgMEMB8GA1UdIwQYMBaAFDR3ZyRMRK/nnirgskxpV5Uk
+sz3aMBAGA1UdJQQJMAcGBWeBBQgBMCEGA1UdCQQaMBgwFgYFZ4EFAhAxDTALDAMy
+LjACAQACAWMwDQYJKoZIhvcNAQELBQADggEBABtrZu0n/7jPTYxak2n30AUakS7f
+Ihomojo14e8Lp/HF7/2VaUcohJH4KekCHTf8wpPxM/b9xRKLSOORA2Ey255Q2h8T
+v19he0dcdTvDPNQVY3AKaFO4cNiXeOYPR8n3IDYK5QdPqrdRX4/Bc34QcTWFDALx
+C00L/kDvBjV7l0Et2DBJIiBNziVKxs1xn136buZYRam6ZJhTRzNMMQ0eZ279Um4M
+39EI4DIFv6FzX0sC5waacVg6HFYd933NtdkDWV0VTGuk+5V8rH4Sjx+sywHahkoz
+BJhQBai2qiWEt7bB0ExGN2ZXPjiQiG4UHvLgGlCOUHX7EDNf0dvfUIZ6hLY=
+-----END CERTIFICATE-----
diff --git a/testdata/keytrust/public-key-from-cert-rsa-2048-pkcs1.pem b/testdata/keytrust/public-key-from-cert-rsa-2048-pkcs1.pem
new file mode 100644 (file)
index 0000000..79c4499
--- /dev/null
@@ -0,0 +1,8 @@
+-----BEGIN RSA PUBLIC KEY-----
+MIIBCgKCAQEAncvm0aOBK05rdNInYXzJGV5SFteVUFptXFxg4evROvlulB3BzUmF
+GQYFDcItVnJX2fAvf0UJLtLBVBQggb5ylL6bRpj72cS3oyNbs0CGmix9Z1QDjkZZ
+FvIsD1GcKO0tvsCvsEItH8Cm0fq8WcGFijWLdRD5eulP55pq1bAHAvIo4+VLMJVB
+G71xrKGZeHPjKoq6seYjh7AGy+hk2vmFzpZ8Ghdgqv+K02IZ7FEdzuylHW8U3qsx
+BHysMut4inj6AiVf467OOs5meHiifIK9MGkovMrfY9iXuUVUs/KXpE1sgeoX9BLv
+x1BPcODosr5K+z5i71OtIXy4CXrPvcGzRwIDAQAB
+-----END RSA PUBLIC KEY-----
diff --git a/testdata/keytrust/rsa2048-pkcs1.der b/testdata/keytrust/rsa2048-pkcs1.der
new file mode 100644 (file)
index 0000000..247b275
Binary files /dev/null and b/testdata/keytrust/rsa2048-pkcs1.der differ
diff --git a/testdata/keytrust/rsa2048b-spki.pem b/testdata/keytrust/rsa2048b-spki.pem
new file mode 100644 (file)
index 0000000..edaa8d5
--- /dev/null
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAncvm0aOBK05rdNInYXzJ
+GV5SFteVUFptXFxg4evROvlulB3BzUmFGQYFDcItVnJX2fAvf0UJLtLBVBQggb5y
+lL6bRpj72cS3oyNbs0CGmix9Z1QDjkZZFvIsD1GcKO0tvsCvsEItH8Cm0fq8WcGF
+ijWLdRD5eulP55pq1bAHAvIo4+VLMJVBG71xrKGZeHPjKoq6seYjh7AGy+hk2vmF
+zpZ8Ghdgqv+K02IZ7FEdzuylHW8U3qsxBHysMut4inj6AiVf467OOs5meHiifIK9
+MGkovMrfY9iXuUVUs/KXpE1sgeoX9BLvx1BPcODosr5K+z5i71OtIXy4CXrPvcGz
+RwIDAQAB
+-----END PUBLIC KEY-----