Browse Source

feat(api): in Domain.keys, return unmanaged DNSSEC keys

Peter Thomassen 3 years ago
parent
commit
ea63dde0b2

+ 17 - 1
api/desecapi/models.py

@@ -339,7 +339,23 @@ class Domain(ExportModelOperationsMixin('Domain'), models.Model):
     @property
     def keys(self):
         if not self._keys:
-            self._keys = pdns.get_keys(self)
+            self._keys = [{**key, 'managed': True} for key in pdns.get_keys(self)]
+            try:
+                unmanaged_keys = self.rrset_set.get(subname='', type='DNSKEY').records.all()
+            except RRset.DoesNotExist:
+                pass
+            else:
+                name = dns.name.from_text(self.name)
+                for rr in unmanaged_keys:
+                    key = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DNSKEY, rr.content)
+                    key_is_sep = key.flags & dns.rdtypes.ANY.DNSKEY.SEP
+                    self._keys.append({
+                        'dnskey': rr.content,
+                        'ds': [dns.dnssec.make_ds(name, key, algo).to_text() for algo in (2, 4)] if key_is_sep else [],
+                        'flags': key.flags,  # deprecated
+                        'keytype': None,  # deprecated
+                        'managed': False,
+                    })
         return self._keys
 
     @property

+ 35 - 19
api/desecapi/tests/base.py

@@ -427,31 +427,47 @@ class MockPDNSTestCase(APITestCase):
             }),
         }
 
+    @staticmethod
+    def get_body_pdns_zone_retrieve_crypto_keys():
+        common_body = {
+            'algorithm': 'ECDSAP256SHA256',
+            'bits': 256,
+            'dnskey': '257 3 13 EVBcsqrnOp6RGWtsrr9QW8cUtt/WI5C81RcCZDTGNI9elAiMQlxRdnic+7V+b7jJDE2vgY08qAbxiNh5NdzkzA==',
+            'id': 179425943,
+            'published': True,
+            'type': 'Cryptokey',
+        }
+        common_cds = [
+            '62745 13 2 5cddaeaa383e2ea7de49bd1212bf520228f0e3b334626517e5f6a68eb85b48f6',
+            '62745 13 4 b3f2565901ddcb0b78337301cf863d1045774377bca05c7ad69e17a167734b929f0a49b7edcca913eb6f5dfeac4645b8'
+        ]
+        return [
+            {
+                **common_body,
+                'flags': 257,
+                'keytype': 'csk',
+                'cds': common_cds,
+            },
+            {
+                **common_body,
+                'flags': 257,
+                'keytype': 'ksk',
+                'cds': common_cds,
+            },
+            {
+                **common_body,
+                'flags': 256,
+                'keytype': 'zsk',
+            },
+        ]
+
     @classmethod
     def request_pdns_zone_retrieve_crypto_keys(cls, name=None):
         return {
             'method': 'GET',
             'uri': cls.get_full_pdns_url(cls.PDNS_ZONE_CRYPTO_KEYS, id=cls._pdns_zone_id_heuristic(name)),
             'status': 200,
-            'body': json.dumps([
-                {
-                    'algorithm': 'ECDSAP256SHA256',
-                    'bits': 256,
-                    'dnskey': '257 3 13 EVBcsqrnOp6RGWtsrr9QW8cUtt/'
-                              'WI5C81RcCZDTGNI9elAiMQlxRdnic+7V+b7jJDE2vgY08qAbxiNh5NdzkzA==',
-                    'cds': [
-                        '62745 13 2 5cddaeaa383e2ea7de49bd1212bf520228f0e3b334626517e5f6a68eb85b48f6',
-                        '62745 13 4 b3f2565901ddcb0b78337301cf863d1045774377bca05c7ad69e17a167734b92'
-                        '9f0a49b7edcca913eb6f5dfeac4645b8'
-                    ],
-                    'flags': 257,
-                    'id': 179425943,
-                    'keytype': key_type,
-                    'published': True,
-                    'type': 'Cryptokey',
-                }
-                for key_type in ['csk', 'ksk', 'zsk']
-            ])
+            'body': json.dumps(cls.get_body_pdns_zone_retrieve_crypto_keys())
         }
 
     @classmethod

+ 1 - 0
api/desecapi/tests/test_domains.py

@@ -255,6 +255,7 @@ class DomainOwnerTestCase1(DomainOwnerTestCase):
         ):
             response = self.client.get(url)
             self.assertStatus(response, status.HTTP_200_OK)
+            self.assertEqual(response.data.keys(), {'created', 'keys', 'minimum_ttl', 'name', 'published', 'touched'})
             self.assertEqual(response.data['name'], self.my_domain.name)
             self.assertTrue(isinstance(response.data['keys'], list))
 

+ 54 - 1
api/desecapi/tests/test_rrsets.py

@@ -8,7 +8,7 @@ from django.core.exceptions import ValidationError
 from django.core.management import call_command
 from rest_framework import status
 
-from desecapi.models import Domain, RRset, RR_SET_TYPES_AUTOMATIC, RR_SET_TYPES_UNSUPPORTED
+from desecapi.models import Domain, RR, RRset, RR_SET_TYPES_AUTOMATIC, RR_SET_TYPES_UNSUPPORTED
 from desecapi.tests.base import DesecTestCase, AuthenticatedRRSetBaseTestCase
 
 
@@ -942,3 +942,56 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             self.assertStatus(response, status.HTTP_200_OK)
             self.assertEqual(len(response.data), 1, response.data)
             self.assertContainsRRSets(response.data, [dict(subname='', records=settings.DEFAULT_NS, type='NS')])
+
+    def test_extra_dnskeys(self):
+        name = 'ietf.org'
+        dnskeys = [
+            '256 3 5 AwEAAdDECajHaTjfSoNTY58WcBah1BxPKVIHBz4IfLjfqMvium4lgKtKZLe97DgJ5/NQrNEGGQmr6fKvUj67cfrZUojZ2cGRiz'
+            'VhgkOqZ9scaTVXNuXLM5Tw7VWOVIceeXAuuH2mPIiEV6MhJYUsW6dvmNsJ4XwCgNgroAmXhoMEiWEjBB+wjYZQ5GtZHBFKVXACSWTiCtdd'
+            'HcueOeSVPi5WH94VlubhHfiytNPZLrObhUCHT6k0tNE6phLoHnXWU+6vpsYpz6GhMw/R9BFxW5PdPFIWBgoWk2/XFVRSKG9Lr61b2z1R12'
+            '6xeUwvw46RVy3hanV3vNO7LM5HniqaYclBbhk=',
+            '257 3 5 AwEAAavjQ1H6pE8FV8LGP0wQBFVL0EM9BRfqxz9p/sZ+8AByqyFHLdZcHoOGF7CgB5OKYMvGOgysuYQloPlwbq7Ws5WywbutbX'
+            'yG24lMWy4jijlJUsaFrS5EvUu4ydmuRc/TGnEXnN1XQkO+waIT4cLtrmcWjoY8Oqud6lDaJdj1cKr2nX1NrmMRowIu3DIVtGbQJmzpukpD'
+            'VZaYMMAm8M5vz4U2vRCVETLgDoQ7rhsiD127J8gVExjO8B0113jCajbFRcMtUtFTjH4z7jXP2ZzDcXsgpe4LYFuenFQAcRBRlE6oaykHR7'
+            'rlPqqmw58nIELJUFoMcb/BdRLgbyTeurFlnxs=',
+        ]
+        expected_ds = [
+            '45586 5 2 67fcd7e0b9e0366309f3b6f7476dff931d5226edc5348cd80fd82a081dfcf6ee',
+            '45586 5 4 aee6931c7790c428bca35dab9179cb27f042715e38e5a8adb6bb24c57c21c65dbd02a5b09887787f30128bfac8b6f0b5'
+        ]
+
+        domain = Domain.objects.create(name=name, owner=self.owner)
+        rrset = domain.rrset_set.create(subname='', type='DNSKEY', ttl=3600)
+        rrset.records.bulk_create([RR(rrset=rrset, content=dnskey) for dnskey in dnskeys])
+
+        url = self.reverse('v1:domain-detail', name=domain.name)
+        with self.assertPdnsRequests(
+            self.request_pdns_zone_retrieve_crypto_keys(name=domain.name)
+        ):
+            response = self.client.get(url)
+            self.assertStatus(response, status.HTTP_200_OK)
+            self.assertEqual(response.data['keys'], [
+                {
+                    'dnskey': key['dnskey'],
+                    'ds': key['cds'] if key['flags'] & 1 else [],
+                    'flags': key['flags'],
+                    'keytype': key['keytype'],
+                    'managed': True,
+                }
+                for key in self.get_body_pdns_zone_retrieve_crypto_keys()
+            ] + [
+                {
+                    'dnskey': dnskeys[0],
+                    'ds': [],
+                    'flags': 256,
+                    'keytype': None,
+                    'managed': False,
+                },
+                {
+                    'dnskey': dnskeys[1],
+                    'ds': expected_ds,
+                    'flags': 257,
+                    'keytype': None,
+                    'managed': False,
+                }
+            ])

+ 9 - 4
docs/dns/domains.rst

@@ -28,7 +28,8 @@ A JSON object representing a domain has the following structure::
                     "6006 13 4 2fdcf8..."
                 ],
                 "flags": 257,  # deprecated
-                "keytype": "csk"  # deprecated
+                "keytype": "csk",  # deprecated
+                "managed": true
             },
             ...
         ],
@@ -51,9 +52,9 @@ Field details:
 
     Array with DNSSEC public key information.  Each entry contains ``DNSKEY``
     and ``DS`` record contents.  For delegation of DNSSEC-secured domains,
-    the parent domain needs to publish these ``DS`` records.  (This usually
-    involves telling your registrar/registry about those records, and they
-    will publish them for you.)
+    the parent domain should publish the combined list of ``DS`` records.
+    (This usually involves telling your registrar/registry about those
+    records, and they will publish them for you.)
 
     Notes:
 
@@ -61,6 +62,10 @@ Field details:
       a specific domain.  In contrast, when listing all domains, the ``keys``
       field is omitted for performance reasons.
 
+    - The ``managed`` field differentiates keys managed by deSEC (``true``)
+      from any additional keys the user may have added (``false``, see
+      :ref:`DNSKEY caveat <DNSKEY caveat>`).
+
     - ``DS`` values are calculated for each applicable key by applying hash
       algorithms 2 (SHA-256) and 4 (SHA-384), respectively.
       For keys not suitable for delegation (indicated by the first field