From ea3589f41b9db2ddb7bea3a69f5e1b2d285f5173 Mon Sep 17 00:00:00 2001 From: Nathaniel McCallum Date: Tue, 28 Jan 2014 17:11:04 -0500 Subject: Add HOTP support --- API.txt | 10 ++++--- VERSION | 2 +- daemons/ipa-slapi-plugins/libotp/libotp.c | 43 ++++++++++++++++++++++++++----- install/share/70ipaotp.ldif | 2 ++ ipalib/plugins/otptoken.py | 26 ++++++++++++++----- 5 files changed, 64 insertions(+), 19 deletions(-) diff --git a/API.txt b/API.txt index a6c3aed82..e2f5e036e 100644 --- a/API.txt +++ b/API.txt @@ -2220,12 +2220,13 @@ output: Entry('result', , Gettext('A dictionary representing an LDA output: Output('summary', (, ), None) output: Output('value', , None) command: otptoken_add -args: 1,20,3 +args: 1,21,3 arg: Str('ipatokenuniqueid', attribute=True, cli_name='id', multivalue=False, primary_key=True, required=False) option: Str('addattr*', cli_name='addattr', exclude='webui') option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False) option: Bool('ipatokendisabled', attribute=True, cli_name='disabled', multivalue=False, required=False) +option: Int('ipatokenhotpcounter', attribute=True, cli_name='counter', minvalue=0, multivalue=False, required=False) option: Str('ipatokenmodel', attribute=True, cli_name='model', multivalue=False, required=False) option: Str('ipatokennotafter', attribute=True, cli_name='not_after', multivalue=False, required=False) option: Str('ipatokennotbefore', attribute=True, cli_name='not_before', multivalue=False, required=False) @@ -2240,7 +2241,7 @@ option: Str('ipatokenvendor', attribute=True, cli_name='vendor', multivalue=Fals option: Flag('qrcode?', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') option: Str('setattr*', cli_name='setattr', exclude='webui') -option: StrEnum('type', attribute=False, cli_name='type', multivalue=False, required=False, values=(u'totp',)) +option: StrEnum('type', attribute=False, cli_name='type', multivalue=False, required=False, values=(u'totp', u'hotp')) option: Str('version?', exclude='webui') output: Entry('result', , Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) output: Output('summary', (, ), None) @@ -2254,11 +2255,12 @@ output: Output('result', , None) output: Output('summary', (, ), None) output: Output('value', , None) command: otptoken_find -args: 1,20,4 +args: 1,21,4 arg: Str('criteria?', noextrawhitespace=False) option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui') option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False) option: Bool('ipatokendisabled', attribute=True, autofill=False, cli_name='disabled', multivalue=False, query=True, required=False) +option: Int('ipatokenhotpcounter', attribute=True, autofill=False, cli_name='counter', minvalue=0, multivalue=False, query=True, required=False) option: Str('ipatokenmodel', attribute=True, autofill=False, cli_name='model', multivalue=False, query=True, required=False) option: Str('ipatokennotafter', attribute=True, autofill=False, cli_name='not_after', multivalue=False, query=True, required=False) option: Str('ipatokennotbefore', attribute=True, autofill=False, cli_name='not_before', multivalue=False, query=True, required=False) @@ -2274,7 +2276,7 @@ option: Flag('pkey_only?', autofill=True, default=False) option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui') option: Int('sizelimit?', autofill=False, minvalue=0) option: Int('timelimit?', autofill=False, minvalue=0) -option: StrEnum('type', attribute=False, autofill=False, cli_name='type', multivalue=False, query=True, required=False, values=(u'totp',)) +option: StrEnum('type', attribute=False, autofill=False, cli_name='type', multivalue=False, query=True, required=False, values=(u'totp', u'hotp')) option: Str('version?', exclude='webui') output: Output('count', , None) output: ListOfEntries('result', (, ), Gettext('A list of LDAP entries', domain='ipa', localedir=None)) diff --git a/VERSION b/VERSION index 5ce16b522..3072bfaed 100644 --- a/VERSION +++ b/VERSION @@ -89,4 +89,4 @@ IPA_DATA_VERSION=20100614120000 # # ######################################################## IPA_API_VERSION_MAJOR=2 -IPA_API_VERSION_MINOR=72 +IPA_API_VERSION_MINOR=73 diff --git a/daemons/ipa-slapi-plugins/libotp/libotp.c b/daemons/ipa-slapi-plugins/libotp/libotp.c index 3fb298035..31cc5915a 100644 --- a/daemons/ipa-slapi-plugins/libotp/libotp.c +++ b/daemons/ipa-slapi-plugins/libotp/libotp.c @@ -46,14 +46,17 @@ #define TOKEN(s) "ipaToken" s #define O(s) TOKEN("OTP" s) #define T(s) TOKEN("TOTP" s) +#define H(s) TOKEN("HOTP" s) #define IPA_OTP_DEFAULT_TOKEN_STEP 30 -#define IPA_OTP_OBJCLS_FILTER "(objectClass=ipaTokenTOTP)" +#define IPA_OTP_OBJCLS_FILTER \ + "(|(objectClass=ipaTokenTOTP)(objectClass=ipaTokenHOTP))" enum otptoken_type { OTPTOKEN_NONE = 0, OTPTOKEN_TOTP, + OTPTOKEN_HOTP, }; struct otptoken { @@ -61,10 +64,15 @@ struct otptoken { Slapi_DN *sdn; struct hotp_token token; enum otptoken_type type; - struct { - unsigned int step; - int offset; - } totp; + union { + struct { + unsigned int step; + int offset; + } totp; + struct { + uint64_t counter; + } hotp; + }; }; static const char *get_basedn(Slapi_DN *dn) @@ -124,6 +132,9 @@ static bool validate(struct otptoken *token, time_t now, ssize_t step, case OTPTOKEN_TOTP: step = (now + token->totp.offset) / token->totp.step + step; break; + case OTPTOKEN_HOTP: + step = token->hotp.counter + step; + break; default: return false; } @@ -160,6 +171,13 @@ static bool writeback(struct otptoken *token, ssize_t step, bool sync) attr = T("clockOffset"); value = token->totp.offset + step * token->totp.step; break; + case OTPTOKEN_HOTP: + /* Having support for LDAP_MOD_INCREMENT could be helpful here. */ + if (step < 0) + return false; /* NEVER go backwards! */ + attr = H("counter"); + value = token->hotp.counter + step; + break; default: return false; } @@ -190,6 +208,9 @@ static bool writeback(struct otptoken *token, ssize_t step, bool sync) case OTPTOKEN_TOTP: token->totp.offset = value; break; + case OTPTOKEN_HOTP: + token->hotp.counter = value; + break; default: break; } @@ -243,6 +264,8 @@ static struct otptoken *otptoken_new(Slapi_ComponentId *id, Slapi_Entry *entry) for (int i = 0; vals[i] != NULL; i++) { if (strcasecmp(vals[i], "ipaTokenTOTP") == 0) token->type = OTPTOKEN_TOTP; + else if (strcasecmp(vals[i], "ipaTokenHOTP") == 0) + token->type = OTPTOKEN_HOTP; } slapi_ch_array_free(vals); if (token->type == OTPTOKEN_NONE) @@ -285,6 +308,10 @@ static struct otptoken *otptoken_new(Slapi_ComponentId *id, Slapi_Entry *entry) if (token->totp.step == 0) token->totp.step = IPA_OTP_DEFAULT_TOKEN_STEP; break; + case OTPTOKEN_HOTP: + /* Get counter. */ + token->hotp.counter = slapi_entry_attr_get_int(entry, H("counter")); + break; default: break; } @@ -433,7 +460,8 @@ bool otptoken_validate(struct otptoken *token, size_t steps, uint32_t code) if (validate(token, now, i, code, NULL)) return writeback(token, i + 1, false); - if (i == 0) + /* Counter-based tokens must NEVER validate old steps! */ + if (i == 0 || token->type == OTPTOKEN_HOTP) continue; /* Validate the negative step. */ @@ -497,7 +525,8 @@ bool otptoken_sync(struct otptoken * const *tokens, size_t steps, if (validate(tokens[j], now, i, first_code, &second_code)) return writeback(tokens[j], i + 2, true); - if (i == 0) + /* Counter-based tokens must NEVER validate old steps! */ + if (i == 0 || tokens[j]->type == OTPTOKEN_HOTP) continue; /* Validate the negative step. */ diff --git a/install/share/70ipaotp.ldif b/install/share/70ipaotp.ldif index d257a46c3..620c2ccde 100644 --- a/install/share/70ipaotp.ldif +++ b/install/share/70ipaotp.ldif @@ -22,7 +22,9 @@ attributeTypes: (2.16.840.1.113730.3.8.16.1.17 NAME 'ipatokenRadiusSecret' DESC attributeTypes: (2.16.840.1.113730.3.8.16.1.18 NAME 'ipatokenRadiusTimeout' DESC 'Server Timeout' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA OTP') attributeTypes: (2.16.840.1.113730.3.8.16.1.19 NAME 'ipatokenRadiusRetries' DESC 'Number of allowed Retries' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA OTP') attributeTypes: (2.16.840.1.113730.3.8.16.1.20 NAME 'ipatokenUserMapAttribute' DESC 'Attribute to map from the user entry for RADIUS server authentication' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA OTP') +attributeTypes: (2.16.840.1.113730.3.8.16.1.21 NAME 'ipatokenHOTPcounter' DESC 'HOTP counter' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA OTP') objectClasses: (2.16.840.1.113730.3.8.16.2.1 NAME 'ipaToken' SUP top ABSTRACT DESC 'Abstract token class for tokens' MUST (ipatokenUniqueID) MAY (description $ ipatokenOwner $ ipatokenDisabled $ ipatokenNotBefore $ ipatokenNotAfter $ ipatokenVendor $ ipatokenModel $ ipatokenSerial) X-ORIGIN 'IPA OTP') objectClasses: (2.16.840.1.113730.3.8.16.2.2 NAME 'ipatokenTOTP' SUP ipaToken STRUCTURAL DESC 'TOTP Token Type' MAY (ipatokenOTPkey $ ipatokenOTPalgorithm $ ipatokenOTPdigits $ ipatokenTOTPclockOffset $ ipatokenTOTPtimeStep) X-ORIGIN 'IPA OTP') objectClasses: (2.16.840.1.113730.3.8.16.2.3 NAME 'ipatokenRadiusProxyUser' SUP top AUXILIARY DESC 'Radius Proxy User' MAY (ipatokenRadiusConfigLink $ ipatokenRadiusUserName) X-ORIGIN 'IPA OTP') objectClasses: (2.16.840.1.113730.3.8.16.2.4 NAME 'ipatokenRadiusConfiguration' SUP top STRUCTURAL DESC 'Proxy Radius Configuration' MUST (cn $ ipatokenRadiusServer $ ipatokenRadiusSecret) MAY (description $ ipatokenRadiusTimeout $ ipatokenRadiusRetries $ ipatokenUserMapAttribute) X-ORIGIN 'IPA OTP') +objectClasses: (2.16.840.1.113730.3.8.16.2.5 NAME 'ipatokenHOTP' SUP ipaToken STRUCTURAL DESC 'HOTP Token Type' MAY (ipatokenOTPkey $ ipatokenOTPalgorithm $ ipatokenOTPdigits $ ipatokenHOTPcounter) X-ORIGIN 'IPA OTP') diff --git a/ipalib/plugins/otptoken.py b/ipalib/plugins/otptoken.py index 67f248595..c7c0fe71f 100644 --- a/ipalib/plugins/otptoken.py +++ b/ipalib/plugins/otptoken.py @@ -53,7 +53,7 @@ EXAMPLES: register = Registry() -TOKEN_TYPES = (u'totp',) +TOKEN_TYPES = (u'totp', u'hotp') # NOTE: For maximum compatibility, KEY_LENGTH % 5 == 0 KEY_LENGTH = 10 @@ -102,7 +102,7 @@ class otptoken(LDAPObject): object_name = _('OTP tokens') object_name_plural = _('OTP tokens') object_class = ['ipatoken'] - possible_objectclasses = ['ipatokentotp'] + possible_objectclasses = ['ipatokentotp', 'ipatokenhotp'] default_attributes = [ 'ipatokenuniqueid', 'description', 'ipatokenowner', 'ipatokendisabled', 'ipatokennotbefore', 'ipatokennotafter', @@ -185,6 +185,12 @@ class otptoken(LDAPObject): minvalue=5, flags=('no_update'), ), + Int('ipatokenhotpcounter?', + cli_name='counter', + label=_('Counter'), + minvalue=0, + flags=('no_update'), + ), ) @@ -213,14 +219,17 @@ class otptoken_add(LDAPCreate): entry_attrs.setdefault('ipatokenserial', entry_attrs['ipatokenuniqueid']) entry_attrs.setdefault('ipatokenotpalgorithm', u'sha1') entry_attrs.setdefault('ipatokenotpdigits', 6) - entry_attrs.setdefault('ipatokentotpclockoffset', 0) - entry_attrs.setdefault('ipatokentotptimestep', 30) entry_attrs.setdefault('ipatokenotpkey', "".join(map(chr, random.SystemRandom().sample(range(255), KEY_LENGTH)))) - # Set the object class + # Set the object class and defaults for specific token types if options['type'] == 'totp': entry_attrs['objectclass'] = otptoken.object_class + ['ipatokentotp'] + entry_attrs.setdefault('ipatokentotpclockoffset', 0) + entry_attrs.setdefault('ipatokentotptimestep', 30) + elif options['type'] == 'hotp': + entry_attrs['objectclass'] = otptoken.object_class + ['ipatokenhotp'] + entry_attrs.setdefault('ipatokenhotpcounter', 0) # Resolve the user's dn _normalize_owner(self.api.Object.user, entry_attrs) @@ -239,13 +248,16 @@ class otptoken_add(LDAPCreate): args['issuer'] = issuer args['secret'] = base64.b32encode(entry_attrs['ipatokenotpkey']) args['digits'] = entry_attrs['ipatokenotpdigits'] - args['period'] = entry_attrs['ipatokentotptimestep'] args['algorithm'] = entry_attrs['ipatokenotpalgorithm'] + if options['type'] == 'totp': + args['period'] = entry_attrs['ipatokentotptimestep'] + elif options['type'] == 'hotp': + args['counter'] = entry_attrs['ipatokenhotpcounter'] # Build the URI label = urllib.quote(entry_attrs['ipatokenuniqueid']) parameters = urllib.urlencode(args) - uri = u'otpauth://totp/%s:%s?%s' % (issuer, label, parameters) + uri = u'otpauth://%s/%s:%s?%s' % (options['type'], issuer, label, parameters) setattr(context, 'uri', uri) return dn -- cgit