Last active
June 3, 2024 12:14
-
-
Save joostd/aa213a6f6fe654fcff91429055056915 to your computer and use it in GitHub Desktop.
Validate a YubiOTP value
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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