Skip to content

Commit

Permalink
Merge branch 'fix/LINOTP-795-for-master' into 'master'
Browse files Browse the repository at this point in the history
LINOTP-975 fix for master

See merge request dev/linotp/linotp!51
  • Loading branch information
Andreas Rammhold committed Jun 25, 2019
2 parents fcce22f + 86bf5f8 commit 6d28d93
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 2 deletions.
6 changes: 6 additions & 0 deletions adminclient/LinOTPAdminClientCLI/src/debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
linotp-adminclient-cli (2.10.7~0dev0-1) jessie; urgency=low

* Set new version 2.10.7dev0

-- LSE LinOTP2 Packaging <linotp2@lsexperts.de> Mon, 24 Jun 2019 16:24:24 +0200

linotp-adminclient-cli (2.10.5.2-1) jessie; urgency=low

* Set new version 2.10.5.2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@
__license__ = "Gnu AGPLv3"
__contact__ = "www.linotp.org"
__email__ = "linotp@keyidentity.com"
__version__ = '2.10.5.2'
__version__ = '2.10.7.dev0'
6 changes: 6 additions & 0 deletions linotpd/src/debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
linotp (2.10.7~0dev0-1) jessie; urgency=low

* Fix for TOTP replay using auto-resync [CVE-2019-12887]

-- LSE LinOTP2 Packaging <linotp2@lsexperts.de> Mon, 24 Jun 2019 16:11:41 +0200

linotp (2.10.5.2-1) jessie; urgency=low

Fix:
Expand Down
2 changes: 1 addition & 1 deletion linotpd/src/linotp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,5 @@
__license__ = "Gnu AGPLv3"
__contact__ = "www.linotp.org"
__email__ = "linotp@keyidentity.com"
__version__ = '2.10.5.2'
__version__ = '2.10.7.dev0'
__api__ = "2.0802"
171 changes: 171 additions & 0 deletions linotpd/src/linotp/tests/functional/test_totp.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,177 @@ def test_use_token_twice(self):

return

def test_resync_no_replay(self):
'''
totp test: verify that auto resync does not succeed with reused (sync) OTPs
We will use the same OTP twice. Once for starting the sync
and then to complete it. Both of those should not yield a
valid authentication. The user must provide two consecutive
OTPs to finish the sync. The second OTP must be within a
small timeframe after the first.
'''
user = 'root'
step = 30

params = {
'AutoResyncTimeout': '240',
'AutoResync': True
}

response = self.make_system_request('setConfig', params=params)
assert 'false' not in response.body

for offset in range(10*step, 20*step, step/2):
# Freeze time to the current system time
with freeze_time(datetime.datetime.now()) as frozen_time:
t1 = TotpToken(timestep=step)
key = t1.getKey().encode('hex')
step = t1.getTimeStep()

tserial = self.addToken(user=user, otplen=t1.digits,
typ='totp', key=key, timeStep=step)

self.serials.append(tserial)

(otp, counter) = t1.getOtp()
_tt1 = t1.getTimeFromCounter(counter)

res = self.checkOtp(user, otp)
assert '"value": true' in res.body

# replay doesn't work
res = self.checkOtp(user, otp)
assert '"value": false' in res.body

# advance to a future time where the old otp is no longer valid
frozen_time.tick(delta=datetime.timedelta(seconds=offset))

# start resync
res = self.checkOtp(user, otp)
assert '"value": false' in res.body, "%s: %s" %(offset,res.body)

# finish resync
res = self.checkOtp(user, otp)
assert '"value": true' not in res.body, offset

def test_resync_non_consecutive(self):
'''
totp test: verify that auto resync does not succeed with non-consecutive OTPs
'''
user = 'root'
timeWindow = 180
params = {
'AutoResyncTimeout': '240',
'AutoResync': True
}

response = self.make_system_request('setConfig', params=params)
assert 'false' not in response.body

# Freeze time to the current system time
with freeze_time(datetime.datetime.now()) as frozen_time:
t1 = TotpToken()
key = t1.getKey().encode('hex')
step = t1.getTimeStep()

tserial = self.addToken(user=user, otplen=t1.digits,
typ='totp', key=key, timeStep=step)

self.serials.append(tserial)

(otp, counter) = t1.getOtp()

res = self.checkOtp(user, otp)
assert '"value": true' in res.body

# advance to a future time where the old otp is no longer valid
frozen_time.tick(delta=datetime.timedelta(seconds=timeWindow))
res = self.checkOtp(user, otp)
assert '"value": false' in res.body

# skip enough OTPs to leave the current window
counter += 2 * timeWindow

# get the first token
(first_otp, _) = t1.getOtp(counter=counter)
# get the second token
(second_otp, counter) = t1.getOtp(counter=counter+step)

# start resync with 2nd otp
res = self.checkOtp(user, second_otp)
assert '"value": false' in res.body

# provide the first OTP for the resync, it should fail
res = self.checkOtp(user, first_otp)
assert '"value": true' not in res.body

def test_resync_consecutive(self):
'''
totp test: verify that auto resync does succeed with consecutive OTPs and fails if they are outside of the range
'''
user = 'root'
timeWindow = 180
syncTimeout = 240
step = 30
params = {
'AutoResyncTimeout': '%s' % syncTimeout,
'AutoResync': True
}

response = self.make_system_request('setConfig', params=params)
assert 'false' not in response.body

for offset in range(1, 5):
# Freeze time to the current system time
with freeze_time(datetime.datetime.now()) as frozen_time:
t1 = TotpToken(timestep=step)
key = t1.getKey().encode('hex')

tserial = self.addToken(user=user, otplen=t1.digits,
typ='totp', key=key, timeStep=step)

self.serials.append(tserial)

(otp, counter) = t1.getOtp()
res = self.checkOtp(user, otp)
assert '"value": true' in res.body

log.info("Successful counter: %s", counter)

# advance to a future time where the old otp is no longer valid
frozen_time.tick(delta=datetime.timedelta(seconds=timeWindow))
res = self.checkOtp(user, otp)
assert '"value": false' in res.body

counter_advance = (40 * timeWindow)
log.info("Advancing counter by %s", counter_advance)
# skip enough OTPs to leave the current window
counter = (counter * step) + counter_advance

# get the first token
(first_otp, first_counter) = t1.getOtp(counter=counter)

# get the second token that is offset by a few but within range
(second_otp, second_counter) = t1.getOtp(counter=counter+step*offset)

log.info("First OTP: %s (%s), Second OTP: %s (%s)",
first_otp, first_counter,
second_otp, second_counter)

# start resync with a valid OTP
res = self.checkOtp(user, first_otp)
assert '"value": false' in res.body

# provide the second otp that follows the previous one
res = self.checkOtp(user, second_otp)

if offset <= 3:
# as long as the OTP is not out of the sync range it should be good
assert '"value": true' in res.body, offset
else:
# if we are out of the sync range the OTP should be rejected
assert '"value": false' in res.body, offset

def test_getotp(self):
'''
Expand Down
14 changes: 14 additions & 0 deletions linotpd/src/linotp/tokens/totptoken.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,20 @@ def autosync(self, hmac2Otp, anOtpVal):
#check if the otpval is valid in the sync scope
res = hmac2Otp.checkOtp(anOtpVal, syncWindow, symetric=True)

# ------------------------------------------------------------------ --

# protect against a replay

# if the counter belonging to the provided otp is lower than the one
# we have last seen (which is the stored otp counter), then we deny
# the resync as it might be replay or an error

if res != -1 and res < self.getOtpCount():
log.info('otp below the last seen!')
return -1

# ------------------------------------------------------------------ --

#if yes:
if res != -1:
# if former is defined
Expand Down

0 comments on commit 6d28d93

Please sign in to comment.