Skip to content

Instantly share code, notes, and snippets.

@joostd
Last active June 3, 2024 12:14
Show Gist options
  • Save joostd/aa213a6f6fe654fcff91429055056915 to your computer and use it in GitHub Desktop.
Save joostd/aa213a6f6fe654fcff91429055056915 to your computer and use it in GitHub Desktop.
Validate a YubiOTP value
#!/usr/bin/env python
# validate Yubico OTP
# To get your API key:
# https://upgrade.yubico.com/getapikey/
from sys import exit, stderr
from argparse import ArgumentParser
from requests import get
from secrets import token_hex
import hmac
from hashlib import sha1
from base64 import b64encode, b64decode
# this script implements the Yubico OTP validation protocol, see:
# https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
# verify API endpoint:
url = 'https://api.yubico.com/wsapi/2.0/verify'
# calculate signature over API request data
def sign(key, data):
# data must be sorted
sorted_data = sorted([ f'{name}={value}' for name,value in data.items() ])
datatosign = "&".join(sorted_data).encode('utf-8')
return hmac.new(key, datatosign, sha1).digest()
# parse command line arguments
parser = ArgumentParser(description='validate a YubiOTP')
parser.add_argument('--id', dest='id', required=True, help='specify YubiCloud client ID (eg 12345)')
parser.add_argument('--key', dest='key', required=True, help='specify YubiCloud client HMAC key (base64-encoded)')
parser.add_argument('--otp', dest='otp', required=True, help='specify OTP to validate (eg ccccc...)')
parser.add_argument('--timeout', dest='timeout', required=False, help='number of seconds to wait for sync responses')
parser.add_argument('--sl', dest='sl', required=False, help='percentage of syncing required by client')
parser.add_argument('--timestamp', dest='timestamp', required=False, action='store_true', help='requests timestamp and session counter information in the response')
parser.add_argument('--verbose', dest='verbose', required=False, action='store_true', help='verbose output')
args = parser.parse_args()
# input parameters
nonce = token_hex(20)
data_in = {
"id": args.id,
"nonce": nonce,
"otp": args.otp,
}
if args.timestamp:
data_in["timestamp"] = "1"
if args.sl:
data_in["sl"] = args.sl
if args.timeout:
data_in["timeout"] = args.timeout
# add API request signature
key = b64decode(args.key)
data_in['h'] = b64encode(sign(key, data_in))
# query YubiCloud
response = get(url, params=data_in)
# parse response
lines = [ line.strip() for line in response.text.split('\r\n') ]
data_out = dict([ line.partition("=")[::2] for line in lines if line])
# validate response signature
response_sig = b64decode(data_out["h"])
del data_out["h"] # remove signature before verifying
calculated_sig = sign(key, data_out)
try:
if( calculated_sig != response_sig):
raise Exception('signature validation failed', data_out['status'])
if( args.otp != data_out['otp']):
raise Exception('OTPs do not match', data_out['status'])
if( nonce != data_out['nonce']):
raise Exception('nonce does not match', data_out['status'])
if( "OK" != data_out['status']):
raise Exception('OTP validation failed', data_out['status'])
print(f"OTP is valid ({data_out['status']}) at {data_out['t']}", file=stderr)
if args.verbose:
print(f"percentage of external validation servers that replied successfully: {data_out['sl']}%", file=stderr)
if args.timestamp:
print(f"OTP timestamp: {data_out['timestamp']}, usage counter: {data_out['sessioncounter']}, session usage counter: {data_out['sessionuse']}", file=stderr)
exit(0)
except Exception as e:
msg,status = e.args
print(f"ERROR: {msg} ({data_out['status']})", file=stderr)
exit(-1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment