test081-totp.py revision 1.1.1.1
1# -*- coding: utf-8 -*-
2# $OpenLDAP$
3## This work is part of OpenLDAP Software <http://www.openldap.org/>.
4##
5## Copyright 2016-2021 Ond��ej Kuzn��k, Symas Corp.
6## Copyright 2021 The OpenLDAP Foundation.
7## All rights reserved.
8##
9## Redistribution and use in source and binary forms, with or without
10## modification, are permitted only as authorized by the OpenLDAP
11## Public License.
12##
13## A copy of this license is available in the file LICENSE in the
14## top-level directory of the distribution or, alternatively, at
15## <http://www.OpenLDAP.org/license.html>.
16
17from __future__ import print_function
18
19import hashlib
20import hmac
21import os
22import struct
23import sys
24import time
25
26import ldap
27from ldap.cidict import cidict as CIDict
28from ldap.ldapobject import LDAPObject
29
30if len(sys.argv) > 1 and sys.argv[1] == "--check":
31    raise SystemExit(0)
32
33
34def get_digits(h, digits):
35    offset = h[19] & 15
36    number = struct.unpack(">I", h[offset:offset+4])[0] & 0x7fffffff
37    number %= (10 ** digits)
38    return ("%0*d" % (digits, number)).encode()
39
40
41def get_hotp_token(secret, interval_no):
42    msg = struct.pack(">Q", interval_no)
43    h = hmac.new(secret, msg, hashlib.sha1).digest()
44    return get_digits(bytearray(h), 6)
45
46
47def get_interval(period=30):
48    return int(time.time() // period)
49
50
51def get_token_for(connection, dn, typ="totp"):
52    result = connection.search_s(dn, ldap.SCOPE_BASE)
53    dn, attrs = result[0]
54    attrs = CIDict(attrs)
55
56    tokendn = attrs['oath'+typ+'token'][0].decode()
57
58    result = connection.search_s(tokendn, ldap.SCOPE_BASE)
59    dn, attrs = result[0]
60    attrs = CIDict(attrs)
61
62    return dn, attrs
63
64
65def main():
66    uri = os.environ["URI1"]
67
68    managerdn = os.environ['MANAGERDN']
69    passwd = os.environ['PASSWD']
70
71    babsdn = os.environ['BABSDN']
72    babspw = b"bjensen"
73
74    bjornsdn = os.environ['BJORNSDN']
75    bjornspw = b"bjorn"
76
77    connection = LDAPObject(uri)
78
79    start = time.time()
80    connection.bind_s(managerdn, passwd)
81    end = time.time()
82
83    if end - start > 1:
84        print("It takes more than a second to connect and bind, "
85              "skipping potentially unstable test", file=sys.stderr)
86        raise SystemExit(0)
87
88    dn, token_entry = get_token_for(connection, babsdn)
89
90    paramsdn = token_entry['oathTOTPParams'][0].decode()
91    result = connection.search_s(paramsdn, ldap.SCOPE_BASE)
92    _, attrs = result[0]
93    params = CIDict(attrs)
94
95    secret = token_entry['oathSecret'][0]
96    period = int(params['oathTOTPTimeStepPeriod'][0].decode())
97
98    bind_conn = LDAPObject(uri)
99
100    interval_no = get_interval(period)
101    token = get_hotp_token(secret, interval_no-3)
102
103    print("Testing old tokens are not useable")
104    bind_conn.bind_s(babsdn, babspw+token)
105    try:
106        bind_conn.bind_s(babsdn, babspw+token)
107    except ldap.INVALID_CREDENTIALS:
108        pass
109    else:
110        raise SystemExit("Bind with an old token should have failed")
111
112    interval_no = get_interval(period)
113    token = get_hotp_token(secret, interval_no)
114
115    print("Testing token can only be used once")
116    bind_conn.bind_s(babsdn, babspw+token)
117    try:
118        bind_conn.bind_s(babsdn, babspw+token)
119    except ldap.INVALID_CREDENTIALS:
120        pass
121    else:
122        raise SystemExit("Bind with a reused token should have failed")
123
124    token = get_hotp_token(secret, interval_no+1)
125    try:
126        bind_conn.bind_s(babsdn, babspw+token)
127    except ldap.INVALID_CREDENTIALS:
128        raise SystemExit("Bind should have succeeded")
129
130    dn, token_entry = get_token_for(connection, babsdn)
131    last = int(token_entry['oathTOTPLastTimeStep'][0].decode())
132    if last != interval_no+1:
133        SystemExit("Unexpected counter value %d (expected %d)" %
134                   (last, interval_no+1))
135
136    print("Resetting counter and testing secret sharing between accounts")
137    connection.modify_s(dn, [(ldap.MOD_REPLACE, 'oathTOTPLastTimeStep', [])])
138
139    interval_no = get_interval(period)
140    token = get_hotp_token(secret, interval_no)
141
142    try:
143        bind_conn.bind_s(bjornsdn, bjornspw+token)
144    except ldap.INVALID_CREDENTIALS:
145        raise SystemExit("Bind should have succeeded")
146
147    try:
148        bind_conn.bind_s(babsdn, babspw+token)
149    except ldap.INVALID_CREDENTIALS:
150        pass
151    else:
152        raise SystemExit("Bind with a reused token should have failed")
153
154    print("Testing token is retired even with a wrong password")
155    connection.modify_s(dn, [(ldap.MOD_REPLACE, 'oathTOTPLastTimeStep', [])])
156
157    interval_no = get_interval(period)
158    token = get_hotp_token(secret, interval_no)
159
160    try:
161        bind_conn.bind_s(babsdn, b"not the password"+token)
162    except ldap.INVALID_CREDENTIALS:
163        pass
164    else:
165        raise SystemExit("Bind with an incorrect password should have failed")
166
167    try:
168        bind_conn.bind_s(babsdn, babspw+token)
169    except ldap.INVALID_CREDENTIALS:
170        pass
171    else:
172        raise SystemExit("Bind with a reused token should have failed")
173
174    token = get_hotp_token(secret, interval_no+1)
175    try:
176        bind_conn.bind_s(babsdn, babspw+token)
177    except ldap.INVALID_CREDENTIALS:
178        raise SystemExit("Bind should have succeeded")
179
180
181if __name__ == "__main__":
182    sys.exit(main())
183