mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
Add HOTP support
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
This commit is contained in:
parent
189bdcb95d
commit
abb63ed9d1
10
API.txt
10
API.txt
@ -2220,12 +2220,13 @@ output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDA
|
||||
output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
|
||||
output: Output('value', <type 'unicode'>, 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', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
|
||||
output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
|
||||
@ -2254,11 +2255,12 @@ output: Output('result', <type 'dict'>, None)
|
||||
output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
|
||||
output: Output('value', <type 'unicode'>, 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', <type 'int'>, None)
|
||||
output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
|
||||
|
4
VERSION
4
VERSION
@ -89,5 +89,5 @@ IPA_DATA_VERSION=20100614120000
|
||||
# #
|
||||
########################################################
|
||||
IPA_API_VERSION_MAJOR=2
|
||||
IPA_API_VERSION_MINOR=74
|
||||
# Last change: pviktori - permissions: multivalued targetfilter
|
||||
IPA_API_VERSION_MINOR=75
|
||||
# Last change: npmccallum - HOTP support
|
||||
|
@ -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. */
|
||||
|
@ -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' MUST (ipatokenOTPkey $ ipatokenOTPalgorithm $ ipatokenOTPdigits $ ipatokenHOTPcounter) X-ORIGIN 'IPA OTP')
|
||||
|
@ -3,7 +3,7 @@
|
||||
dn: $SUFFIX
|
||||
changetype: modify
|
||||
add: aci
|
||||
aci: (targetfilter = "(&(!(objectClass=ipaToken))(!(objectClass=ipatokenTOTP))(!(objectClass=ipatokenRadiusConfiguration)))")(target != "ldap:///idnsname=*,cn=dns,$SUFFIX")(targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey || userPKCS12 || ipaNTHash || ipaNTTrustAuthOutgoing || ipaNTTrustAuthIncoming")(version 3.0; acl "Enable Anonymous access"; allow (read, search, compare) userdn = "ldap:///anyone";)
|
||||
aci: (targetfilter = "(&(!(objectClass=ipaToken))(!(objectClass=ipatokenTOTP))(!(objectClass=ipatokenHOTP))(!(objectClass=ipatokenRadiusConfiguration)))")(target != "ldap:///idnsname=*,cn=dns,$SUFFIX")(targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey || userPKCS12 || ipaNTHash || ipaNTTrustAuthOutgoing || ipaNTTrustAuthIncoming")(version 3.0; acl "Enable Anonymous access"; allow (read, search, compare) userdn = "ldap:///anyone";)
|
||||
aci: (targetattr = "memberOf || memberHost || memberUser")(version 3.0; acl "No anonymous access to member information"; deny (read,search,compare) userdn != "ldap:///all";)
|
||||
aci: (targetattr != "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory || krbMKey || krbPrincipalName || krbCanonicalName || krbUPEnabled || krbTicketPolicyReference || krbPasswordExpiration || krbPwdPolicyReference || krbPrincipalType || krbPwdHistory || krbLastPwdChange || krbPrincipalAliases || krbExtraData || krbLastSuccessfulAuth || krbLastFailedAuth || krbLoginFailedCount || ipaUniqueId || memberOf || serverHostName || enrolledBy || ipaNTHash")(version 3.0; acl "Admin can manage any entry"; allow (all) groupdn = "ldap:///cn=admins,cn=groups,cn=accounts,$SUFFIX";)
|
||||
aci: (targetattr = "userpassword || krbprincipalkey || sambalmpassword || sambantpassword")(version 3.0; acl "selfservice:Self can write own password"; allow (write) userdn="ldap:///self";)
|
||||
@ -104,3 +104,4 @@ aci: (targetfilter = "(objectClass=ipaToken)")(targetattrs = "objectclass || ipa
|
||||
aci: (targetfilter = "(objectClass=ipaToken)")(targetattrs = "ipatokenUniqueID || description || ipatokenOwner || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial")(version 3.0; acl "Users can write basic token info"; allow (write) userattr = "ipatokenOwner#USERDN";)
|
||||
aci: (targetfilter = "(objectClass=ipatokenTOTP)")(targetattrs = "ipatokenOTPkey || ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenTOTPclockOffset || ipatokenTOTPtimeStep")(version 3.0; acl "Users can add TOTP token secrets"; allow (write, search) userattr = "ipatokenOwner#USERDN";)
|
||||
aci: (target = "ldap:///ipatokenuniqueid=*,cn=otp,$SUFFIX")(targetfilter = "(objectClass=ipaToken)")(version 3.0; acl "Users can create and delete tokens"; allow (add, delete) userattr = "ipatokenOwner#SELFDN";)
|
||||
aci: (targetfilter = "(objectClass=ipatokenHOTP)")(targetattrs = "ipatokenOTPkey || ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenHOTPcounter")(version 3.0; acl "Users can add HOTP token secrets"; allow (write, search) userattr = "ipatokenOwner#USERDN";)
|
||||
|
@ -8,6 +8,7 @@ add: aci:'(targetfilter = "(objectClass=ipaToken)")(targetattrs = "objectclass |
|
||||
add: aci:'(targetfilter = "(objectClass=ipaToken)")(targetattrs = "ipatokenUniqueID || description || ipatokenOwner || ipatokenNotBefore || ipatokenNotAfter || ipatokenVendor || ipatokenModel || ipatokenSerial")(version 3.0; acl "Users can write basic token info"; allow (write) userattr = "ipatokenOwner#USERDN";)'
|
||||
add: aci:'(targetfilter = "(objectClass=ipatokenTOTP)")(targetattrs = "ipatokenOTPkey || ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenTOTPclockOffset || ipatokenTOTPtimeStep")(version 3.0; acl "Users can add TOTP token secrets"; allow (write, search) userattr = "ipatokenOwner#USERDN";)'
|
||||
add: aci:'(target = "ldap:///ipatokenuniqueid=*,cn=otp,$SUFFIX")(targetfilter = "(objectClass=ipaToken)")(version 3.0; acl "Users can create and delete tokens"; allow (add, delete) userattr = "ipatokenOwner#SELFDN";)'
|
||||
add: aci:'(targetfilter = "(objectClass=ipatokenHOTP)")(targetattrs = "ipatokenOTPkey || ipatokenOTPalgorithm || ipatokenOTPdigits || ipatokenHOTPcounter")(version 3.0; acl "Users can add HOTP token secrets"; allow (write, search) userattr = "ipatokenOwner#USERDN";)'
|
||||
|
||||
dn: cn=radiusproxy,$SUFFIX
|
||||
default: objectClass: nsContainer
|
||||
|
@ -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 token')
|
||||
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'),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -222,14 +228,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)
|
||||
@ -248,13 +257,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
|
||||
|
@ -35,7 +35,7 @@ class update_anonymous_aci(PostUpdate):
|
||||
aciname = u'Enable Anonymous access'
|
||||
aciprefix = u'none'
|
||||
ldap = self.obj.backend
|
||||
targetfilter = '(&(!(objectClass=ipaToken))(!(objectClass=ipatokenTOTP))(!(objectClass=ipatokenRadiusConfiguration)))'
|
||||
targetfilter = '(&(!(objectClass=ipaToken))(!(objectClass=ipatokenTOTP))(!(objectClass=ipatokenHOTP))(!(objectClass=ipatokenRadiusConfiguration)))'
|
||||
filter = None
|
||||
|
||||
entry_attrs = ldap.get_entry(api.env.basedn, ['aci'])
|
||||
|
Loading…
Reference in New Issue
Block a user