ITS#9437 Add otp_2fa overlay

This commit is contained in:
Ondřej Kuzník 2021-01-25 14:22:23 +00:00 committed by Quanah Gibson-Mount
parent 3bd1b0909a
commit 87f3bad8bb
13 changed files with 1863 additions and 1 deletions

View File

@ -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)

View 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.

View File

@ -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)

View 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,
&params ) ) {
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,
&params ) ) {
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) */

View 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

View 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

View 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

View File

@ -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 \

View File

@ -37,6 +37,7 @@ for CMD in $SRCDIR/scripts/test*; do
*.bak) continue;;
*.orig) continue;;
*.sav) continue;;
*.py) continue;;
*) test -f "$CMD" || continue;;
esac

View File

@ -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
View 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
View 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
View 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())