Skip to content

Commit f09f0bc

Browse files
committed
work in progress
1 parent ec712e6 commit f09f0bc

File tree

13 files changed

+546
-0
lines changed

13 files changed

+546
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 3.1.5 on 2021-01-30 15:24
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
import uuid
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('desecapi', '0012_rrset_label_length'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='TLSIdentity',
18+
fields=[
19+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
20+
('name', models.CharField(max_length=24)),
21+
('created', models.DateTimeField(auto_now_add=True)),
22+
('certificate', models.TextField()),
23+
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='identities', to=settings.AUTH_USER_MODEL)),
24+
],
25+
options={
26+
'abstract': False,
27+
},
28+
),
29+
]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Generated by Django 3.1.5 on 2021-01-31 13:00
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('desecapi', '0013_identities'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='tlsidentity',
15+
name='port',
16+
field=models.IntegerField(default=443),
17+
),
18+
migrations.AddField(
19+
model_name='tlsidentity',
20+
name='protocol',
21+
field=models.TextField(choices=[('tcp', 'Tcp'), ('udp', 'Udp'), ('sctp', 'Sctp')], default='tcp'),
22+
),
23+
migrations.AddField(
24+
model_name='tlsidentity',
25+
name='scheduled_removal',
26+
field=models.DateTimeField(null=True),
27+
),
28+
migrations.AddField(
29+
model_name='tlsidentity',
30+
name='tlsa_certificate_usage',
31+
field=models.IntegerField(choices=[(0, 'Ca Constraint'), (1, 'Service Certificate Constraint'), (2, 'Trust Anchor Assertion'), (3, 'Domain Issued Certificate')], default=3),
32+
),
33+
migrations.AddField(
34+
model_name='tlsidentity',
35+
name='tlsa_matching_type',
36+
field=models.IntegerField(choices=[(0, 'No Hash Used'), (1, 'Sha256'), (2, 'Sha512')], default=1),
37+
),
38+
migrations.AddField(
39+
model_name='tlsidentity',
40+
name='tlsa_selector',
41+
field=models.IntegerField(choices=[(0, 'Full Certificate'), (1, 'Subject Public Key Info')], default=1),
42+
),
43+
]

api/desecapi/models.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
from datetime import timedelta
1313
from functools import cached_property
1414
from hashlib import sha256
15+
from typing import Set
1516

1617
import dns
1718
import psl_dns
1819
import rest_framework.authtoken.models
20+
from cryptography import x509, hazmat
1921
from django.conf import settings
2022
from django.contrib.auth.hashers import make_password
2123
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
@@ -939,3 +941,132 @@ def verify(self, solution: str):
939941
and
940942
age <= settings.CAPTCHA_VALIDITY_PERIOD # not expired
941943
)
944+
945+
946+
class Identity(models.Model):
947+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
948+
name = models.CharField(max_length=24)
949+
created = models.DateTimeField(auto_now_add=True)
950+
owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='identities')
951+
952+
class Meta:
953+
abstract = True
954+
955+
956+
class TLSIdentity(Identity):
957+
958+
class CertificateUsage(models.Choices):
959+
CA_CONSTRAINT = 0
960+
SERVICE_CERTIFICATE_CONSTRAINT = 1
961+
TRUST_ANCHOR_ASSERTION = 2
962+
DOMAIN_ISSUED_CERTIFICATE = 3
963+
964+
class Selector(models.Choices):
965+
FULL_CERTIFICATE = 0
966+
SUBJECT_PUBLIC_KEY_INFO = 1
967+
968+
class MatchingType(models.Choices):
969+
NO_HASH_USED = 0
970+
SHA256 = 1
971+
SHA512 = 2
972+
973+
class Protocol(models.TextChoices):
974+
TCP = 'tcp'
975+
UDP = 'udp'
976+
SCTP = 'sctp'
977+
978+
certificate = models.TextField()
979+
980+
tlsa_selector = models.IntegerField(choices=Selector.choices, default=Selector.SUBJECT_PUBLIC_KEY_INFO)
981+
tlsa_matching_type = models.IntegerField(choices=MatchingType.choices, default=MatchingType.SHA256)
982+
tlsa_certificate_usage = models.IntegerField(choices=CertificateUsage.choices,
983+
default=CertificateUsage.DOMAIN_ISSUED_CERTIFICATE)
984+
985+
port = models.IntegerField(default=443)
986+
protocol = models.TextField(choices=Protocol.choices, default=Protocol.TCP)
987+
988+
scheduled_removal = models.DateTimeField(null=True)
989+
990+
def __init__(self, *args, **kwargs):
991+
super().__init__(*args, **kwargs)
992+
if 'not_valid_after' not in kwargs:
993+
self.scheduled_removal = self.not_valid_after
994+
995+
@property
996+
def tlsa_record(self) -> str:
997+
# choose hash function
998+
if self.tlsa_matching_type == 1:
999+
hash_function = hazmat.primitives.hashes.SHA256()
1000+
elif self.tlsa_matching_type == 2:
1001+
hash_function = hazmat.primitives.hashes.SHA512()
1002+
else:
1003+
raise NotImplementedError
1004+
1005+
# choose data to hash
1006+
if self.tlsa_selector == 0:
1007+
to_be_hashed = self._cert.public_key().public_bytes(
1008+
hazmat.primitives.serialization.Encoding.DER,
1009+
hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo
1010+
)
1011+
else:
1012+
raise NotImplementedError
1013+
1014+
# compute the hash
1015+
h = hazmat.primitives.hashes.Hash(hash_function)
1016+
h.update(to_be_hashed)
1017+
hash = h.finalize.hex()
1018+
1019+
# create TLSA record
1020+
return f"{self.tlsa_certificate_usage:n} {self.tlsa_selector:n} {self.tlsa_matching_type:n} {hash}"
1021+
1022+
@property
1023+
def _cert(self) -> x509.Certificate:
1024+
return x509.load_pem_x509_certificate(self.certificate.encode())
1025+
1026+
@property
1027+
def fingerprint(self) -> str:
1028+
return self._cert.fingerprint(hazmat.primitives.hashes.SHA256()).hex()
1029+
1030+
@property
1031+
def subject_names(self) -> Set[str]:
1032+
subject_names = {
1033+
x.value for x in
1034+
self._cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)
1035+
}
1036+
1037+
try:
1038+
subject_alternative_names = {
1039+
x for x in
1040+
self._cert.extensions.get_extension_for_oid(
1041+
x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME).value.get_values_for_type(x509.DNSName)
1042+
}
1043+
except x509.extensions.ExtensionNotFound:
1044+
subject_alternative_names = set()
1045+
1046+
return subject_names | subject_alternative_names
1047+
1048+
@property
1049+
def domains_subnames(self):
1050+
domains_subnames = []
1051+
for name in self.subject_names:
1052+
# filter names for valid domain names
1053+
try:
1054+
validate_domain_name[1](name)
1055+
except ValidationError:
1056+
continue
1057+
1058+
# find user-owned parent domain
1059+
domain = 'example.dedyn.io'
1060+
subname = '*'
1061+
1062+
# return subname, domain pair
1063+
domains_subnames.append((subname, domain))
1064+
return domains_subnames
1065+
1066+
@property
1067+
def not_valid_before(self):
1068+
return self._cert.not_valid_before
1069+
1070+
@property
1071+
def not_valid_after(self):
1072+
return self._cert.not_valid_after

api/desecapi/serializers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,3 +831,11 @@ class AuthenticatedRenewDomainBasicUserActionSerializer(AuthenticatedDomainBasic
831831

832832
class Meta(AuthenticatedDomainBasicUserActionSerializer.Meta):
833833
model = models.AuthenticatedRenewDomainBasicUserAction
834+
835+
836+
class TLSIdentitySerializer(serializers.ModelSerializer):
837+
838+
class Meta:
839+
model = models.TLSIdentity
840+
fields = ('id', 'name', 'certificate', 'created', 'tlsa_record', 'fingerprint', 'not_valid_before', 'not_valid_after', 'subject_names')
841+
read_only_fields = ('id', 'created', 'tlsa_record', 'fingerprint', 'not_valid_before', 'not_valid_after', 'subject_names')
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from desecapi.tests.base import DesecTestCase
2+
3+
4+
class TLSAIdentityTest(DesecTestCase):
5+
pass

api/desecapi/urls/version_1.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
tokens_router = SimpleRouter()
77
tokens_router.register(r'', views.TokenViewSet, basename='token')
88

9+
identities_router = SimpleRouter()
10+
identities_router.register(r'tls', views.TLSIdentityViewSet, basename='identities-tls')
11+
912
auth_urls = [
1013
# User management
1114
path('', views.AccountCreateView.as_view(), name='register'),
@@ -56,6 +59,9 @@
5659

5760
# CAPTCHA
5861
path('captcha/', views.CaptchaView.as_view(), name='captcha'),
62+
63+
# Identities management
64+
path('identities/', include(identities_router.urls)),
5965
]
6066

6167
app_name = 'desecapi'

api/desecapi/views.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,3 +746,21 @@ def finalize(self):
746746
class CaptchaView(generics.CreateAPIView):
747747
serializer_class = serializers.CaptchaSerializer
748748
throttle_scope = 'account_management_passive'
749+
750+
751+
class IdentityViewSet(IdempotentDestroyMixin, viewsets.ModelViewSet):
752+
permission_classes = (IsAuthenticated, IsOwner,)
753+
754+
def perform_create(self, serializer):
755+
serializer.save(owner=self.request.user)
756+
757+
758+
class TLSIdentityViewSet(IdentityViewSet):
759+
serializer_class = serializers.TLSIdentitySerializer
760+
761+
@property
762+
def throttle_scope(self):
763+
return 'dns_api_read' if self.request.method in SAFE_METHODS else 'dns_api_write'
764+
765+
def get_queryset(self):
766+
return self.request.user.identities.all() # TODO filter for TLS

webapp/src/App.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ export default {
235235
'name': 'tokens',
236236
'text': 'Token Management',
237237
},
238+
'dane': {
239+
'name': 'dane',
240+
'text': 'DANE Management',
241+
}
238242
},
239243
tabmenumore: {
240244
'change-email': {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<template>
2+
<v-textarea
3+
:label="label"
4+
:disabled="disabled || readonly"
5+
:error-messages="errorMessages"
6+
:value="value"
7+
:type="type || ''"
8+
:placeholder="required ? '' : '(optional)'"
9+
:hint="hint"
10+
persistent-hint
11+
:required="required"
12+
:rules="[v => !required || !!v || 'Required.']"
13+
@input="changed('input', $event)"
14+
@input.native="$emit('dirty', $event)"
15+
@keyup="changed('keyup', $event)"
16+
/>
17+
</template>
18+
19+
<script>
20+
export default {
21+
name: 'MultilineText',
22+
props: {
23+
disabled: {
24+
type: Boolean,
25+
required: false,
26+
},
27+
errorMessages: {
28+
type: [String, Array],
29+
default: () => [],
30+
},
31+
hint: {
32+
type: String,
33+
default: '',
34+
},
35+
label: {
36+
type: String,
37+
required: false,
38+
},
39+
readonly: {
40+
type: Boolean,
41+
required: false,
42+
},
43+
required: {
44+
type: Boolean,
45+
default: false,
46+
},
47+
value: {
48+
type: [String, Number],
49+
required: false,
50+
},
51+
type: {
52+
type: String,
53+
required: false,
54+
},
55+
},
56+
methods: {
57+
changed(event, e) {
58+
this.$emit(event, e);
59+
this.$emit('dirty');
60+
},
61+
},
62+
};
63+
</script>
64+
65+
<style>
66+
/* Removes dropdown icon from read-only select */
67+
.v-application--is-ltr .v-text-field.v-input--is-disabled .v-input__append-inner {
68+
display: none;
69+
}
70+
/* remove underline from disabled text fields so they look like regular text */
71+
:not(v-select).theme--light.v-text-field.v-input--is-disabled .v-input__slot::before {
72+
content: none;
73+
}
74+
/* display disabled text fields in normal color */
75+
.theme--light.v-input--is-disabled input {
76+
color: rgba(0, 0, 0, 0.87);
77+
}
78+
</style>

webapp/src/router/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ const routes = [
123123
component: () => import(/* webpackChunkName: "gui" */ '../views/Domain/CrudDomain.vue'),
124124
meta: {guest: false},
125125
},
126+
{
127+
path: '/dane',
128+
name: 'dane',
129+
component: () => import(/* webpackChunkName: "gui" */ '../views/DaneHome.vue'),
130+
meta: {guest: false},
131+
},
126132
]
127133

128134
const router = new VueRouter({

0 commit comments

Comments
 (0)