mirror of
https://git.openldap.org/openldap/openldap.git
synced 2024-12-21 03:10:25 +08:00
ITS#9437 Add otp_2fa overlay
This commit is contained in:
parent
3bd1b0909a
commit
87f3bad8bb
21
configure.ac
21
configure.ac
@ -351,6 +351,7 @@ Overlays="accesslog \
|
||||
dynlist \
|
||||
homedir \
|
||||
memberof \
|
||||
otp \
|
||||
ppolicy \
|
||||
proxycache \
|
||||
refint \
|
||||
@ -393,6 +394,8 @@ OL_ARG_ENABLE(homedir, [AS_HELP_STRING([--enable-homedir], [Home Directory Manag
|
||||
no, [no yes mod], ol_enable_overlays)
|
||||
OL_ARG_ENABLE(memberof, [AS_HELP_STRING([--enable-memberof], [Reverse Group Membership overlay])],
|
||||
no, [no yes mod], ol_enable_overlays)
|
||||
OL_ARG_ENABLE(otp, [AS_HELP_STRING([--enable-otp], [OTP 2-factor authentication overlay])],
|
||||
no, [no yes mod], ol_enable_overlays)
|
||||
OL_ARG_ENABLE(ppolicy, [AS_HELP_STRING([--enable-ppolicy], [Password Policy overlay])],
|
||||
no, [no yes mod], ol_enable_overlays)
|
||||
OL_ARG_ENABLE(proxycache, [AS_HELP_STRING([--enable-proxycache], [Proxy Cache overlay])],
|
||||
@ -593,6 +596,7 @@ BUILD_DYNLIST=no
|
||||
BUILD_LASTMOD=no
|
||||
BUILD_HOMEDIR=no
|
||||
BUILD_MEMBEROF=no
|
||||
BUILD_OTP=no
|
||||
BUILD_PPOLICY=no
|
||||
BUILD_PROXYCACHE=no
|
||||
BUILD_REFINT=no
|
||||
@ -2867,6 +2871,22 @@ if test "$ol_enable_memberof" != no ; then
|
||||
AC_DEFINE_UNQUOTED(SLAPD_OVER_MEMBEROF,$MFLAG,[define for Reverse Group Membership overlay])
|
||||
fi
|
||||
|
||||
if test "$ol_enable_otp" != no ; then
|
||||
if test $ol_with_tls = no ; then
|
||||
AC_MSG_ERROR([--enable-otp=$ol_enable_otp requires --with-tls])
|
||||
fi
|
||||
|
||||
BUILD_OTP=$ol_enable_otp
|
||||
if test "$ol_enable_otp" = mod ; then
|
||||
MFLAG=SLAPD_MOD_DYNAMIC
|
||||
SLAPD_DYNAMIC_OVERLAYS="$SLAPD_DYNAMIC_OVERLAYS otp_2fa.la"
|
||||
else
|
||||
MFLAG=SLAPD_MOD_STATIC
|
||||
SLAPD_STATIC_OVERLAYS="$SLAPD_STATIC_OVERLAYS otp_2fa.o"
|
||||
fi
|
||||
AC_DEFINE_UNQUOTED(SLAPD_OVER_OTP,$MFLAG,[define for OTP 2-factor Authentication overlay])
|
||||
fi
|
||||
|
||||
if test "$ol_enable_ppolicy" != no ; then
|
||||
BUILD_PPOLICY=$ol_enable_ppolicy
|
||||
if test "$ol_enable_ppolicy" = mod ; then
|
||||
@ -3142,6 +3162,7 @@ dnl overlays
|
||||
AC_SUBST(BUILD_LASTMOD)
|
||||
AC_SUBST(BUILD_HOMEDIR)
|
||||
AC_SUBST(BUILD_MEMBEROF)
|
||||
AC_SUBST(BUILD_OTP)
|
||||
AC_SUBST(BUILD_PPOLICY)
|
||||
AC_SUBST(BUILD_PROXYCACHE)
|
||||
AC_SUBST(BUILD_REFINT)
|
||||
|
134
doc/man/man5/slapo-otp_2fa.5
Normal file
134
doc/man/man5/slapo-otp_2fa.5
Normal file
@ -0,0 +1,134 @@
|
||||
.TH PW-TOTP 5 "2018/6/29" "SLAPO-OTP_2FA"
|
||||
.\" Copyright 2015-2021 The OpenLDAP Foundation.
|
||||
.\" Portions Copyright 2015 by Howard Chu, Symas Corp. All rights reserved.
|
||||
.\" Portions Copyright 2018 by Ondřej Kuzník, Symas Corp. All rights reserved.
|
||||
.\" Copying restrictions apply. See COPYRIGHT/LICENSE.
|
||||
.SH NAME
|
||||
slapo-otp \- Two factor authentication module
|
||||
.SH SYNOPSIS
|
||||
.B moduleload
|
||||
.I otp_2fa.la
|
||||
.SH DESCRIPTION
|
||||
The
|
||||
.B otp_2fa
|
||||
module allows time-based one-time password, AKA "authenticator-style", and
|
||||
HMAC-based one-time password authentication to be used in applications that use
|
||||
LDAP for authentication. In most cases no changes to the applications are
|
||||
needed to switch to this type of authentication.
|
||||
|
||||
With this module, users would use their password, followed with the one-time
|
||||
password in the password prompt to authenticate.
|
||||
|
||||
The password needed for a user to authenticate is calculated based on a counter
|
||||
(current time in case of TOTP) and a key that is referenced in the user's LDAP
|
||||
entry. Since the password is based on the time or number of uses, it changes
|
||||
periodically. Once used, it cannot be used again so keyloggers and
|
||||
shoulder-surfers are thwarted. A mobile phone application, such as the Google
|
||||
Authenticator or YubiKey (a
|
||||
.BR prover ),
|
||||
can be used to calculate the user's current one-time password, which is
|
||||
expressed as a (usually six-digit) number.
|
||||
|
||||
Alternatively, the value can be calculated by some other application with
|
||||
access to the user's key and delivered to the user through SMS or some other
|
||||
channel. When prompted to authenticate, the user merely appends the code
|
||||
provided by the prover at the end of their password when authenticating.
|
||||
|
||||
This implementation complies with
|
||||
.B RFC 4226 HOTP HMAC-Based One Time Passwords
|
||||
and
|
||||
.B RFC 6238 TOTP Time-based One Time Passwords
|
||||
and includes support for the SHA-1, SHA-256, and SHA-512 HMAC
|
||||
algorithms.
|
||||
|
||||
The HMAC key used in the OTP computation is stored in the oathOTPToken entry referenced in
|
||||
the user's LDAP entry and the parameters are stored in the oathOTPParams LDAP
|
||||
entry referenced in the token.
|
||||
|
||||
.SH CONFIGURATION
|
||||
Once the module is configured on the database, it will intercept LDAP simple
|
||||
binds for users whose LDAP entry has any of the
|
||||
.B oathOTPUser
|
||||
derived objectlasses attached to it. The attributes linking the user and the
|
||||
shared secret are:
|
||||
|
||||
.RS
|
||||
.TP
|
||||
.B oathTOTPToken: <dn>
|
||||
Mandatory for
|
||||
.BR oathTOTPUser ,
|
||||
indicates that the named entry is designated to hold the time-based one-time
|
||||
password shared secret and the last password used.
|
||||
.TP
|
||||
.B oathHOTPToken: <dn>
|
||||
Mandatory for
|
||||
.BR oathHOTPUser ,
|
||||
indicates that the named entry is designated to hold the one-time password
|
||||
shared secret and the last password used.
|
||||
.TP
|
||||
.B oathTOTPParams: <dn>
|
||||
Mandatory for
|
||||
.BR oathTOTPToken ,
|
||||
indicates that the named entry is designated to hold the parameters to generate
|
||||
time-based one-time password shared secret: its length and algorithm to use as
|
||||
well as the length of each time step and the grace period.
|
||||
.TP
|
||||
.B oathHOTPParams: <dn>
|
||||
Mandatory for
|
||||
.BR oathHOTPToken ,
|
||||
indicates that the named entry is designated to hold the parameters to generate
|
||||
one-time password shared secret: its length and algorithm to use as well as the
|
||||
permitted number of passwords to skip.
|
||||
.RE
|
||||
|
||||
The following parts of the OATH-LDAP schema are implemented.
|
||||
|
||||
General attributes:
|
||||
|
||||
.RS
|
||||
.TP
|
||||
.B oathSecret: <data>
|
||||
The shared secret is stored here as raw bytes.
|
||||
.TP
|
||||
.B oathOTPLength: <length>
|
||||
The password length, usually 6.
|
||||
.TP
|
||||
.B oathHMACAlgorithm: <OID>
|
||||
The OID of the hash algorithm to use as defined in RFC 8018.
|
||||
Supported algorithms include SHA1, SHA224, SHA256, SHA384 and SHA512.
|
||||
.RE
|
||||
|
||||
The HOTP attributes:
|
||||
|
||||
.RS
|
||||
.TP
|
||||
.B oathHOTPLookAhead: <number>
|
||||
The number of successive HOTP tokens that can be skipped.
|
||||
.TP
|
||||
.B oathHOTPCounter: <number>
|
||||
The order of the last HOTP token successfully redeemed by the user.
|
||||
.RE
|
||||
|
||||
The TOTP attributes:
|
||||
|
||||
.RS
|
||||
.TP
|
||||
.B oathTOTPTimeStepPeriod: <seconds>
|
||||
The length of the time-step period for TOTP calculation.
|
||||
.TP
|
||||
.B oathTOTPLastTimeStep: <number>
|
||||
The order of the last TOTP token successfully redeemed by the user.
|
||||
.TP
|
||||
.B oathTOTPGrace: <number>
|
||||
The number of time periods around the current time to try when checking the
|
||||
password provided by the user.
|
||||
.RE
|
||||
|
||||
.SH "SEE ALSO"
|
||||
.BR slapd\-config (5).
|
||||
|
||||
.SH ACKNOWLEDGEMENT
|
||||
This work was developed by Ondřej Kuzník and Howard Chu of Symas Corporation
|
||||
for inclusion in OpenLDAP Software.
|
||||
|
||||
This work reuses the OATH-LDAP schema developed by Michael Ströder.
|
@ -24,6 +24,7 @@ SRCS = overlays.c \
|
||||
dynlist.c \
|
||||
homedir.c \
|
||||
memberof.c \
|
||||
otp_2fa.c \
|
||||
pcache.c \
|
||||
collect.c \
|
||||
ppolicy.c \
|
||||
@ -95,6 +96,9 @@ homedir.la : homedir.lo
|
||||
memberof.la : memberof.lo
|
||||
$(LTLINK_MOD) -module -o $@ memberof.lo version.lo $(LINK_LIBS)
|
||||
|
||||
otp_2fa.la : otp_2fa.lo
|
||||
$(LTLINK_MOD) -module -o $@ otp_2fa.lo version.lo $(LINK_LIBS)
|
||||
|
||||
pcache.la : pcache.lo
|
||||
$(LTLINK_MOD) -module -o $@ pcache.lo version.lo $(LINK_LIBS)
|
||||
|
||||
|
950
servers/slapd/overlays/otp_2fa.c
Normal file
950
servers/slapd/overlays/otp_2fa.c
Normal file
@ -0,0 +1,950 @@
|
||||
/* otp_2fa.c - OATH 2-factor authentication module */
|
||||
/* $OpenLDAP$ */
|
||||
/* This work is part of OpenLDAP Software <http://www.openldap.org/>.
|
||||
*
|
||||
* Copyright 2015-2021 The OpenLDAP Foundation.
|
||||
* Portions Copyright 2015 by Howard Chu, Symas Corp.
|
||||
* Portions Copyright 2016-2017 by Michael Ströder <michael@stroeder.com>
|
||||
* Portions Copyright 2018 by Ondřej Kuzník, Symas Corp.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted only as authorized by the OpenLDAP
|
||||
* Public License.
|
||||
*
|
||||
* A copy of this license is available in the file LICENSE in the
|
||||
* top-level directory of the distribution or, alternatively, at
|
||||
* <http://www.OpenLDAP.org/license.html>.
|
||||
*/
|
||||
/* ACKNOWLEDGEMENTS:
|
||||
* This work includes code from the lastbind overlay.
|
||||
*/
|
||||
|
||||
#include <portable.h>
|
||||
|
||||
#ifdef SLAPD_OVER_OTP
|
||||
|
||||
#if HAVE_STDINT_H
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
|
||||
#include <lber.h>
|
||||
#include <lber_pvt.h>
|
||||
#include "lutil.h"
|
||||
#include <ac/stdlib.h>
|
||||
#include <ac/ctype.h>
|
||||
#include <ac/string.h>
|
||||
/* include socket.h to get sys/types.h and/or winsock2.h */
|
||||
#include <ac/socket.h>
|
||||
|
||||
#if HAVE_OPENSSL
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/hmac.h>
|
||||
|
||||
#define TOTP_SHA512_DIGEST_LENGTH SHA512_DIGEST_LENGTH
|
||||
#define TOTP_SHA1 EVP_sha1()
|
||||
#define TOTP_SHA224 EVP_sha224()
|
||||
#define TOTP_SHA256 EVP_sha256()
|
||||
#define TOTP_SHA384 EVP_sha384()
|
||||
#define TOTP_SHA512 EVP_sha512()
|
||||
#define TOTP_HMAC_CTX HMAC_CTX *
|
||||
|
||||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
static HMAC_CTX *
|
||||
HMAC_CTX_new( void )
|
||||
{
|
||||
HMAC_CTX *ctx = OPENSSL_malloc( sizeof(*ctx) );
|
||||
if ( ctx != NULL ) {
|
||||
HMAC_CTX_init( ctx );
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static void
|
||||
HMAC_CTX_free( HMAC_CTX *ctx )
|
||||
{
|
||||
if ( ctx != NULL ) {
|
||||
HMAC_CTX_cleanup( ctx );
|
||||
OPENSSL_free( ctx );
|
||||
}
|
||||
}
|
||||
#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */
|
||||
|
||||
#define HMAC_setup( ctx, key, len, hash ) \
|
||||
ctx = HMAC_CTX_new(); \
|
||||
HMAC_Init_ex( ctx, key, len, hash, 0 )
|
||||
#define HMAC_crunch( ctx, buf, len ) HMAC_Update( ctx, buf, len )
|
||||
#define HMAC_finish( ctx, dig, dlen ) \
|
||||
HMAC_Final( ctx, dig, &dlen ); \
|
||||
HMAC_CTX_free( ctx )
|
||||
|
||||
#elif HAVE_GNUTLS
|
||||
#include <nettle/hmac.h>
|
||||
|
||||
#define TOTP_SHA512_DIGEST_LENGTH SHA512_DIGEST_SIZE
|
||||
#define TOTP_SHA1 &nettle_sha1
|
||||
#define TOTP_SHA224 &nettle_sha224
|
||||
#define TOTP_SHA256 &nettle_sha256
|
||||
#define TOTP_SHA384 &nettle_sha384
|
||||
#define TOTP_SHA512 &nettle_sha512
|
||||
#define TOTP_HMAC_CTX struct hmac_sha512_ctx
|
||||
|
||||
#define HMAC_setup( ctx, key, len, hash ) \
|
||||
const struct nettle_hash *h = hash; \
|
||||
hmac_set_key( &ctx.outer, &ctx.inner, &ctx.state, h, len, key )
|
||||
#define HMAC_crunch( ctx, buf, len ) hmac_update( &ctx.state, h, len, buf )
|
||||
#define HMAC_finish( ctx, dig, dlen ) \
|
||||
hmac_digest( &ctx.outer, &ctx.inner, &ctx.state, h, h->digest_size, dig ); \
|
||||
dlen = h->digest_size
|
||||
|
||||
#else
|
||||
#error Unsupported crypto backend.
|
||||
#endif
|
||||
|
||||
#include "slap.h"
|
||||
#include "slap-config.h"
|
||||
|
||||
/* Schema from OATH-LDAP project by Michael Ströder */
|
||||
|
||||
static struct {
|
||||
char *name, *oid;
|
||||
} otp_oid[] = {
|
||||
{ "oath-ldap", "1.3.6.1.4.1.5427.1.389.4226" },
|
||||
{ "oath-ldap-at", "oath-ldap:4" },
|
||||
{ "oath-ldap-oc", "oath-ldap:6" },
|
||||
{ NULL }
|
||||
};
|
||||
|
||||
AttributeDescription *ad_oathOTPToken;
|
||||
AttributeDescription *ad_oathSecret;
|
||||
AttributeDescription *ad_oathOTPLength;
|
||||
AttributeDescription *ad_oathHMACAlgorithm;
|
||||
|
||||
AttributeDescription *ad_oathHOTPParams;
|
||||
AttributeDescription *ad_oathHOTPToken;
|
||||
AttributeDescription *ad_oathHOTPCounter;
|
||||
AttributeDescription *ad_oathHOTPLookahead;
|
||||
|
||||
AttributeDescription *ad_oathTOTPTimeStepPeriod;
|
||||
AttributeDescription *ad_oathTOTPParams;
|
||||
AttributeDescription *ad_oathTOTPToken;
|
||||
AttributeDescription *ad_oathTOTPLastTimeStep;
|
||||
AttributeDescription *ad_oathTOTPTimeStepWindow;
|
||||
|
||||
static struct otp_at {
|
||||
char *schema;
|
||||
AttributeDescription **adp;
|
||||
} otp_at[] = {
|
||||
{ "( oath-ldap-at:1 "
|
||||
"NAME 'oathSecret' "
|
||||
"DESC 'OATH-LDAP: Shared Secret (possibly encrypted with public key in oathEncKey)' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY octetStringMatch "
|
||||
"SUBSTR octetStringSubstringsMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )",
|
||||
&ad_oathSecret },
|
||||
|
||||
{ "( oath-ldap-at:2 "
|
||||
"NAME 'oathTokenSerialNumber' "
|
||||
"DESC 'OATH-LDAP: Proprietary hardware token serial number assigned by vendor' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY caseIgnoreMatch "
|
||||
"SUBSTR caseIgnoreSubstringsMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{64})" },
|
||||
|
||||
{ "( oath-ldap-at:3 "
|
||||
"NAME 'oathTokenIdentifier' "
|
||||
"DESC 'OATH-LDAP: Globally unique OATH token identifier' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY caseIgnoreMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )" },
|
||||
|
||||
{ "( oath-ldap-at:4 "
|
||||
"NAME 'oathParamsEntry' "
|
||||
"DESC 'OATH-LDAP: DN pointing to OATH parameter/policy object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP distinguishedName )" },
|
||||
{ "( oath-ldap-at:4.1 "
|
||||
"NAME 'oathTOTPTimeStepPeriod' "
|
||||
"DESC 'OATH-LDAP: Time window for TOTP (seconds)' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY integerMatch "
|
||||
"ORDERING integerOrderingMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )",
|
||||
&ad_oathTOTPTimeStepPeriod },
|
||||
|
||||
{ "( oath-ldap-at:5 "
|
||||
"NAME 'oathOTPLength' "
|
||||
"DESC 'OATH-LDAP: Length of OTP (number of digits)' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY integerMatch "
|
||||
"ORDERING integerOrderingMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )",
|
||||
&ad_oathOTPLength },
|
||||
{ "( oath-ldap-at:5.1 "
|
||||
"NAME 'oathHOTPParams' "
|
||||
"DESC 'OATH-LDAP: DN pointing to HOTP parameter object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathParamsEntry )",
|
||||
&ad_oathHOTPParams },
|
||||
{ "( oath-ldap-at:5.2 "
|
||||
"NAME 'oathTOTPParams' "
|
||||
"DESC 'OATH-LDAP: DN pointing to TOTP parameter object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathParamsEntry )",
|
||||
&ad_oathTOTPParams },
|
||||
|
||||
{ "( oath-ldap-at:6 "
|
||||
"NAME 'oathHMACAlgorithm' "
|
||||
"DESC 'OATH-LDAP: HMAC algorithm used for generating OTP values' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY objectIdentifierMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )",
|
||||
&ad_oathHMACAlgorithm },
|
||||
|
||||
{ "( oath-ldap-at:7 "
|
||||
"NAME 'oathTimestamp' "
|
||||
"DESC 'OATH-LDAP: Timestamp (not directly used).' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY generalizedTimeMatch "
|
||||
"ORDERING generalizedTimeOrderingMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 )" },
|
||||
{ "( oath-ldap-at:7.1 "
|
||||
"NAME 'oathLastFailure' "
|
||||
"DESC 'OATH-LDAP: Timestamp of last failed OATH validation' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathTimestamp )" },
|
||||
{ "( oath-ldap-at:7.2 "
|
||||
"NAME 'oathLastLogin' "
|
||||
"DESC 'OATH-LDAP: Timestamp of last successful OATH validation' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathTimestamp )" },
|
||||
{ "( oath-ldap-at:7.3 "
|
||||
"NAME 'oathSecretTime' "
|
||||
"DESC 'OATH-LDAP: Timestamp of generation of oathSecret attribute.' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathTimestamp )" },
|
||||
|
||||
{ "( oath-ldap-at:8 "
|
||||
"NAME 'oathSecretMaxAge' "
|
||||
"DESC 'OATH-LDAP: Time in seconds for which the shared secret (oathSecret) will be valid from oathSecretTime value.' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY integerMatch "
|
||||
"ORDERING integerOrderingMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )" },
|
||||
|
||||
{ "( oath-ldap-at:9 "
|
||||
"NAME 'oathToken' "
|
||||
"DESC 'OATH-LDAP: DN pointing to OATH token object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP distinguishedName )" },
|
||||
{ "( oath-ldap-at:9.1 "
|
||||
"NAME 'oathHOTPToken' "
|
||||
"DESC 'OATH-LDAP: DN pointing to OATH/HOTP token object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathToken )",
|
||||
&ad_oathHOTPToken },
|
||||
{ "( oath-ldap-at:9.2 "
|
||||
"NAME 'oathTOTPToken' "
|
||||
"DESC 'OATH-LDAP: DN pointing to OATH/TOTP token object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathToken )",
|
||||
&ad_oathTOTPToken },
|
||||
|
||||
{ "( oath-ldap-at:10 "
|
||||
"NAME 'oathCounter' "
|
||||
"DESC 'OATH-LDAP: Counter for OATH data (not directly used)' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY integerMatch "
|
||||
"ORDERING integerOrderingMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )" },
|
||||
{ "( oath-ldap-at:10.1 "
|
||||
"NAME 'oathFailureCount' "
|
||||
"DESC 'OATH-LDAP: OATH failure counter' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathCounter )" },
|
||||
{ "( oath-ldap-at:10.2 "
|
||||
"NAME 'oathHOTPCounter' "
|
||||
"DESC 'OATH-LDAP: Counter for HOTP' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathCounter )",
|
||||
&ad_oathHOTPCounter },
|
||||
{ "( oath-ldap-at:10.3 "
|
||||
"NAME 'oathHOTPLookAhead' "
|
||||
"DESC 'OATH-LDAP: Look-ahead window for HOTP' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathCounter )",
|
||||
&ad_oathHOTPLookahead },
|
||||
{ "( oath-ldap-at:10.5 "
|
||||
"NAME 'oathThrottleLimit' "
|
||||
"DESC 'OATH-LDAP: Failure throttle limit' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathCounter )" },
|
||||
{ "( oath-ldap-at:10.6 "
|
||||
"NAME 'oathTOTPLastTimeStep' "
|
||||
"DESC 'OATH-LDAP: Last time step seen for TOTP (time/period)' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathCounter )",
|
||||
&ad_oathTOTPLastTimeStep },
|
||||
{ "( oath-ldap-at:10.7 "
|
||||
"NAME 'oathMaxUsageCount' "
|
||||
"DESC 'OATH-LDAP: Maximum number of times a token can be used' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathCounter )" },
|
||||
{ "( oath-ldap-at:10.8 "
|
||||
"NAME 'oathTOTPTimeStepWindow' "
|
||||
"DESC 'OATH-LDAP: Size of time step +/- tolerance window used for TOTP validation' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathCounter )",
|
||||
&ad_oathTOTPTimeStepWindow },
|
||||
{ "( oath-ldap-at:10.9 "
|
||||
"NAME 'oathTOTPTimeStepDrift' "
|
||||
"DESC 'OATH-LDAP: Last observed time step shift seen for TOTP' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"SUP oathCounter )" },
|
||||
|
||||
{ "( oath-ldap-at:11 "
|
||||
"NAME 'oathSecretLength' "
|
||||
"DESC 'OATH-LDAP: Length of plain-text shared secret (number of bytes)' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY integerMatch "
|
||||
"ORDERING integerOrderingMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )" },
|
||||
|
||||
{ "( oath-ldap-at:12 "
|
||||
"NAME 'oathEncKey' "
|
||||
"DESC 'OATH-LDAP: public key to be used for encrypting new shared secrets' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY caseIgnoreMatch "
|
||||
"SUBSTR caseIgnoreSubstringsMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )" },
|
||||
|
||||
{ "( oath-ldap-at:13 "
|
||||
"NAME 'oathResultCode' "
|
||||
"DESC 'OATH-LDAP: LDAP resultCode to use in response' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY integerMatch "
|
||||
"ORDERING integerOrderingMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 )" },
|
||||
{ "( oath-ldap-at:13.1 "
|
||||
"NAME 'oathSuccessResultCode' "
|
||||
"DESC 'OATH-LDAP: success resultCode to use in bind/compare response' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SUP oathResultCode )" },
|
||||
{ "( oath-ldap-at:13.2 "
|
||||
"NAME 'oathFailureResultCode' "
|
||||
"DESC 'OATH-LDAP: failure resultCode to use in bind/compare response' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SUP oathResultCode )" },
|
||||
|
||||
{ "( oath-ldap-at:14 "
|
||||
"NAME 'oathTokenPIN' "
|
||||
"DESC 'OATH-LDAP: Configuration PIN (possibly encrypted with oathEncKey)' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY caseIgnoreMatch "
|
||||
"SUBSTR caseIgnoreSubstringsMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )" },
|
||||
|
||||
{ "( oath-ldap-at:15 "
|
||||
"NAME 'oathMessage' "
|
||||
"DESC 'OATH-LDAP: success diagnosticMessage to use in bind/compare response' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SINGLE-VALUE "
|
||||
"EQUALITY caseIgnoreMatch "
|
||||
"SUBSTR caseIgnoreSubstringsMatch "
|
||||
"SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} )" },
|
||||
{ "( oath-ldap-at:15.1 "
|
||||
"NAME 'oathSuccessMessage' "
|
||||
"DESC 'OATH-LDAP: success diagnosticMessage to use in bind/compare response' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SUP oathMessage )" },
|
||||
{ "( oath-ldap-at:15.2 "
|
||||
"NAME 'oathFailureMessage' "
|
||||
"DESC 'OATH-LDAP: failure diagnosticMessage to use in bind/compare response' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"SUP oathMessage )" },
|
||||
|
||||
{ NULL }
|
||||
};
|
||||
|
||||
ObjectClass *oc_oathOTPUser;
|
||||
ObjectClass *oc_oathHOTPToken;
|
||||
ObjectClass *oc_oathTOTPToken;
|
||||
ObjectClass *oc_oathHOTPParams;
|
||||
ObjectClass *oc_oathTOTPParams;
|
||||
|
||||
static struct otp_oc {
|
||||
char *schema;
|
||||
ObjectClass **ocp;
|
||||
} otp_oc[] = {
|
||||
{ "( oath-ldap-oc:1 "
|
||||
"NAME 'oathUser' "
|
||||
"DESC 'OATH-LDAP: User Object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"ABSTRACT )",
|
||||
&oc_oathOTPUser },
|
||||
{ "( oath-ldap-oc:1.1 "
|
||||
"NAME 'oathHOTPUser' "
|
||||
"DESC 'OATH-LDAP: HOTP user object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"AUXILIARY "
|
||||
"SUP oathUser "
|
||||
"MAY ( oathHOTPToken ) )" },
|
||||
{ "( oath-ldap-oc:1.2 "
|
||||
"NAME 'oathTOTPUser' "
|
||||
"DESC 'OATH-LDAP: TOTP user object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"AUXILIARY "
|
||||
"SUP oathUser "
|
||||
"MUST ( oathTOTPToken ) )" },
|
||||
{ "( oath-ldap-oc:2 "
|
||||
"NAME 'oathParams' "
|
||||
"DESC 'OATH-LDAP: Parameter object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"ABSTRACT "
|
||||
"MUST ( oathOTPLength $ oathHMACAlgorithm ) "
|
||||
"MAY ( oathSecretMaxAge $ oathSecretLength $ "
|
||||
"oathMaxUsageCount $ oathThrottleLimit $ oathEncKey $ "
|
||||
"oathSuccessResultCode $ oathSuccessMessage $ "
|
||||
"oathFailureResultCode $ oathFailureMessage ) )" },
|
||||
{ "( oath-ldap-oc:2.1 "
|
||||
"NAME 'oathHOTPParams' "
|
||||
"DESC 'OATH-LDAP: HOTP parameter object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"AUXILIARY "
|
||||
"SUP oathParams "
|
||||
"MUST ( oathHOTPLookAhead ) )",
|
||||
&oc_oathHOTPParams },
|
||||
{ "( oath-ldap-oc:2.2 "
|
||||
"NAME 'oathTOTPParams' "
|
||||
"DESC 'OATH-LDAP: TOTP parameter object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"AUXILIARY "
|
||||
"SUP oathParams "
|
||||
"MUST ( oathTOTPTimeStepPeriod ) "
|
||||
"MAY ( oathTOTPTimeStepWindow $ oathTOTPTimeStepDrift ) )",
|
||||
&oc_oathTOTPParams },
|
||||
{ "( oath-ldap-oc:3 "
|
||||
"NAME 'oathToken' "
|
||||
"DESC 'OATH-LDAP: User Object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"ABSTRACT "
|
||||
"MAY ( oathSecret $ oathSecretTime $ "
|
||||
"oathLastLogin $ oathFailureCount $ oathLastFailure $ "
|
||||
"oathTokenSerialNumber $ oathTokenIdentifier $ oathTokenPIN ) )" },
|
||||
{ "( oath-ldap-oc:3.1 "
|
||||
"NAME 'oathHOTPToken' "
|
||||
"DESC 'OATH-LDAP: HOTP token object' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"AUXILIARY "
|
||||
"SUP oathToken "
|
||||
"MAY ( oathHOTPParams $ oathHOTPCounter ) )",
|
||||
&oc_oathHOTPToken },
|
||||
{ "( oath-ldap-oc:3.2 "
|
||||
"NAME 'oathTOTPToken' "
|
||||
"DESC 'OATH-LDAP: TOTP token' "
|
||||
"X-ORIGIN 'OATH-LDAP' "
|
||||
"AUXILIARY "
|
||||
"SUP oathToken "
|
||||
"MAY ( oathTOTPParams $ oathTOTPLastTimeStep ) )",
|
||||
&oc_oathTOTPToken },
|
||||
{ NULL }
|
||||
};
|
||||
|
||||
typedef struct myval {
|
||||
ber_len_t mv_len;
|
||||
void *mv_val;
|
||||
} myval;
|
||||
|
||||
static void
|
||||
do_hmac( const void *hash, myval *key, myval *data, myval *out )
|
||||
{
|
||||
TOTP_HMAC_CTX ctx;
|
||||
unsigned int digestLen;
|
||||
|
||||
HMAC_setup( ctx, key->mv_val, key->mv_len, hash );
|
||||
HMAC_crunch( ctx, data->mv_val, data->mv_len );
|
||||
HMAC_finish( ctx, out->mv_val, digestLen );
|
||||
out->mv_len = digestLen;
|
||||
}
|
||||
|
||||
#define MAX_DIGITS 8
|
||||
static const int DIGITS_POWER[] = {
|
||||
1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000,
|
||||
};
|
||||
|
||||
static const void *
|
||||
otp_choose_mech( struct berval *oid )
|
||||
{
|
||||
/* RFC 8018 OIDs */
|
||||
const struct berval oid_hmacwithSHA1 = BER_BVC("1.2.840.113549.2.7");
|
||||
const struct berval oid_hmacwithSHA224 = BER_BVC("1.2.840.113549.2.8");
|
||||
const struct berval oid_hmacwithSHA256 = BER_BVC("1.2.840.113549.2.9");
|
||||
const struct berval oid_hmacwithSHA384 = BER_BVC("1.2.840.113549.2.10");
|
||||
const struct berval oid_hmacwithSHA512 = BER_BVC("1.2.840.113549.2.11");
|
||||
|
||||
if ( !ber_bvcmp( &oid_hmacwithSHA1, oid ) ) {
|
||||
return TOTP_SHA1;
|
||||
} else if ( !ber_bvcmp( &oid_hmacwithSHA224, oid ) ) {
|
||||
return TOTP_SHA224;
|
||||
} else if ( !ber_bvcmp( &oid_hmacwithSHA256, oid ) ) {
|
||||
return TOTP_SHA256;
|
||||
} else if ( !ber_bvcmp( &oid_hmacwithSHA384, oid ) ) {
|
||||
return TOTP_SHA384;
|
||||
} else if ( !ber_bvcmp( &oid_hmacwithSHA512, oid ) ) {
|
||||
return TOTP_SHA512;
|
||||
}
|
||||
|
||||
Debug( LDAP_DEBUG_TRACE, "otp_choose_mech: "
|
||||
"hmac OID %s unsupported\n",
|
||||
oid->bv_val );
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
generate(
|
||||
struct berval *bv,
|
||||
uint64_t tval,
|
||||
int digits,
|
||||
struct berval *out,
|
||||
const void *mech )
|
||||
{
|
||||
unsigned char digest[TOTP_SHA512_DIGEST_LENGTH];
|
||||
myval digval;
|
||||
myval key, data;
|
||||
unsigned char msg[8];
|
||||
int i, offset, res, otp;
|
||||
|
||||
#if WORDS_BIGENDIAN
|
||||
*(uint64_t *)msg = tval;
|
||||
#else
|
||||
for ( i = 7; i >= 0; i-- ) {
|
||||
msg[i] = tval & 0xff;
|
||||
tval >>= 8;
|
||||
}
|
||||
#endif
|
||||
|
||||
key.mv_len = bv->bv_len;
|
||||
key.mv_val = bv->bv_val;
|
||||
|
||||
data.mv_val = msg;
|
||||
data.mv_len = sizeof(msg);
|
||||
|
||||
digval.mv_val = digest;
|
||||
digval.mv_len = sizeof(digest);
|
||||
do_hmac( mech, &key, &data, &digval );
|
||||
|
||||
offset = digest[digval.mv_len - 1] & 0xf;
|
||||
res = ( (digest[offset] & 0x7f) << 24 ) |
|
||||
( ( digest[offset + 1] & 0xff ) << 16 ) |
|
||||
( ( digest[offset + 2] & 0xff ) << 8 ) |
|
||||
( digest[offset + 3] & 0xff );
|
||||
|
||||
otp = res % DIGITS_POWER[digits];
|
||||
out->bv_len = snprintf( out->bv_val, out->bv_len, "%0*d", digits, otp );
|
||||
}
|
||||
|
||||
static int
|
||||
otp_bind_response( Operation *op, SlapReply *rs )
|
||||
{
|
||||
if ( rs->sr_err == LDAP_SUCCESS ) {
|
||||
/* If the bind succeeded, return our result */
|
||||
rs->sr_err = LDAP_INVALID_CREDENTIALS;
|
||||
}
|
||||
return SLAP_CB_CONTINUE;
|
||||
}
|
||||
|
||||
static long
|
||||
otp_hotp( Operation *op, Entry *token )
|
||||
{
|
||||
char outbuf[MAX_DIGITS + 1];
|
||||
Entry *params = NULL;
|
||||
Attribute *a;
|
||||
BerValue *secret, client_otp;
|
||||
const void *mech;
|
||||
long last_step = -1, found = -1;
|
||||
int i, otp_len, window;
|
||||
|
||||
a = attr_find( token->e_attrs, ad_oathSecret );
|
||||
secret = &a->a_vals[0];
|
||||
|
||||
a = attr_find( token->e_attrs, ad_oathHOTPCounter );
|
||||
if ( a && lutil_atol( &last_step, a->a_vals[0].bv_val ) != 0 ) {
|
||||
Debug( LDAP_DEBUG_ANY, "otp_hotp: "
|
||||
"could not parse oathHOTPCounter value %s\n",
|
||||
a->a_vals[0].bv_val );
|
||||
goto done;
|
||||
}
|
||||
|
||||
a = attr_find( token->e_attrs, ad_oathHOTPParams );
|
||||
if ( !a ||
|
||||
be_entry_get_rw( op, &a->a_nvals[0], oc_oathHOTPParams, NULL, 0,
|
||||
¶ms ) ) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
a = attr_find( params->e_attrs, ad_oathOTPLength );
|
||||
if ( lutil_atoi( &otp_len, a->a_vals[0].bv_val ) != 0 ) {
|
||||
Debug( LDAP_DEBUG_ANY, "otp_hotp: "
|
||||
"could not parse oathOTPLength value %s\n",
|
||||
a->a_vals[0].bv_val );
|
||||
goto done;
|
||||
}
|
||||
if ( otp_len > MAX_DIGITS || op->orb_cred.bv_len < otp_len ) {
|
||||
/* Client didn't even send the token, fail immediately */
|
||||
goto done;
|
||||
}
|
||||
|
||||
a = attr_find( params->e_attrs, ad_oathHOTPLookahead );
|
||||
if ( lutil_atoi( &window, a->a_vals[0].bv_val ) != 0 ) {
|
||||
Debug( LDAP_DEBUG_ANY, "otp_hotp: "
|
||||
"could not parse oathHOTPLookAhead value %s\n",
|
||||
a->a_vals[0].bv_val );
|
||||
goto done;
|
||||
}
|
||||
window++;
|
||||
|
||||
a = attr_find( params->e_attrs, ad_oathHMACAlgorithm );
|
||||
if ( !(mech = otp_choose_mech( &a->a_vals[0] )) ) {
|
||||
goto done;
|
||||
}
|
||||
be_entry_release_r( op, params );
|
||||
params = NULL;
|
||||
|
||||
/* We are provided "password" + "OTP", split accordingly */
|
||||
client_otp.bv_len = otp_len;
|
||||
client_otp.bv_val = op->orb_cred.bv_val + op->orb_cred.bv_len - otp_len;
|
||||
|
||||
/* If check succeeds, advance the step counter accordingly */
|
||||
for ( i = 1; i <= window; i++ ) {
|
||||
BerValue out = { .bv_val = outbuf, .bv_len = sizeof(outbuf) };
|
||||
|
||||
generate( secret, last_step + i, otp_len, &out, mech );
|
||||
if ( !ber_bvcmp( &out, &client_otp ) ) {
|
||||
found = last_step + i;
|
||||
/* Would we leak information if we stopped right now? */
|
||||
}
|
||||
}
|
||||
|
||||
if ( found >= 0 ) {
|
||||
/* OTP check passed, trim the password */
|
||||
op->orb_cred.bv_len -= otp_len;
|
||||
Debug( LDAP_DEBUG_STATS, "%s HOTP token %s no. %ld redeemed\n",
|
||||
op->o_log_prefix, token->e_name.bv_val, found );
|
||||
}
|
||||
|
||||
done:
|
||||
memset( outbuf, 0, sizeof(outbuf) );
|
||||
if ( params ) {
|
||||
be_entry_release_r( op, params );
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
static long
|
||||
otp_totp( Operation *op, Entry *token )
|
||||
{
|
||||
Entry *params = NULL;
|
||||
Attribute *a;
|
||||
BerValue *secret, client_otp;
|
||||
const void *mech;
|
||||
long t, last_step = -1, found = -1, window = 0;
|
||||
int i, otp_len, time_step;
|
||||
|
||||
a = attr_find( token->e_attrs, ad_oathSecret );
|
||||
secret = &a->a_vals[0];
|
||||
|
||||
a = attr_find( token->e_attrs, ad_oathTOTPLastTimeStep );
|
||||
if ( a && lutil_atol( &last_step, a->a_vals[0].bv_val ) != 0 ) {
|
||||
Debug( LDAP_DEBUG_ANY, "otp_totp: "
|
||||
"could not parse oathTOTPLastTimeStep value %s\n",
|
||||
a->a_vals[0].bv_val );
|
||||
goto done;
|
||||
}
|
||||
|
||||
a = attr_find( token->e_attrs, ad_oathTOTPParams );
|
||||
if ( !a ||
|
||||
be_entry_get_rw( op, &a->a_nvals[0], oc_oathTOTPParams, NULL, 0,
|
||||
¶ms ) ) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
a = attr_find( params->e_attrs, ad_oathTOTPTimeStepPeriod );
|
||||
if ( lutil_atoi( &time_step, a->a_vals[0].bv_val ) != 0 ) {
|
||||
Debug( LDAP_DEBUG_ANY, "otp_totp: "
|
||||
"could not parse oathTOTPTimeStepPeriod value %s\n",
|
||||
a->a_vals[0].bv_val );
|
||||
goto done;
|
||||
}
|
||||
t = op->o_time / time_step;
|
||||
|
||||
a = attr_find( params->e_attrs, ad_oathTOTPTimeStepWindow );
|
||||
if ( a && lutil_atol( &window, a->a_vals[0].bv_val ) != 0 ) {
|
||||
Debug( LDAP_DEBUG_ANY, "otp_totp: "
|
||||
"could not parse oathTOTPTimeStepWindow value %s\n",
|
||||
a->a_vals[0].bv_val );
|
||||
goto done;
|
||||
}
|
||||
|
||||
a = attr_find( params->e_attrs, ad_oathOTPLength );
|
||||
if ( lutil_atoi( &otp_len, a->a_vals[0].bv_val ) != 0 ) {
|
||||
Debug( LDAP_DEBUG_ANY, "otp_totp: "
|
||||
"could not parse oathOTPLength value %s\n",
|
||||
a->a_vals[0].bv_val );
|
||||
goto done;
|
||||
}
|
||||
if ( otp_len > MAX_DIGITS || op->orb_cred.bv_len < otp_len ) {
|
||||
/* Client didn't even send the token, fail immediately */
|
||||
goto done;
|
||||
}
|
||||
|
||||
a = attr_find( params->e_attrs, ad_oathHMACAlgorithm );
|
||||
if ( !(mech = otp_choose_mech( &a->a_vals[0] )) ) {
|
||||
goto done;
|
||||
}
|
||||
be_entry_release_r( op, params );
|
||||
params = NULL;
|
||||
|
||||
/* We are provided "password" + "OTP", split accordingly */
|
||||
client_otp.bv_len = otp_len;
|
||||
client_otp.bv_val = op->orb_cred.bv_val + op->orb_cred.bv_len - otp_len;
|
||||
|
||||
/* If check succeeds, advance the step counter accordingly */
|
||||
for ( i = -window; i <= window; i++ ) {
|
||||
char outbuf[MAX_DIGITS + 1];
|
||||
BerValue out = { .bv_val = outbuf, .bv_len = sizeof(outbuf) };
|
||||
|
||||
if ( t + i < 0 ) continue;
|
||||
|
||||
generate( secret, t + i, otp_len, &out, mech );
|
||||
if ( !ber_bvcmp( &out, &client_otp ) ) {
|
||||
found = t + i;
|
||||
}
|
||||
}
|
||||
|
||||
if ( found >= 0 ) {
|
||||
int offset = found - t;
|
||||
|
||||
if ( found <= last_step ) {
|
||||
/* Token re-used, refuse */
|
||||
found = -1;
|
||||
Debug( LDAP_DEBUG_TRACE, "%s client tried to reuse old TOTP token %s, offset %d\n",
|
||||
op->o_log_prefix, token->e_name.bv_val, offset );
|
||||
} else {
|
||||
/* OTP check passed, trim the password */
|
||||
op->orb_cred.bv_len -= otp_len;
|
||||
Debug( LDAP_DEBUG_TRACE, "%s TOTP token %s redeemed with offset %d\n",
|
||||
op->o_log_prefix, token->e_name.bv_val, offset );
|
||||
}
|
||||
} else {
|
||||
Debug( LDAP_DEBUG_TRACE, "%s TOTP token was not valid\n",
|
||||
op->o_log_prefix );
|
||||
}
|
||||
|
||||
done:
|
||||
if ( params ) {
|
||||
be_entry_release_r( op, params );
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
static int
|
||||
otp_op_bind( Operation *op, SlapReply *rs )
|
||||
{
|
||||
slap_overinst *on = (slap_overinst *)op->o_bd->bd_info;
|
||||
BerValue totpdn = BER_BVNULL, hotpdn = BER_BVNULL, ndn;
|
||||
Entry *user = NULL, *token = NULL;
|
||||
AttributeDescription *ad = NULL;
|
||||
Attribute *a;
|
||||
long t = -1;
|
||||
int rc = SLAP_CB_CONTINUE;
|
||||
|
||||
if ( op->oq_bind.rb_method != LDAP_AUTH_SIMPLE ) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
op->o_bd->bd_info = (BackendInfo *)on->on_info;
|
||||
|
||||
if ( be_entry_get_rw( op, &op->o_req_ndn, NULL, NULL, 0, &user ) ) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
if ( !is_entry_objectclass_or_sub( user, oc_oathOTPUser ) ) {
|
||||
be_entry_release_r( op, user );
|
||||
goto done;
|
||||
}
|
||||
|
||||
if ( (a = attr_find( user->e_attrs, ad_oathTOTPToken )) ) {
|
||||
ber_dupbv_x( &totpdn, &a->a_nvals[0], op->o_tmpmemctx );
|
||||
}
|
||||
|
||||
if ( (a = attr_find( user->e_attrs, ad_oathHOTPToken )) ) {
|
||||
ber_dupbv_x( &hotpdn, &a->a_nvals[0], op->o_tmpmemctx );
|
||||
}
|
||||
be_entry_release_r( op, user );
|
||||
|
||||
if ( !BER_BVISNULL( &totpdn ) &&
|
||||
be_entry_get_rw( op, &totpdn, oc_oathTOTPToken, ad_oathSecret, 0,
|
||||
&token ) == LDAP_SUCCESS ) {
|
||||
ndn = totpdn;
|
||||
ad = ad_oathTOTPLastTimeStep;
|
||||
t = otp_totp( op, token );
|
||||
be_entry_release_r( op, token );
|
||||
token = NULL;
|
||||
}
|
||||
if ( t < 0 && !BER_BVISNULL( &hotpdn ) &&
|
||||
be_entry_get_rw( op, &hotpdn, oc_oathHOTPToken, ad_oathSecret, 0,
|
||||
&token ) == LDAP_SUCCESS ) {
|
||||
ndn = hotpdn;
|
||||
ad = ad_oathHOTPCounter;
|
||||
t = otp_hotp( op, token );
|
||||
be_entry_release_r( op, token );
|
||||
token = NULL;
|
||||
}
|
||||
|
||||
/* If check succeeds, advance the step counter accordingly */
|
||||
if ( t >= 0 ) {
|
||||
char outbuf[32];
|
||||
Operation op2;
|
||||
Opheader oh;
|
||||
Modifications mod;
|
||||
SlapReply rs2 = { REP_RESULT };
|
||||
slap_callback cb = { .sc_response = &slap_null_cb };
|
||||
BerValue bv[2];
|
||||
|
||||
bv[0].bv_val = outbuf;
|
||||
bv[0].bv_len = snprintf( bv[0].bv_val, sizeof(outbuf), "%ld", t );
|
||||
BER_BVZERO( &bv[1] );
|
||||
|
||||
mod.sml_numvals = 1;
|
||||
mod.sml_values = bv;
|
||||
mod.sml_nvalues = NULL;
|
||||
mod.sml_desc = ad;
|
||||
mod.sml_op = LDAP_MOD_REPLACE;
|
||||
mod.sml_flags = SLAP_MOD_INTERNAL;
|
||||
mod.sml_next = NULL;
|
||||
|
||||
op2 = *op;
|
||||
oh = *op->o_hdr;
|
||||
op2.o_hdr = &oh;
|
||||
|
||||
op2.o_callback = &cb;
|
||||
|
||||
op2.o_tag = LDAP_REQ_MODIFY;
|
||||
op2.orm_modlist = &mod;
|
||||
op2.o_dn = op->o_bd->be_rootdn;
|
||||
op2.o_ndn = op->o_bd->be_rootndn;
|
||||
op2.o_req_dn = ndn;
|
||||
op2.o_req_ndn = ndn;
|
||||
op2.o_opid = -1;
|
||||
|
||||
op2.o_bd->be_modify( &op2, &rs2 );
|
||||
if ( rs2.sr_err != LDAP_SUCCESS ) {
|
||||
rc = LDAP_OTHER;
|
||||
goto done;
|
||||
}
|
||||
} else {
|
||||
/* Client failed the bind, but we still have to pass it over to the
|
||||
* backend and fail the Bind later */
|
||||
slap_callback *cb;
|
||||
cb = op->o_tmpcalloc( 1, sizeof(slap_callback), op->o_tmpmemctx );
|
||||
cb->sc_response = otp_bind_response;
|
||||
cb->sc_next = op->o_callback;
|
||||
op->o_callback = cb;
|
||||
}
|
||||
|
||||
done:
|
||||
if ( !BER_BVISNULL( &hotpdn ) ) {
|
||||
ber_memfree_x( hotpdn.bv_val, op->o_tmpmemctx );
|
||||
}
|
||||
if ( !BER_BVISNULL( &totpdn ) ) {
|
||||
ber_memfree_x( totpdn.bv_val, op->o_tmpmemctx );
|
||||
}
|
||||
op->o_bd->bd_info = (BackendInfo *)on;
|
||||
return rc;
|
||||
}
|
||||
|
||||
static slap_overinst otp;
|
||||
|
||||
int
|
||||
otp_initialize( void )
|
||||
{
|
||||
ConfigArgs ca;
|
||||
char *argv[4];
|
||||
int i;
|
||||
|
||||
otp.on_bi.bi_type = "otp_2fa";
|
||||
otp.on_bi.bi_op_bind = otp_op_bind;
|
||||
|
||||
ca.argv = argv;
|
||||
argv[0] = "otp_2fa";
|
||||
ca.argv = argv;
|
||||
ca.argc = 3;
|
||||
ca.fname = argv[0];
|
||||
|
||||
argv[3] = NULL;
|
||||
for ( i = 0; otp_oid[i].name; i++ ) {
|
||||
argv[1] = otp_oid[i].name;
|
||||
argv[2] = otp_oid[i].oid;
|
||||
parse_oidm( &ca, 0, NULL );
|
||||
}
|
||||
|
||||
/* schema integration */
|
||||
for ( i = 0; otp_at[i].schema; i++ ) {
|
||||
if ( register_at( otp_at[i].schema, otp_at[i].adp, 0 ) ) {
|
||||
Debug( LDAP_DEBUG_ANY, "otp_initialize: "
|
||||
"register_at failed\n" );
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
for ( i = 0; otp_oc[i].schema; i++ ) {
|
||||
if ( register_oc( otp_oc[i].schema, otp_oc[i].ocp, 0 ) ) {
|
||||
Debug( LDAP_DEBUG_ANY, "otp_initialize: "
|
||||
"register_oc failed\n" );
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return overlay_register( &otp );
|
||||
}
|
||||
|
||||
#if SLAPD_OVER_OTP == SLAPD_MOD_DYNAMIC
|
||||
int
|
||||
init_module( int argc, char *argv[] )
|
||||
{
|
||||
return otp_initialize();
|
||||
}
|
||||
#endif /* SLAPD_OVER_OTP == SLAPD_MOD_DYNAMIC */
|
||||
|
||||
#endif /* defined(SLAPD_OVER_OTP) */
|
61
tests/data/otp_2fa/hotp.ldif
Normal file
61
tests/data/otp_2fa/hotp.ldif
Normal file
@ -0,0 +1,61 @@
|
||||
dn: dc=example, dc=com
|
||||
changetype: modify
|
||||
add: objectClass
|
||||
objectClass: oathHOTPParams
|
||||
-
|
||||
add: oathOTPLength
|
||||
oathOTPLength: 6
|
||||
-
|
||||
add: oathHOTPLookAhead
|
||||
oathHOTPLookAhead: 3
|
||||
-
|
||||
add: oathHMACAlgorithm
|
||||
# SHA-1
|
||||
oathHMACAlgorithm: 1.2.840.113549.2.7
|
||||
|
||||
dn: ou=Information Technology Division,ou=People,dc=example,dc=com
|
||||
changetype: modify
|
||||
add: objectClass
|
||||
objectclass: oathHOTPToken
|
||||
-
|
||||
add: oathHOTPParams
|
||||
oathHOTPParams: dc=example, dc=com
|
||||
-
|
||||
add: oathSecret
|
||||
oathSecret:: PcbKpIJKbSiHZ7IzHiC0MWbLhdk=
|
||||
-
|
||||
add: oathHOTPCounter
|
||||
oathHOTPCounter: 3
|
||||
|
||||
dn: ou=Alumni Association,ou=People,dc=example,dc=com
|
||||
changetype: modify
|
||||
add: objectClass
|
||||
objectClass: oathHOTPParams
|
||||
-
|
||||
add: oathOTPLength
|
||||
oathOTPLength: 8
|
||||
-
|
||||
add: oathHOTPLookAhead
|
||||
oathHOTPLookAhead: 0
|
||||
-
|
||||
add: oathHMACAlgorithm
|
||||
# SHA-512
|
||||
oathHMACAlgorithm: 1.2.840.113549.2.11
|
||||
|
||||
dn: cn=Barbara Jensen,ou=Information Technology Division,ou=People,dc=example,
|
||||
dc=com
|
||||
changetype: modify
|
||||
add: objectClass
|
||||
objectClass: oathHOTPUser
|
||||
-
|
||||
add: oathHOTPToken
|
||||
oathHOTPToken: ou=Information Technology Division,ou=People,dc=example,dc=com
|
||||
|
||||
dn: cn=Bjorn Jensen,ou=Information Technology Division,ou=People,dc=example,
|
||||
dc=com
|
||||
changetype: modify
|
||||
add: objectClass
|
||||
objectClass: oathHOTPUser
|
||||
-
|
||||
add: oathHOTPToken
|
||||
oathHOTPToken: ou=Information Technology Division,ou=People,dc=example,dc=com
|
5
tests/data/otp_2fa/test001-out.ldif
Normal file
5
tests/data/otp_2fa/test001-out.ldif
Normal file
@ -0,0 +1,5 @@
|
||||
dn: ou=Information Technology Division,ou=People,dc=example,dc=com
|
||||
oathSecret:: PcbKpIJKbSiHZ7IzHiC0MWbLhdk=
|
||||
oathHOTPParams: ou=Alumni Association,ou=People,dc=example,dc=com
|
||||
oathHOTPCounter: 12
|
||||
|
64
tests/data/otp_2fa/totp.ldif
Normal file
64
tests/data/otp_2fa/totp.ldif
Normal file
@ -0,0 +1,64 @@
|
||||
dn: dc=example, dc=com
|
||||
changetype: modify
|
||||
add: objectClass
|
||||
objectClass: oathTOTPParams
|
||||
-
|
||||
add: oathOTPLength
|
||||
oathOTPLength: 6
|
||||
-
|
||||
add: oathTOTPTimeStepPeriod
|
||||
oathTOTPTimeStepPeriod: 30
|
||||
-
|
||||
add: oathTOTPTimeStepWindow
|
||||
oathTOTPTimeStepWindow: 3
|
||||
-
|
||||
add: oathHMACAlgorithm
|
||||
# SHA-1
|
||||
oathHMACAlgorithm: 1.2.840.113549.2.7
|
||||
|
||||
dn: ou=Information Technology Division,ou=People,dc=example,dc=com
|
||||
changetype: modify
|
||||
add: objectClass
|
||||
objectclass: oathTOTPToken
|
||||
-
|
||||
add: oathTOTPParams
|
||||
oathTOTPParams: dc=example, dc=com
|
||||
-
|
||||
add: oathSecret
|
||||
oathSecret:: PcbKpIJKbSiHZ7IzHiC0MWbLhdk=
|
||||
|
||||
dn: ou=Alumni Association,ou=People,dc=example,dc=com
|
||||
changetype: modify
|
||||
add: objectClass
|
||||
objectClass: oathTOTPParams
|
||||
-
|
||||
add: oathOTPLength
|
||||
oathOTPLength: 8
|
||||
-
|
||||
add: oathTOTPTimeStepPeriod
|
||||
oathTOTPTimeStepPeriod: 30
|
||||
-
|
||||
add: oathTOTPTimeStepWindow
|
||||
oathTOTPTimeStepWindow: 0
|
||||
-
|
||||
add: oathHMACAlgorithm
|
||||
# SHA-512
|
||||
oathHMACAlgorithm: 1.2.840.113549.2.11
|
||||
|
||||
dn: cn=Barbara Jensen,ou=Information Technology Division,ou=People,dc=example,
|
||||
dc=com
|
||||
changetype: modify
|
||||
add: objectClass
|
||||
objectClass: oathTOTPUser
|
||||
-
|
||||
add: oathTOTPToken
|
||||
oathTOTPToken: ou=Information Technology Division,ou=People,dc=example,dc=com
|
||||
|
||||
dn: cn=Bjorn Jensen,ou=Information Technology Division,ou=People,dc=example,
|
||||
dc=com
|
||||
changetype: modify
|
||||
add: objectClass
|
||||
objectClass: oathTOTPUser
|
||||
-
|
||||
add: oathTOTPToken
|
||||
oathTOTPToken: ou=Information Technology Division,ou=People,dc=example,dc=com
|
@ -49,6 +49,7 @@ AC_deref=deref@BUILD_DEREF@
|
||||
AC_dynlist=dynlist@BUILD_DYNLIST@
|
||||
AC_homedir=homedir@BUILD_HOMEDIR@
|
||||
AC_memberof=memberof@BUILD_MEMBEROF@
|
||||
AC_otp=otp@BUILD_OTP@
|
||||
AC_pcache=pcache@BUILD_PROXYCACHE@
|
||||
AC_ppolicy=ppolicy@BUILD_PPOLICY@
|
||||
AC_refint=refint@BUILD_REFINT@
|
||||
@ -80,7 +81,7 @@ if test "${AC_asyncmeta}" = "asyncmetamod" && test "${AC_LIBS_DYNAMIC}" = "stati
|
||||
fi
|
||||
export AC_ldap AC_mdb AC_meta AC_asyncmeta AC_monitor AC_null AC_perl AC_relay AC_sql \
|
||||
AC_accesslog AC_argon2 AC_autoca AC_constraint AC_dds AC_deref AC_dynlist \
|
||||
AC_homedir AC_memberof AC_pcache AC_ppolicy AC_refint AC_remoteauth \
|
||||
AC_homedir AC_memberof AC_otp AC_pcache AC_ppolicy AC_refint AC_remoteauth \
|
||||
AC_retcode AC_rwm AC_unique AC_syncprov AC_translucent \
|
||||
AC_valsort \
|
||||
AC_lloadd \
|
||||
|
@ -37,6 +37,7 @@ for CMD in $SRCDIR/scripts/test*; do
|
||||
*.bak) continue;;
|
||||
*.orig) continue;;
|
||||
*.sav) continue;;
|
||||
*.py) continue;;
|
||||
*) test -f "$CMD" || continue;;
|
||||
esac
|
||||
|
||||
|
@ -37,6 +37,7 @@ DEREF=${AC_deref-derefno}
|
||||
DYNLIST=${AC_dynlist-dynlistno}
|
||||
HOMEDIR=${AC_homedir-homedirno}
|
||||
MEMBEROF=${AC_memberof-memberofno}
|
||||
OTP=${AC_otp-otpno}
|
||||
PROXYCACHE=${AC_pcache-pcacheno}
|
||||
PPOLICY=${AC_ppolicy-ppolicyno}
|
||||
REFINT=${AC_refint-refintno}
|
||||
|
295
tests/scripts/test080-hotp
Executable file
295
tests/scripts/test080-hotp
Executable file
@ -0,0 +1,295 @@
|
||||
#! /bin/sh
|
||||
# $OpenLDAP$
|
||||
## This work is part of OpenLDAP Software <http://www.openldap.org/>.
|
||||
##
|
||||
## Copyright 2016-2021 Ondřej Kuzník, Symas Corp.
|
||||
## Copyright 2021 The OpenLDAP Foundation.
|
||||
## All rights reserved.
|
||||
##
|
||||
## Redistribution and use in source and binary forms, with or without
|
||||
## modification, are permitted only as authorized by the OpenLDAP
|
||||
## Public License.
|
||||
##
|
||||
## A copy of this license is available in the file LICENSE in the
|
||||
## top-level directory of the distribution or, alternatively, at
|
||||
## <http://www.OpenLDAP.org/license.html>.
|
||||
|
||||
echo "running defines.sh"
|
||||
. $SRCDIR/scripts/defines.sh
|
||||
|
||||
if test $OTP = otpno; then
|
||||
echo "OTP overlay not available, test skipped"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
OTP_DATA=$DATADIR/otp_2fa/hotp.ldif
|
||||
|
||||
# OTPs for this token
|
||||
TOKEN_0=818800
|
||||
TOKEN_1=320382
|
||||
TOKEN_2=404533
|
||||
TOKEN_3=127122
|
||||
TOKEN_4=892599
|
||||
TOKEN_5=407030
|
||||
TOKEN_6=880935
|
||||
TOKEN_7=920291
|
||||
TOKEN_8=145192
|
||||
TOKEN_9=316404
|
||||
TOKEN_10=409144
|
||||
|
||||
# OTPs for the second set of parameters
|
||||
TOKEN_SHA512_11=17544155
|
||||
TOKEN_SHA512_12=48953477
|
||||
|
||||
mkdir -p $TESTDIR $DBDIR1
|
||||
|
||||
echo "Running slapadd to build slapd database..."
|
||||
. $CONFFILTER $BACKEND < $CONF > $ADDCONF
|
||||
$SLAPADD -f $ADDCONF -l $LDIFORDERED
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "slapadd failed ($RC)!"
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
mkdir $TESTDIR/confdir
|
||||
. $CONFFILTER $BACKEND < $CONF > $CONF1
|
||||
|
||||
$SLAPPASSWD -g -n >$CONFIGPWF
|
||||
echo "database config" >>$CONF1
|
||||
echo "rootpw `$SLAPPASSWD -T $CONFIGPWF`" >>$CONF1
|
||||
|
||||
echo "Starting slapd on TCP/IP port $PORT1..."
|
||||
$SLAPD -f $CONF1 -F $TESTDIR/confdir -h $URI1 -d $LVL > $LOG1 2>&1 &
|
||||
PID=$!
|
||||
if test $WAIT != 0 ; then
|
||||
echo PID $PID
|
||||
read foo
|
||||
fi
|
||||
KILLPIDS="$PID"
|
||||
|
||||
sleep $SLEEP0
|
||||
|
||||
for i in 0 1 2 3 4 5; do
|
||||
$LDAPSEARCH -s base -b "$MONITOR" -H $URI1 \
|
||||
'objectclass=*' > /dev/null 2>&1
|
||||
RC=$?
|
||||
if test $RC = 0 ; then
|
||||
break
|
||||
fi
|
||||
echo "Waiting ${SLEEP1} seconds for slapd to start..."
|
||||
sleep ${SLEEP1}
|
||||
done
|
||||
|
||||
if [ "$OTP" = otpmod ]; then
|
||||
$LDAPADD -D cn=config -H $URI1 -y $CONFIGPWF \
|
||||
>> $TESTOUT 2>&1 <<EOMOD
|
||||
dn: cn=module,cn=config
|
||||
objectClass: olcModuleList
|
||||
cn: module
|
||||
olcModulePath: $TESTWD/../servers/slapd/overlays
|
||||
olcModuleLoad: otp_2fa.la
|
||||
EOMOD
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapmodify failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Loading test otp_2fa configuration..."
|
||||
$LDAPMODIFY -v -D cn=config -H $URI1 -y $CONFIGPWF \
|
||||
>> $TESTOUT 2>&1 <<EOMOD
|
||||
dn: olcOverlay={0}otp_2fa,olcDatabase={1}$BACKEND,cn=config
|
||||
changetype: add
|
||||
objectClass: olcOverlayConfig
|
||||
EOMOD
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapmodify failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "Provisioning tokens and configuration..."
|
||||
$LDAPMODIFY -D "$MANAGERDN" -H $URI1 -w $PASSWD \
|
||||
>> $TESTOUT 2>&1 < $OTP_DATA
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapmodify failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
|
||||
echo "Authentication tests:"
|
||||
echo "\ttoken that's not valid yet..."
|
||||
$LDAPWHOAMI -D "$BABSDN" -H $URI1 -w "bjensen$TOKEN_10" \
|
||||
>> $TESTOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 49 ; then
|
||||
echo "ldapwhoami should have failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "\ta valid and expected token..."
|
||||
$LDAPWHOAMI -D "$BABSDN" -H $URI1 -w "bjensen$TOKEN_4" \
|
||||
>> $TESTOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapwhoami failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "\ta valid token skipping some..."
|
||||
$LDAPWHOAMI -D "$BABSDN" -H $URI1 -w "bjensen$TOKEN_6" \
|
||||
>> $TESTOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapwhoami failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "\treusing the same token..."
|
||||
$LDAPWHOAMI -D "$BABSDN" -H $URI1 -w "bjensen$TOKEN_6" \
|
||||
>> $TESTOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 49 ; then
|
||||
echo "ldapwhoami should have failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "\tanother account sharing the same token..."
|
||||
$LDAPWHOAMI -D "$BJORNSDN" -H $URI1 -w "bjorn$TOKEN_7" \
|
||||
>> $TESTOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapwhoami failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "\ttrying an old token..."
|
||||
$LDAPWHOAMI -D "$BJORNSDN" -H $URI1 -w "bjorn$TOKEN_5" \
|
||||
>> $TESTOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 49 ; then
|
||||
echo "ldapwhoami should have failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "\tright token, wrong password..."
|
||||
$LDAPWHOAMI -D "$BJORNSDN" -H $URI1 -w "bjensen$TOKEN_8" \
|
||||
>> $TESTOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 49 ; then
|
||||
echo "ldapwhoami should have failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "\tmaking sure previous token has been retired too..."
|
||||
$LDAPWHOAMI -D "$BJORNSDN" -H $URI1 -w "bjorn$TOKEN_8" \
|
||||
>> $TESTOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 49 ; then
|
||||
echo "ldapwhoami should have failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "\tthe first token we tested that's just become valid..."
|
||||
$LDAPWHOAMI -D "$BABSDN" -H $URI1 -w "bjensen$TOKEN_10" \
|
||||
>> $TESTOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapwhoami failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "Reconfiguring token parameters..."
|
||||
$LDAPMODIFY -D "$MANAGERDN" -H $URI1 -w $PASSWD \
|
||||
>/dev/null 2>&1 << EOMODS
|
||||
dn: ou=Information Technology Division,ou=People,dc=example,dc=com
|
||||
changetype: modify
|
||||
replace: oathHOTPParams
|
||||
oathHOTPParams: ou=Alumni Association,ou=People,dc=example,dc=com
|
||||
EOMODS
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapmodify failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "A new round of tests:"
|
||||
|
||||
echo "\ta long token that's not valid yet..."
|
||||
$LDAPWHOAMI -D "$BABSDN" -H $URI1 -w "bjensen$TOKEN_SHA512_12" \
|
||||
>> $TESTOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 49 ; then
|
||||
echo "ldapwhoami should have failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "\ta valid and expected token..."
|
||||
$LDAPWHOAMI -D "$BABSDN" -H $URI1 -w "bjensen$TOKEN_SHA512_11" \
|
||||
>> $TESTOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapwhoami failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "\tthe previous long token that's just become valid..."
|
||||
$LDAPWHOAMI -D "$BABSDN" -H $URI1 -w "bjensen$TOKEN_SHA512_12" \
|
||||
>> $TESTOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapwhoami failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "Retrieving token status..."
|
||||
$LDAPSEARCH -b "ou=Information Technology Division,ou=People,dc=example,dc=com" \
|
||||
-H $URI1 objectclass=oathHOTPToken '@oathHOTPToken' \
|
||||
>> $SEARCHOUT 2>&1
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapsearch failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
|
||||
LDIF=$DATADIR/otp_2fa/test001-out.ldif
|
||||
|
||||
echo "Filtering ldapsearch results..."
|
||||
$LDIFFILTER < $SEARCHOUT > $SEARCHFLT
|
||||
echo "Filtering ldif with expected data..."
|
||||
$LDIFFILTER < $LDIF > $LDIFFLT
|
||||
echo "Comparing filter output..."
|
||||
$CMP $SEARCHFLT $LDIFFLT > $CMPOUT
|
||||
|
||||
if test $? != 0 ; then
|
||||
echo "Comparison failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ">>>>> Test succeeded"
|
||||
|
||||
test $KILLSERVERS != no && wait
|
||||
|
||||
exit 0
|
143
tests/scripts/test081-totp
Executable file
143
tests/scripts/test081-totp
Executable file
@ -0,0 +1,143 @@
|
||||
#!/bin/sh
|
||||
# $OpenLDAP$
|
||||
## This work is part of OpenLDAP Software <http://www.openldap.org/>.
|
||||
##
|
||||
## Copyright 2016-2021 Ondřej Kuzník, Symas Corp.
|
||||
## Copyright 2021 The OpenLDAP Foundation.
|
||||
## All rights reserved.
|
||||
##
|
||||
## Redistribution and use in source and binary forms, with or without
|
||||
## modification, are permitted only as authorized by the OpenLDAP
|
||||
## Public License.
|
||||
##
|
||||
## A copy of this license is available in the file LICENSE in the
|
||||
## top-level directory of the distribution or, alternatively, at
|
||||
## <http://www.OpenLDAP.org/license.html>.
|
||||
|
||||
echo "running defines.sh"
|
||||
. $SRCDIR/scripts/defines.sh
|
||||
|
||||
if test $OTP = otpno; then
|
||||
echo "OTP overlay not available, test skipped"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for python in python3 python2 python2.7 python27 python ""; do
|
||||
if test x"$python" = x; then
|
||||
echo "Useable Python environment not found, skipping test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"$python" "$0".py --check >>$TESTOUT 2>&1
|
||||
RC=$?
|
||||
case $RC in
|
||||
0)
|
||||
break;;
|
||||
1)
|
||||
echo "$python is missing some required modules, skipping"
|
||||
python=""
|
||||
continue;;
|
||||
127)
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
export URI1 MANAGERDN PASSWD BABSDN BJORNSDN
|
||||
|
||||
OTP_DATA=$DATADIR/otp_2fa/totp.ldif
|
||||
|
||||
mkdir -p $TESTDIR $DBDIR1
|
||||
|
||||
echo "Running slapadd to build slapd database..."
|
||||
. $CONFFILTER $BACKEND < $CONF > $ADDCONF
|
||||
$SLAPADD -f $ADDCONF -l $LDIFORDERED
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "slapadd failed ($RC)!"
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
mkdir $TESTDIR/confdir
|
||||
. $CONFFILTER $BACKEND < $CONF > $CONF1
|
||||
|
||||
$SLAPPASSWD -g -n >$CONFIGPWF
|
||||
echo "database config" >>$CONF1
|
||||
echo "rootpw `$SLAPPASSWD -T $CONFIGPWF`" >>$CONF1
|
||||
|
||||
echo "Starting slapd on TCP/IP port $PORT1..."
|
||||
$SLAPD -f $CONF1 -F $TESTDIR/confdir -h $URI1 -d $LVL > $LOG1 2>&1 &
|
||||
PID=$!
|
||||
if test $WAIT != 0 ; then
|
||||
echo PID $PID
|
||||
read foo
|
||||
fi
|
||||
KILLPIDS="$PID"
|
||||
|
||||
sleep $SLEEP0
|
||||
|
||||
for i in 0 1 2 3 4 5; do
|
||||
$LDAPSEARCH -s base -b "$MONITOR" -H $URI1 \
|
||||
'objectclass=*' > /dev/null 2>&1
|
||||
RC=$?
|
||||
if test $RC = 0 ; then
|
||||
break
|
||||
fi
|
||||
echo "Waiting ${SLEEP1} seconds for slapd to start..."
|
||||
sleep ${SLEEP1}
|
||||
done
|
||||
|
||||
if [ "$OTP" = otpmod ]; then
|
||||
$LDAPADD -D cn=config -H $URI1 -y $CONFIGPWF \
|
||||
>> $TESTOUT 2>&1 <<EOMOD
|
||||
dn: cn=module,cn=config
|
||||
objectClass: olcModuleList
|
||||
cn: module
|
||||
olcModulePath: $TESTWD/../servers/slapd/overlays
|
||||
olcModuleLoad: otp_2fa.la
|
||||
EOMOD
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapmodify failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Loading test otp_2fa configuration..."
|
||||
$LDAPMODIFY -v -D cn=config -H $URI1 -y $CONFIGPWF \
|
||||
>> $TESTOUT 2>&1 <<EOMOD
|
||||
dn: olcOverlay={0}otp_2fa,olcDatabase={1}$BACKEND,cn=config
|
||||
changetype: add
|
||||
objectClass: olcOverlayConfig
|
||||
EOMOD
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapmodify failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
echo "Provisioning tokens and configuration..."
|
||||
$LDAPMODIFY -D "$MANAGERDN" -H $URI1 -w $PASSWD \
|
||||
>> $TESTOUT 2>&1 < $OTP_DATA
|
||||
RC=$?
|
||||
if test $RC != 0 ; then
|
||||
echo "ldapmodify failed ($RC)!"
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
exit $RC
|
||||
fi
|
||||
|
||||
"$python" "$0".py
|
||||
RC=$?
|
||||
|
||||
test $KILLSERVERS != no && kill -HUP $KILLPIDS
|
||||
|
||||
if test $RC != 0 ; then
|
||||
echo "Test failed ($RC)!"
|
||||
else
|
||||
echo ">>>>> Test succeeded"
|
||||
fi
|
||||
|
||||
test $KILLSERVERS != no && wait
|
||||
|
||||
exit $RC
|
182
tests/scripts/test081-totp.py
Executable file
182
tests/scripts/test081-totp.py
Executable file
@ -0,0 +1,182 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# $OpenLDAP$
|
||||
## This work is part of OpenLDAP Software <http://www.openldap.org/>.
|
||||
##
|
||||
## Copyright 2016-2021 Ondřej Kuzník, Symas Corp.
|
||||
## Copyright 2021 The OpenLDAP Foundation.
|
||||
## All rights reserved.
|
||||
##
|
||||
## Redistribution and use in source and binary forms, with or without
|
||||
## modification, are permitted only as authorized by the OpenLDAP
|
||||
## Public License.
|
||||
##
|
||||
## A copy of this license is available in the file LICENSE in the
|
||||
## top-level directory of the distribution or, alternatively, at
|
||||
## <http://www.OpenLDAP.org/license.html>.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
import ldap
|
||||
from ldap.cidict import cidict as CIDict
|
||||
from ldap.ldapobject import LDAPObject
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--check":
|
||||
raise SystemExit(0)
|
||||
|
||||
|
||||
def get_digits(h, digits):
|
||||
offset = h[19] & 15
|
||||
number = struct.unpack(">I", h[offset:offset+4])[0] & 0x7fffffff
|
||||
number %= (10 ** digits)
|
||||
return ("%0*d" % (digits, number)).encode()
|
||||
|
||||
|
||||
def get_hotp_token(secret, interval_no):
|
||||
msg = struct.pack(">Q", interval_no)
|
||||
h = hmac.new(secret, msg, hashlib.sha1).digest()
|
||||
return get_digits(bytearray(h), 6)
|
||||
|
||||
|
||||
def get_interval(period=30):
|
||||
return int(time.time() // period)
|
||||
|
||||
|
||||
def get_token_for(connection, dn, typ="totp"):
|
||||
result = connection.search_s(dn, ldap.SCOPE_BASE)
|
||||
dn, attrs = result[0]
|
||||
attrs = CIDict(attrs)
|
||||
|
||||
tokendn = attrs['oath'+typ+'token'][0].decode()
|
||||
|
||||
result = connection.search_s(tokendn, ldap.SCOPE_BASE)
|
||||
dn, attrs = result[0]
|
||||
attrs = CIDict(attrs)
|
||||
|
||||
return dn, attrs
|
||||
|
||||
|
||||
def main():
|
||||
uri = os.environ["URI1"]
|
||||
|
||||
managerdn = os.environ['MANAGERDN']
|
||||
passwd = os.environ['PASSWD']
|
||||
|
||||
babsdn = os.environ['BABSDN']
|
||||
babspw = b"bjensen"
|
||||
|
||||
bjornsdn = os.environ['BJORNSDN']
|
||||
bjornspw = b"bjorn"
|
||||
|
||||
connection = LDAPObject(uri)
|
||||
|
||||
start = time.time()
|
||||
connection.bind_s(managerdn, passwd)
|
||||
end = time.time()
|
||||
|
||||
if end - start > 1:
|
||||
print("It takes more than a second to connect and bind, "
|
||||
"skipping potentially unstable test", file=sys.stderr)
|
||||
raise SystemExit(0)
|
||||
|
||||
dn, token_entry = get_token_for(connection, babsdn)
|
||||
|
||||
paramsdn = token_entry['oathTOTPParams'][0].decode()
|
||||
result = connection.search_s(paramsdn, ldap.SCOPE_BASE)
|
||||
_, attrs = result[0]
|
||||
params = CIDict(attrs)
|
||||
|
||||
secret = token_entry['oathSecret'][0]
|
||||
period = int(params['oathTOTPTimeStepPeriod'][0].decode())
|
||||
|
||||
bind_conn = LDAPObject(uri)
|
||||
|
||||
interval_no = get_interval(period)
|
||||
token = get_hotp_token(secret, interval_no-3)
|
||||
|
||||
print("Testing old tokens are not useable")
|
||||
bind_conn.bind_s(babsdn, babspw+token)
|
||||
try:
|
||||
bind_conn.bind_s(babsdn, babspw+token)
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
pass
|
||||
else:
|
||||
raise SystemExit("Bind with an old token should have failed")
|
||||
|
||||
interval_no = get_interval(period)
|
||||
token = get_hotp_token(secret, interval_no)
|
||||
|
||||
print("Testing token can only be used once")
|
||||
bind_conn.bind_s(babsdn, babspw+token)
|
||||
try:
|
||||
bind_conn.bind_s(babsdn, babspw+token)
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
pass
|
||||
else:
|
||||
raise SystemExit("Bind with a reused token should have failed")
|
||||
|
||||
token = get_hotp_token(secret, interval_no+1)
|
||||
try:
|
||||
bind_conn.bind_s(babsdn, babspw+token)
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
raise SystemExit("Bind should have succeeded")
|
||||
|
||||
dn, token_entry = get_token_for(connection, babsdn)
|
||||
last = int(token_entry['oathTOTPLastTimeStep'][0].decode())
|
||||
if last != interval_no+1:
|
||||
SystemExit("Unexpected counter value %d (expected %d)" %
|
||||
(last, interval_no+1))
|
||||
|
||||
print("Resetting counter and testing secret sharing between accounts")
|
||||
connection.modify_s(dn, [(ldap.MOD_REPLACE, 'oathTOTPLastTimeStep', [])])
|
||||
|
||||
interval_no = get_interval(period)
|
||||
token = get_hotp_token(secret, interval_no)
|
||||
|
||||
try:
|
||||
bind_conn.bind_s(bjornsdn, bjornspw+token)
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
raise SystemExit("Bind should have succeeded")
|
||||
|
||||
try:
|
||||
bind_conn.bind_s(babsdn, babspw+token)
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
pass
|
||||
else:
|
||||
raise SystemExit("Bind with a reused token should have failed")
|
||||
|
||||
print("Testing token is retired even with a wrong password")
|
||||
connection.modify_s(dn, [(ldap.MOD_REPLACE, 'oathTOTPLastTimeStep', [])])
|
||||
|
||||
interval_no = get_interval(period)
|
||||
token = get_hotp_token(secret, interval_no)
|
||||
|
||||
try:
|
||||
bind_conn.bind_s(babsdn, b"not the password"+token)
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
pass
|
||||
else:
|
||||
raise SystemExit("Bind with an incorrect password should have failed")
|
||||
|
||||
try:
|
||||
bind_conn.bind_s(babsdn, babspw+token)
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
pass
|
||||
else:
|
||||
raise SystemExit("Bind with a reused token should have failed")
|
||||
|
||||
token = get_hotp_token(secret, interval_no+1)
|
||||
try:
|
||||
bind_conn.bind_s(babsdn, babspw+token)
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
raise SystemExit("Bind should have succeeded")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
Loading…
Reference in New Issue
Block a user