Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for RADIUS TLS-PSK #108

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions djnro/local_settings.py.dist
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ NRO_PROV_SOCIAL_MEDIA_CONTACT = [

# Helpdesk, used in base.html:
NRO_DOMAIN_HELPDESK_DICT = {"name": _ld({'en': "Domain Helpdesk"}), 'email':'[email protected]', 'phone': '12324567890', 'uri': 'helpdesk.example.com'}
# ream used to generate a TLS-PSK identity for service providers
NRO_TLSPSK_REALM = "example.com"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come the default values for NRO_TLSPSK_REALM is different in settings.py than it is here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Razorfang , this is a change @ghalse did on my suggestion.

The value in settings.py is an intentionally invalid value that is used only when local_settings.py does not provide a value for NRO_TLSPSK_REALM and is there to prevent the code from breaking.

This is a value to be customised by local deployments. Existing deployments that do not update local_settings.py will get the clearly invalid value.

As the two values server different purposes, it makes sense to me to have them different.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as @vladimir-mencl-eresearch suggests, they're intentionally different. The local_settings.py.dist file contains example values and uses the same example domain as the rest of the file (and fortunately that's already a documentation domain). The settings.py file uses an intentionally invalid value and uses a domain reserved for that purpose. Both are consistent with RFC2606.


#Countries for Realm model:
REALM_COUNTRIES = (
Expand Down
3 changes: 3 additions & 0 deletions djnro/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@
'entities': '160,nbsp,173,shy,8194,ensp,8195,emsp,8201,thinsp,8204,zwnj,8205,zwj,8206,lrm,8207,rlm',
}

# to gracefully handle upgrades, a default but definately invalid
# ream used to generate a TLS-PSK identity for service providers
NRO_TLSPSK_REALM = "set.tlspsk.realm.invalid"

#Name_i18n, URL_i18n, language choice field
# If it's the same with LANGUAGES, simply do URL_NAME_LANGS = LANGUAGES
Expand Down
20 changes: 19 additions & 1 deletion djnro/templates/edumanage/server_details.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,25 @@ <h4>{{server.get_name}}</h4>
{{ server.proto }}
</div>
</div>
<div class="control-group">
{% if server.proto in RADPROTOS.TLSPSK %}
{% if server.ertype in ERTYPE_ROLES.IDP %}
<div class="control-group">
<label class="control-label" for="id_psk_identity">{% trans "Your TLS-PSK Identity" %}</label>
<div class="controls">
{{ server.psk_identity }}
</div>
</div>
{% endif %}
{% if server.ertype in ERTYPE_ROLES.SP %}
<div class="control-group">
<label class="control-label" for="id_psk_identity">{% trans "Our TLS-PSK Identity" %}</label>
<div class="controls">
{{ institution.instid }}@{{ OUR_TLSPSK_REALM }}
</div>
</div>
{% endif %}
{% endif %}
<div class="control-group">
<div class="controls">
<a class="btn btn-primary" href="{% url 'edit-servers' server.pk %}">{% trans "Edit" %}</a>
</div>
Expand Down
47 changes: 46 additions & 1 deletion djnro/templates/edumanage/servers_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ <h4>{% if edit %}{{form.instance.get_name}} ({% trans "edit" %}){% else %}{% tra
<div class="form-group {% if form.secret.errors %} error {% endif %}">
<label class="control-label" for="id_secret"><b>{% trans "Secret" %}</b></label>
<div class="controls">
<input type="password" maxlength="80" name="secret" id="id_secret" {% if edit %}value='{{form.instance.secret}}'{% endif %} {% if form.data.secret %}value='{{form.data.secret}}'{% endif %}>
<input type="password" autocomplete="off" maxlength="80" name="secret" id="id_secret" {% if edit %}value='{{form.instance.secret}}'{% endif %} {% if form.data.secret %}value='{{form.data.secret}}'{% endif %}>
{% if form.secret.errors %} <div class="alert-danger"> {{ form.secret.errors|join_with_linebreaks }} </div>
{% endif %} <span class="help-block">{{ form.secret.help_text }}</span>
</div>
Expand All @@ -121,6 +121,39 @@ <h4>{% if edit %}{{form.instance.get_name}} ({% trans "edit" %}){% else %}{% tra
</div>
</div>
</div>
{% if institution.ertype in ERTYPE_ROLES.IDP %}
<div class="col-md-6"{% if form.proto.value not in RADPROTOS.TLSPSK %} style="display: none"{% endif %}>
<div class="form-group {% if form.psk_identity.errors %} error {% endif %}">
<label class="control-label" for="id_psk_identity"><b>{% trans "Your TLS-PSK Identity" %}</b></label>
<div class="controls">
{{ form.psk_identity }}
{% if form.psk_identity.errors %} <div class="alert-danger"> {{ form.psk_identity.errors|join_with_linebreaks }} </div>
{% endif %} <span class="help-block">{{ form.psk_identity.help_text }}</span>
</div>
</div>
</div>
{% endif %}
{% if institution.ertype in ERTYPE_ROLES.SP %}
<div class="col-md-6"{% if form.proto.value not in RADPROTOS.TLSPSK %} style="display: none"{% endif %}>
<div class="form-group">
<label class="control-label" for="our_psk_identity"><b>{% trans "Our TLS-PSK Identity" %}</b></label>
<div class="controls">
<input type="text" id="our_psk_identity" value="{{ institution.instid }}@{{ OUR_TLSPSK_REALM }}" disabled>
<span class="help-block">{{ form.psk_identity.help_text }}</span>
</div>
</div>
</div>
{% endif %}
<div class="col-md-6"{% if form.proto.value not in RADPROTOS.TLSPSK %} style="display: none"{% endif %}>
<div class="form-group {% if form.psk_key.errors %} error {% endif %}">
<label class="control-label" for="id_psk_key"><b>{% trans "TLS-PSK Pre-Shared Key" %}</b></label>
<div class="controls">
{{ form.psk_key }}
{% if form.psk_key.errors %} <div class="alert-danger"> {{ form.psk_key.errors|join_with_linebreaks }} </div>
{% endif %} <span class="help-block">{{ form.psk_key.help_text }}</span>
</div>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<div class="controls">
Expand All @@ -140,6 +173,18 @@ <h4>{% if edit %}{{form.instance.get_name}} ({% trans "edit" %}){% else %}{% tra
<script type="text/javascript">
$(document).ready(function() {
$('#id_secret').showPassword();
$('#id_proto').on('change', function() {
proto = $(this).val();
if (proto.match('psk')) {
$('.col-md-6:has(.form-group #id_psk_identity)').show();
$('.col-md-6:has(.form-group #our_psk_identity)').show();
$('.col-md-6:has(.form-group #id_psk_key)').show();
} else {
$('.col-md-6:has(.form-group #id_psk_identity)').hide();
$('.col-md-6:has(.form-group #our_psk_identity)').hide();
$('.col-md-6:has(.form-group #id_psk_key)').hide();
}
});
});
</script>
{% endblock %}
1 change: 1 addition & 0 deletions edumanage/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def country_code(context):
'GOOGLE_MAPS_API_KEY': settings.GOOGLE_MAPS_API_KEY if hasattr(settings,"GOOGLE_MAPS_API_KEY") else None,
'SHIB_AUTH_ENTITLEMENT': settings.SHIB_AUTH_ENTITLEMENT if hasattr(settings,"SHIB_AUTH_ENTITLEMENT") else None,
'FEDERATION_DOC_URL': settings.FEDERATION_DOC_URL if hasattr(settings,"FEDERATION_DOC_URL") else None,
'OUR_TLSPSK_REALM': settings.NRO_TLSPSK_REALM,
}


Expand Down
36 changes: 35 additions & 1 deletion edumanage/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
RealmServer,
ERTYPES,
ERTYPE_ROLES,
RADPROTOS,
)
from accounts.models import UserProfile
from edumanage.fields import MultipleEmailsField
Expand All @@ -27,6 +28,7 @@

FQDN_RE = r'(^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$)'
DN_RE = r'(^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$)'
NAI_RE = r'(^([^@]+)@(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$)'

def get_model_field(model, field_name):
fields = forms.models.fields_for_model(
Expand Down Expand Up @@ -195,6 +197,38 @@ def clean_host(self):
raise forms.ValidationError(_('This field is required.'))
return self.cleaned_data["host"]

def clean_psk_identity(self):
psk_identity = self.cleaned_data['psk_identity']
if not 'proto' in self.cleaned_data:
raise forms.ValidationError(_('The Protocol field is required to validate this field.'))
proto = self.cleaned_data['proto']
if not 'ertype' in self.cleaned_data:
raise forms.ValidationError(_('The Type field is required to validate this field.'))
ertype = self.cleaned_data['ertype']
if proto in RADPROTOS.TLSPSK:
if ertype in ERTYPE_ROLES.IDP:
if psk_identity:
match = re.match(NAI_RE, psk_identity)
if not match:
raise forms.ValidationError(_('Invalid Network Access Identifier format.'))
return self.cleaned_data["psk_identity"]
else:
raise forms.ValidationError(_('This field is required for IdPs.'))

def clean_psk_key(self):
psk_key = self.cleaned_data['psk_key']
if not 'proto' in self.cleaned_data:
raise forms.ValidationError(_('The Protocol field is required to validate this field.'))
proto = self.cleaned_data['proto']
if proto in RADPROTOS.TLSPSK:
if psk_key:
if len(psk_key) >= 16:
return self.cleaned_data["psk_key"]
else:
raise forms.ValidationError(_('The PSK Key must be at least 16 octets (draft-ietf-radext-tls-psk-11)'))
else:
raise forms.ValidationError(_('This field is required.'))


class ContactForm(forms.ModelForm):

Expand Down Expand Up @@ -357,4 +391,4 @@ def clean_server_name(self):
raise forms.ValidationError(error_text)
else:
raise forms.ValidationError(_('This field is required.'))
return self.cleaned_data["server_name"]
return self.cleaned_data["server_name"]
14 changes: 13 additions & 1 deletion edumanage/management/commands/servdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

from optparse import make_option
from django.core.management.base import BaseCommand
from edumanage.models import InstServer, Institution, ERTYPES, ERTYPE_ROLES
from django.conf import settings
from edumanage.models import InstServer, Institution, ERTYPES, ERTYPE_ROLES, RADPROTOS


class Command(BaseCommand):
Expand Down Expand Up @@ -74,6 +75,12 @@ def servdata():
if srv.name:
srv_dict['label'] = srv.name
srv_dict['secret'] = srv.secret
srv_dict['addr_type'] = srv.addr_type
srv_dict['proto'] = srv.proto
if srv.proto == RADPROTOS.TLSPSK:
# assuming the ManyToManyField is really many-to-one institution, which is true unless people play in the admin interface
srv_dict['psk_identity'] = "%s@%s" % (srv.instid.first().instid, settings.NRO_TLSPSK_REALM)
srv_dict['psk_key'] = srv.psk_key
root['clients'].update({srv_id: srv_dict})

servers = hosts.filter(ertype__in=ERTYPE_ROLES.IDP)
Expand All @@ -92,6 +99,11 @@ def servdata():
srv_dict['label'] = srv.name
srv_dict['secret'] = srv.secret
srv_dict['status_server'] = bool(srv.status_server)
srv_dict['addr_type'] = srv.addr_type
srv_dict['proto'] = srv.proto
if srv.proto == RADPROTOS.TLSPSK:
srv_dict['psk_identity'] = srv.psk_identity
srv_dict['psk_key'] = srv.psk_key
root['servers'].update({srv_id: srv_dict})

if insts:
Expand Down
20 changes: 20 additions & 0 deletions edumanage/migrations/0013_instserver_psk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2025-01-08 11:14
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('edumanage', '0012_venue_info_select'),
]
operations = [
migrations.AddField(
model_name='instserver',
name='psk_identity',
field=models.CharField(blank=True, help_text='Network Access Identifier (user@realm)', max_length=128, null=True),
),
migrations.AddField(
model_name='instserver',
name='psk_key',
field=models.CharField(blank=True, help_text='Randomly-generated string', max_length=80, null=True),
),
]
5 changes: 4 additions & 1 deletion edumanage/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,9 @@ def get_ertype_string(ertype, reverse=False):
RADPROTOS = get_namedtuple_choices(
('UDP', 'radius', 'traditional RADIUS over UDP'),
# ('TCP', 'radius-tcp', 'RADIUS over TCP (RFC6613)'),
('TLS', 'radius-tls', 'RADIUS over TLS (RFC6614)'),
('TLS', 'radius-tls', 'RADIUS over TLS (RadSec, RFC6614)'),
# ('DTLS', 'radius-dtls', 'RADIUS over datagram TLS (RESERVED)'),
('TLSPSK', 'radius-psk', 'RADIUS over TLS (PSK)'), # should be radius-tlspsk, but fit within 12 chars
)


Expand Down Expand Up @@ -541,6 +542,8 @@ class InstServer(models.Model):

secret = models.CharField(max_length=80)
proto = models.CharField(max_length=12, choices=RADPROTOS, default=RADPROTOS.UDP)
psk_identity = models.CharField(max_length=128, null=True, blank=True, help_text=_("Network Access Identifier (user@realm)"))
psk_key = models.CharField(max_length=80, null=True, blank=True, help_text='Randomly-generated string')
ts = models.DateTimeField(auto_now=True)

class Meta:
Expand Down
17 changes: 17 additions & 0 deletions edumanage/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1458,6 +1458,7 @@ def base_response(request):
'institution_canhaveservicelocs': institution_canhaveservicelocs,
'ERTYPES': ERTYPES,
'ERTYPE_ROLES': ERTYPE_ROLES,
'RADPROTOS': RADPROTOS,
}


Expand Down Expand Up @@ -2194,6 +2195,11 @@ def instxml(request, version):
server_type = EDB_SERVER_TYPES.TLS
except:
pass
try:
if server.proto == RADPROTOS.TLSPSK:
server_type = EDB_SERVER_TYPES.TLS

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code here, what would happen if server_type was undefined as a result of server.proto not matching any of the above cases? Wouldn't the call to instServerType.text = "%d" % (server_type) fail?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My PR doesn't introduce a new problem. However, I also think it's safe because of the check at lines 2184-2187. It'll simply become EDB_SERVER_TYPES.UDP.

except:
pass
instServer = ElementTree.SubElement(instElement, "server")
instServerName = ElementTree.SubElement(instServer, "server_name")
instServerName.text = "%s" % (server.host)
Expand Down Expand Up @@ -2544,6 +2550,12 @@ def servdata(request):
if srv.name:
srv_dict['label'] = srv.name
srv_dict['secret'] = srv.secret
srv_dict['addr_type'] = srv.addr_type
srv_dict['proto'] = srv.proto
if srv.proto == RADPROTOS.TLSPSK:
# assuming the ManyToManyField is really many-to-one institution, which is true unless people play in the admin interface
srv_dict['psk_identity'] = "%s@%s" % (srv.instid.first().instid, settings.NRO_TLSPSK_REALM)
srv_dict['psk_key'] = srv.psk_key
root['clients'].update({srv_id: srv_dict})

servers = hosts.filter(ertype__in=ERTYPE_ROLES.IDP)
Expand All @@ -2562,6 +2574,11 @@ def servdata(request):
srv_dict['label'] = srv.name
srv_dict['secret'] = srv.secret
srv_dict['status_server'] = bool(srv.status_server)
srv_dict['addr_type'] = srv.addr_type
srv_dict['proto'] = srv.proto
if srv.proto == RADPROTOS.TLSPSK:
srv_dict['psk_identity'] = srv.psk_identity
srv_dict['psk_key'] = srv.psk_key
root['servers'].update({srv_id: srv_dict})

if insts:
Expand Down