I'm a little bit confused by what I need to do here for Python, but from the Yubikey API documentation for verifying Yubikeys that have YubiOTP the HMAC signature needs to be generated a specific way - from their documentation:
Generating signatures
The protocol uses HMAC-SHA-1 signatures. The HMAC key to use is the client API key.
Generate the signature over the parameters in the message. Each message contains a set of key/value pairs, and the signature is always over the entire set (excluding the signature itself), and sorted in alphabetical order of the keys. More precisely, to generate a message signature do:
Alphabetically sort the set of key/value pairs by key order.
Construct a single line with each ordered key/value pair concatenated using &, and each key and value contatenated with
=
. Do not add any linebreaks. Do not add whitespace. For example:a=2&b=1&c=3
.Apply the HMAC-SHA-1 algorithm on the line as an octet string using the API key as key (remember to base64decode the API key obtained from Yubico).
Base 64 encode the resulting value according to RFC 4648, for example,
t2ZMtKeValdA+H0jVpj3LIichn4=
.Append the value under key h to the message.
Now my understanding of their API from their documentation states the following valid request parameters:
id
- the Client ID from Yubico APIotp
- the YubiOTP value from the YubiOTP component of a yubikey.h
- the HMAC-SHA1 signature for the requesttimestamp
- empty does nothing, 1
includes the timestamp in the reply from the servernonce
- A 16 to 40 character long string with random unique data.sl
- a value of 0 to 100 indicating percentage of syncing required by client, or strings "fast" or "Secure" to use server values; if nonexistent server decidestimeout
- # of seconds to wait for sync responses; let server decide if absent.I have a total of two functions I'm trying to use to try and handle all these things and generate the URL. Namely, we the HMAC support function and the verify_url_generate
which generates the URL (and API_KEY
is statically coded - my API Secret Key from Yubico):
def generate_signature(message, key=base64.b64decode(API_KEY)):
message = bytes(message, 'UTF-8')
digester = hmac.new(key, message, hashlib.sha1)
digest = digester.digest()
signature = base64.urlsafe_b64encode(digest)
return str(signature, 'UTF-8')
def verify_url_generate(otp):
nonce = "".join(secrets.choice(ascii_lowercase) for _ in range(40))
data = OrderedDict(
{
"id": None,
"nonce": None,
"otp": None,
"sl": 50,
"timeout": 10,
"timestamp": 1
}
)
data['otp'] = otp
data['id'] = CLIENT_ID
data['nonce'] = nonce
args = ""
for key, value in data.items():
args += f"{key}={value}&"
sig = generate_signature(args[:-1])
url = YUBICO_API_URL + args + "&h=" + sig
print(url)
return
Any URL generated by this triggers a notice about "BAD_SIGNATURE" from the remote site - any URL generated minus the HMAC sig (h=
) parameter works. So we know the issue isn't the URL, it's the HMAC signature.
Does anyone know what I'm doing wrong with my HMAC generation approach, by passing the HMAC sig generator the concatenated args from the ordered dict in key=value
separated by &
for each parameter format?
Can you try using standard_b64encode and then using urllib.parse.quote(url) in your final URL?
I ask because this page says that "As such, all parameters must be properly URL encoded. In particular, some base64 characters (such as "+") in the value fields needs to be escaped." which means it is expecting +(or %2B) in the args and does a unquote and then normal decode.