← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:archive-gpg-generate-using-signing-service into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:archive-gpg-generate-using-signing-service into launchpad:master.

Commit message:
Generate archive signing keys using the signing service

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/390668

This is guarded by the feature rule 'archivepublisher.gpg.signing_service.enabled'.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:archive-gpg-generate-using-signing-service into launchpad:master.
diff --git a/lib/lp/archivepublisher/archivegpgsigningkey.py b/lib/lp/archivepublisher/archivegpgsigningkey.py
index a342332..20a4d79 100644
--- a/lib/lp/archivepublisher/archivegpgsigningkey.py
+++ b/lib/lp/archivepublisher/archivegpgsigningkey.py
@@ -37,17 +37,24 @@ from lp.archivepublisher.run_parts import (
 from lp.registry.interfaces.gpg import IGPGKeySet
 from lp.services.config import config
 from lp.services.features import getFeatureFlag
-from lp.services.gpg.interfaces import IGPGHandler
+from lp.services.gpg.interfaces import (
+    IGPGHandler,
+    IPymeKey,
+    )
 from lp.services.osutils import remove_if_exists
 from lp.services.propertycache import (
     cachedproperty,
     get_property_cache,
     )
 from lp.services.signing.enums import (
+    OpenPGPKeyAlgorithm,
     SigningKeyType,
     SigningMode,
     )
-from lp.services.signing.interfaces.signingkey import ISigningKeySet
+from lp.services.signing.interfaces.signingkey import (
+    ISigningKey,
+    ISigningKeySet,
+    )
 
 
 @implementer(ISignableArchive)
@@ -72,7 +79,7 @@ class SignableArchive:
     def can_sign(self):
         """See `ISignableArchive`."""
         return (
-            self.archive.signing_key is not None or
+            self.archive.signing_key_fingerprint is not None or
             self._run_parts_dir is not None)
 
     @cachedproperty
@@ -237,9 +244,9 @@ class ArchiveGPGSigningKey(SignableArchive):
         with open(export_path, 'wb') as export_file:
             export_file.write(key.export())
 
-    def generateSigningKey(self, log=None):
+    def generateSigningKey(self, log=None, async_keyserver=False):
         """See `IArchiveGPGSigningKey`."""
-        assert self.archive.signing_key is None, (
+        assert self.archive.signing_key_fingerprint is None, (
             "Cannot override signing_keys.")
 
         # Always generate signing keys for the default PPA, even if it
@@ -257,13 +264,26 @@ class ArchiveGPGSigningKey(SignableArchive):
 
         key_displayname = (
             "Launchpad PPA for %s" % self.archive.owner.displayname)
-        secret_key = getUtility(IGPGHandler).generateKey(
-            key_displayname, logger=log)
-        self._setupSigningKey(secret_key)
+        if getFeatureFlag(PUBLISHER_GPG_USES_SIGNING_SERVICE):
+            try:
+                signing_key = getUtility(ISigningKeySet).generate(
+                    SigningKeyType.OPENPGP, key_displayname,
+                    openpgp_key_algorithm=OpenPGPKeyAlgorithm.RSA, length=4096)
+            except Exception as e:
+                if log is not None:
+                    log.exception(
+                        "Error generating signing key for %s: %s %s" %
+                        (self.archive.reference, e.__class__.__name__, e))
+                raise
+        else:
+            signing_key = getUtility(IGPGHandler).generateKey(
+                key_displayname, logger=log)
+        return self._setupSigningKey(
+            signing_key, async_keyserver=async_keyserver)
 
     def setSigningKey(self, key_path, async_keyserver=False):
         """See `IArchiveGPGSigningKey`."""
-        assert self.archive.signing_key is None, (
+        assert self.archive.signing_key_fingerprint is None, (
             "Cannot override signing_keys.")
         assert os.path.exists(key_path), (
             "%s does not exist" % key_path)
@@ -274,34 +294,46 @@ class ArchiveGPGSigningKey(SignableArchive):
         return self._setupSigningKey(
             secret_key, async_keyserver=async_keyserver)
 
-    def _uploadPublicSigningKey(self, secret_key):
+    def _uploadPublicSigningKey(self, signing_key):
         """Upload the public half of a signing key to the keyserver."""
         # The handler's security proxying doesn't protect anything useful
         # here, and when we're running in a thread we don't have an
         # interaction.
         gpghandler = removeSecurityProxy(getUtility(IGPGHandler))
-        pub_key = gpghandler.retrieveKey(secret_key.fingerprint)
-        gpghandler.uploadPublicKey(pub_key.fingerprint)
-        return pub_key
+        if IPymeKey.providedBy(signing_key):
+            pub_key = gpghandler.retrieveKey(signing_key.fingerprint)
+            gpghandler.uploadPublicKey(pub_key.fingerprint)
+            return pub_key
+        else:
+            assert ISigningKey.providedBy(signing_key)
+            gpghandler.submitKey(removeSecurityProxy(signing_key).public_key)
+            return signing_key
 
     def _storeSigningKey(self, pub_key):
         """Store signing key reference in the database."""
         key_owner = getUtility(ILaunchpadCelebrities).ppa_key_guard
-        key, _ = getUtility(IGPGKeySet).activate(
-            key_owner, pub_key, pub_key.can_encrypt)
-        self.archive.signing_key_owner = key.owner
+        if IPymeKey.providedBy(pub_key):
+            key, _ = getUtility(IGPGKeySet).activate(
+                key_owner, pub_key, pub_key.can_encrypt)
+        else:
+            assert ISigningKey.providedBy(pub_key)
+            key = pub_key
+        self.archive.signing_key_owner = key_owner
         self.archive.signing_key_fingerprint = key.fingerprint
         del get_property_cache(self.archive).signing_key
 
-    def _setupSigningKey(self, secret_key, async_keyserver=False):
+    def _setupSigningKey(self, signing_key, async_keyserver=False):
         """Mandatory setup for signing keys.
 
-        * Export the secret key into the protected disk location.
+        * Export the secret key into the protected disk location (for
+          locally-generated keys).
         * Upload public key to the keyserver.
-        * Store the public GPGKey reference in the database and update
-          the context archive.signing_key.
+        * Store the public GPGKey reference in the database (for
+          locally-generated keys) and update the context
+          archive.signing_key.
         """
-        self.exportSecretKey(secret_key)
+        if IPymeKey.providedBy(signing_key):
+            self.exportSecretKey(signing_key)
         if async_keyserver:
             # If we have an asynchronous keyserver running in the current
             # thread using Twisted, then we need some contortions to ensure
@@ -310,10 +342,10 @@ class ArchiveGPGSigningKey(SignableArchive):
             # Since that thread won't have a Zope interaction, we need to
             # unwrap the security proxy for it.
             d = deferToThread(
-                self._uploadPublicSigningKey, removeSecurityProxy(secret_key))
+                self._uploadPublicSigningKey, removeSecurityProxy(signing_key))
             d.addCallback(ProxyFactory)
             d.addCallback(self._storeSigningKey)
             return d
         else:
-            pub_key = self._uploadPublicSigningKey(secret_key)
+            pub_key = self._uploadPublicSigningKey(signing_key)
             self._storeSigningKey(pub_key)
diff --git a/lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py b/lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py
index 87359e8..67366bd 100644
--- a/lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py
+++ b/lib/lp/archivepublisher/tests/test_archivegpgsigningkey.py
@@ -14,8 +14,15 @@ from testtools.matchers import (
     FileContains,
     StartsWith,
     )
-from testtools.twistedsupport import AsynchronousDeferredRunTest
-from twisted.internet import defer
+from testtools.twistedsupport import (
+    AsynchronousDeferredRunTest,
+    AsynchronousDeferredRunTestForBrokenTwisted,
+    )
+import treq
+from twisted.internet import (
+    defer,
+    reactor,
+    )
 from zope.component import getUtility
 
 from lp.archivepublisher.config import getPubConfig
@@ -26,18 +33,27 @@ from lp.archivepublisher.interfaces.archivegpgsigningkey import (
     )
 from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
 from lp.archivepublisher.tests.test_run_parts import RunPartsMixin
+from lp.registry.interfaces.gpg import IGPGKeySet
 from lp.services.compat import mock
 from lp.services.features.testing import FeatureFixture
+from lp.services.gpg.interfaces import IGPGHandler
+from lp.services.gpg.tests.test_gpghandler import FakeGenerateKey
 from lp.services.log.logger import BufferLogger
 from lp.services.osutils import write_file
 from lp.services.signing.enums import (
     SigningKeyType,
     SigningMode,
     )
+from lp.services.signing.interfaces.signingkey import ISigningKeySet
 from lp.services.signing.tests.helpers import SigningServiceClientFixture
+from lp.services.twistedsupport.testing import TReqFixture
+from lp.services.twistedsupport.treq import check_status
 from lp.soyuz.enums import ArchivePurpose
 from lp.testing import TestCaseWithFactory
-from lp.testing.gpgkeys import gpgkeysdir
+from lp.testing.gpgkeys import (
+    gpgkeysdir,
+    test_pubkey_from_email,
+    )
 from lp.testing.keyserver import InProcessKeyServerFixture
 from lp.testing.layers import ZopelessDatabaseLayer
 
@@ -271,3 +287,89 @@ class TestSignableArchiveWithRunParts(RunPartsMixin, TestCaseWithFactory):
             FileContains(
                 "detached signature of %s (%s, %s/%s)\n" %
                 (filename, self.archive_root, self.distro.name, self.suite)))
+
+
+class TestArchiveGPGSigningKey(TestCaseWithFactory):
+
+    layer = ZopelessDatabaseLayer
+    # treq.content doesn't close the connection before yielding control back
+    # to the test, so we need to spin the reactor at the end to finish
+    # things off.
+    run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory(
+        timeout=10000)
+
+    @defer.inlineCallbacks
+    def setUp(self):
+        super(TestArchiveGPGSigningKey, self).setUp()
+        self.temp_dir = self.makeTemporaryDirectory()
+        self.pushConfig("personalpackagearchive", root=self.temp_dir)
+        self.keyserver = self.useFixture(InProcessKeyServerFixture())
+        yield self.keyserver.start()
+
+    @defer.inlineCallbacks
+    def test_generateSigningKey_local(self):
+        # Generating a signing key locally using GPGHandler stores it in the
+        # database and pushes it to the keyserver.
+        self.useFixture(FakeGenerateKey("ppa-sample@xxxxxxxxxxxxxxxxx"))
+        logger = BufferLogger()
+        # Use a display name that matches the pregenerated sample key.
+        owner = self.factory.makePerson(
+            displayname="Celso \xe1\xe9\xed\xf3\xfa Providelo")
+        archive = self.factory.makeArchive(owner=owner)
+        yield IArchiveGPGSigningKey(archive).generateSigningKey(
+            log=logger, async_keyserver=True)
+        # The key is stored in the database.
+        self.assertIsNotNone(archive.signing_key_owner)
+        self.assertIsNotNone(archive.signing_key_fingerprint)
+        # The key is stored as a GPGKey, not a SigningKey.
+        self.assertIsNotNone(
+            getUtility(IGPGKeySet).getByFingerprint(
+                archive.signing_key_fingerprint))
+        self.assertIsNone(
+            getUtility(ISigningKeySet).get(
+                SigningKeyType.OPENPGP, archive.signing_key_fingerprint))
+        # The key is uploaded to the keyserver.
+        client = self.useFixture(TReqFixture(reactor)).client
+        response = yield client.get(
+            getUtility(IGPGHandler).getURLForKeyInServer(
+                archive.signing_key_fingerprint, "get"))
+        yield check_status(response)
+        content = yield treq.content(response)
+        self.assertIn(b"-----BEGIN PGP PUBLIC KEY BLOCK-----\n", content)
+
+    @defer.inlineCallbacks
+    def test_generateSigningKey_signing_service(self):
+        # Generating a signing key on the signing service stores it in the
+        # database and pushes it to the keyserver.
+        self.useFixture(
+            FeatureFixture({PUBLISHER_GPG_USES_SIGNING_SERVICE: "on"}))
+        signing_service_client = self.useFixture(
+            SigningServiceClientFixture(self.factory))
+        signing_service_client.generate.side_effect = None
+        test_key = test_pubkey_from_email("ftpmaster@xxxxxxxxxxxxx")
+        signing_service_client.generate.return_value = {
+            "fingerprint": "33C0A61893A5DC5EB325B29E415A12CAC2F30234",
+            "public-key": test_key,
+            }
+        logger = BufferLogger()
+        archive = self.factory.makeArchive()
+        yield IArchiveGPGSigningKey(archive).generateSigningKey(
+            log=logger, async_keyserver=True)
+        # The key is stored in the database.
+        self.assertIsNotNone(archive.signing_key_owner)
+        self.assertIsNotNone(archive.signing_key_fingerprint)
+        # The key is stored as a SigningKey, not a GPGKey.
+        self.assertIsNone(
+            getUtility(IGPGKeySet).getByFingerprint(
+                archive.signing_key_fingerprint))
+        signing_key = getUtility(ISigningKeySet).get(
+            SigningKeyType.OPENPGP, archive.signing_key_fingerprint)
+        self.assertEqual(test_key, signing_key.public_key)
+        # The key is uploaded to the keyserver.
+        client = self.useFixture(TReqFixture(reactor)).client
+        response = yield client.get(
+            getUtility(IGPGHandler).getURLForKeyInServer(
+                archive.signing_key_fingerprint, "get"))
+        yield check_status(response)
+        content = yield treq.content(response)
+        self.assertIn(test_key, content)
diff --git a/lib/lp/services/gpg/handler.py b/lib/lp/services/gpg/handler.py
index b2bcbad..e3eba45 100644
--- a/lib/lp/services/gpg/handler.py
+++ b/lib/lp/services/gpg/handler.py
@@ -489,12 +489,8 @@ class GPGHandler:
             raise GPGKeyExpired(key)
         return key
 
-    def _submitKey(self, content):
-        """Submit an ASCII-armored public key export to the keyserver.
-
-        It issues a POST at /pks/add on the keyserver specified in the
-        configuration.
-        """
+    def submitKey(self, content):
+        """See `IGPGHandler`."""
         keyserver_http_url = '%s:%s' % (
             config.gpghandler.host, config.gpghandler.port)
 
@@ -527,7 +523,7 @@ class GPGHandler:
             return
 
         pub_key = self.retrieveKey(fingerprint)
-        self._submitKey(pub_key.export())
+        self.submitKey(pub_key.export())
 
     def getURLForKeyInServer(self, fingerprint, action='index', public=False):
         """See IGPGHandler"""
diff --git a/lib/lp/services/gpg/interfaces.py b/lib/lp/services/gpg/interfaces.py
index 78b44c8..d6f0f73 100644
--- a/lib/lp/services/gpg/interfaces.py
+++ b/lib/lp/services/gpg/interfaces.py
@@ -357,6 +357,17 @@ class IGPGHandler(Interface):
         :return: a `PymeKey`object containing the key information.
         """
 
+    def submitKey(content):
+        """Submit an ASCII-armored public key export to the keyserver.
+
+        It issues a POST at /pks/add on the keyserver specified in the
+        configuration.
+
+        :param content: The exported public key, as a byte string.
+        :raise GPGUploadFailure: if the keyserver could not be reached.
+        :raise AssertionError: if the POST request failed.
+        """
+
     def uploadPublicKey(fingerprint):
         """Upload the specified public key to a keyserver.
 
@@ -365,8 +376,8 @@ class IGPGHandler(Interface):
 
         :param fingerprint: The key fingerprint, which must be an hexadecimal
             string.
-        :raise GPGUploadFailure: if the keyserver could not be reaches.
-        :raise AssertionError: if the POST request doesn't succeed.
+        :raise GPGUploadFailure: if the keyserver could not be reached.
+        :raise AssertionError: if the POST request failed.
         """
 
     def localKeys(filter=None, secret=False):
diff --git a/lib/lp/services/signing/tests/helpers.py b/lib/lp/services/signing/tests/helpers.py
index f819745..6831edb 100644
--- a/lib/lp/services/signing/tests/helpers.py
+++ b/lib/lp/services/signing/tests/helpers.py
@@ -49,7 +49,7 @@ class SigningServiceClientFixture(fixtures.Fixture):
                   openpgp_key_algorithm=None, length=None):
         key = bytes(PrivateKey.generate().public_key)
         data = {
-            "fingerprint": self.factory.getUniqueHexString(40),
+            "fingerprint": self.factory.getUniqueHexString(40).upper(),
             "public-key": key,
             }
         self.generate_returns.append((key_type, data))
@@ -69,7 +69,7 @@ class SigningServiceClientFixture(fixtures.Fixture):
 
     def _inject(self, key_type, private_key, public_key, description,
                 created_at):
-        data = {'fingerprint': self.factory.getUniqueHexString(40)}
+        data = {'fingerprint': self.factory.getUniqueHexString(40).upper()}
         self.inject_returns.append(data)
         return data
 
diff --git a/lib/lp/soyuz/configure.zcml b/lib/lp/soyuz/configure.zcml
index 2e97958..643a85b 100644
--- a/lib/lp/soyuz/configure.zcml
+++ b/lib/lp/soyuz/configure.zcml
@@ -373,7 +373,6 @@
             set_schema="lp.soyuz.interfaces.archive.IArchiveRestricted"/>
         <require
             permission="launchpad.InternalScriptsOnly"
-            attributes="signing_key_owner"
             set_attributes="dirty_suites distribution signing_key_owner
                             signing_key_fingerprint"/>
     </class>
diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py
index 407f953..b6cc663 100644
--- a/lib/lp/soyuz/interfaces/archive.py
+++ b/lib/lp/soyuz/interfaces/archive.py
@@ -461,6 +461,8 @@ class IArchiveSubscriberView(Interface):
             "explicit publish flag and any other constraints."))
     series_with_sources = Attribute(
         "DistroSeries to which this archive has published sources")
+    signing_key_owner = Reference(
+        title=_("Archive signing key owner"), required=False, schema=IPerson)
     signing_key_fingerprint = exported(
         Text(
             title=_("Archive signing key fingerprint"), required=False,
diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py
index a37b737..0d58e56 100644
--- a/lib/lp/soyuz/model/archive.py
+++ b/lib/lp/soyuz/model/archive.py
@@ -466,7 +466,7 @@ class Archive(SQLBase):
         return (
             not config.personalpackagearchive.require_signing_keys or
             not self.is_ppa or
-            self.signing_key is not None)
+            self.signing_key_fingerprint is not None)
 
     @property
     def reference(self):
@@ -2717,10 +2717,12 @@ class ArchiveSet:
                     (owner.name, distribution.name, name))
 
         # Signing-key for the default PPA is reused when it's already present.
-        signing_key = None
+        signing_key_owner = None
+        signing_key_fingerprint = None
         if purpose == ArchivePurpose.PPA:
             if owner.archive is not None:
-                signing_key = owner.archive.signing_key
+                signing_key_owner = owner.archive.signing_key_owner
+                signing_key_fingerprint = owner.archive.signing_key_fingerprint
             else:
                 # owner.archive is a cached property and we've just cached it.
                 del get_property_cache(owner).archive
@@ -2729,9 +2731,8 @@ class ArchiveSet:
             owner=owner, distribution=distribution, name=name,
             displayname=displayname, description=description,
             purpose=purpose, publish=publish,
-            signing_key_owner=signing_key.owner if signing_key else None,
-            signing_key_fingerprint=(
-                signing_key.fingerprint if signing_key else None),
+            signing_key_owner=signing_key_owner,
+            signing_key_fingerprint=signing_key_fingerprint,
             require_virtualized=require_virtualized)
 
         # Upon creation archives are enabled by default.
diff --git a/lib/lp/soyuz/scripts/ppakeygenerator.py b/lib/lp/soyuz/scripts/ppakeygenerator.py
index 190b4a0..88e1d84 100644
--- a/lib/lp/soyuz/scripts/ppakeygenerator.py
+++ b/lib/lp/soyuz/scripts/ppakeygenerator.py
@@ -34,7 +34,7 @@ class PPAKeyGenerator(LaunchpadCronScript):
             (archive.reference, archive.displayname))
         archive_signing_key = IArchiveGPGSigningKey(archive)
         archive_signing_key.generateSigningKey(log=self.logger)
-        self.logger.info("Key %s" % archive.signing_key.fingerprint)
+        self.logger.info("Key %s" % archive.signing_key_fingerprint)
 
     def main(self):
         """Generate signing keys for the selected PPAs."""
@@ -45,11 +45,11 @@ class PPAKeyGenerator(LaunchpadCronScript):
                 raise LaunchpadScriptFailure(
                     "No archive named '%s' could be found."
                     % self.options.archive)
-            if archive.signing_key is not None:
+            if archive.signing_key_fingerprint is not None:
                 raise LaunchpadScriptFailure(
                     "%s (%s) already has a signing_key (%s)"
                     % (archive.reference, archive.displayname,
-                       archive.signing_key.fingerprint))
+                       archive.signing_key_fingerprint))
             archives = [archive]
         else:
             archive_set = getUtility(IArchiveSet)
diff --git a/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py b/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py
index 56e8710..a5d3caf 100644
--- a/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py
+++ b/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py
@@ -83,7 +83,7 @@ class TestPPAKeyGenerator(TestCase):
             LaunchpadScriptFailure,
             ("~cprov/ubuntu/ppa (PPA for Celso Providelo) already has a "
              "signing_key (%s)" %
-             cprov.archive.signing_key.fingerprint),
+             cprov.archive.signing_key_fingerprint),
             key_generator.main)
 
     def testGenerateKeyForASinglePPA(self):
@@ -95,14 +95,14 @@ class TestPPAKeyGenerator(TestCase):
         cprov = getUtility(IPersonSet).getByName('cprov')
         self._fixArchiveForKeyGeneration(cprov.archive)
 
-        self.assertTrue(cprov.archive.signing_key is None)
+        self.assertIsNone(cprov.archive.signing_key_fingerprint)
 
         txn = FakeTransaction()
         key_generator = self._getKeyGenerator(
             archive_reference='~cprov/ubuntutest/ppa', txn=txn)
         key_generator.main()
 
-        self.assertTrue(cprov.archive.signing_key is not None)
+        self.assertIsNotNone(cprov.archive.signing_key_fingerprint)
         self.assertEqual(txn.commit_count, 1)
 
     def testGenerateKeyForAllPPA(self):
@@ -115,13 +115,13 @@ class TestPPAKeyGenerator(TestCase):
 
         for archive in archives:
             self._fixArchiveForKeyGeneration(archive)
-            self.assertTrue(archive.signing_key is None)
+            self.assertIsNone(archive.signing_key_fingerprint)
 
         txn = FakeTransaction()
         key_generator = self._getKeyGenerator(txn=txn)
         key_generator.main()
 
         for archive in archives:
-            self.assertTrue(archive.signing_key is not None)
+            self.assertIsNotNone(archive.signing_key_fingerprint)
 
         self.assertEqual(txn.commit_count, len(archives))
diff --git a/lib/lp/soyuz/tests/test_archive.py b/lib/lp/soyuz/tests/test_archive.py
index 91acf53..5c3f3b6 100644
--- a/lib/lp/soyuz/tests/test_archive.py
+++ b/lib/lp/soyuz/tests/test_archive.py
@@ -4046,7 +4046,7 @@ class TestSigningKeyPropagation(TestCaseWithFactory):
 
     def test_ppa_created_with_no_signing_key(self):
         ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
-        self.assertIsNone(ppa.signing_key)
+        self.assertIsNone(ppa.signing_key_fingerprint)
 
     def test_default_signing_key_propagated_to_new_ppa(self):
         person = self.factory.makePerson()