yubicootp: Add signature support

Works both ways! Optional too!
This commit is contained in:
Ave 2020-10-13 18:04:21 +03:00
parent 490916a1ca
commit 0487031974
No known key found for this signature in database
GPG key ID: 398DD7BD03276F6D
2 changed files with 35 additions and 6 deletions

View file

@ -3,6 +3,8 @@ import re
import config import config
import secrets import secrets
import asyncio import asyncio
import base64
import hmac
class YubicoOTP(Cog): class YubicoOTP(Cog):
@ -53,10 +55,31 @@ class YubicoOTP(Cog):
return int("".join(hexconv), 16) return int("".join(hexconv), 16)
def calc_signature(self, text):
key = base64.b64decode(config.yubico_otp_secret)
signature_bytes = hmac.digest(key, text.encode(), "SHA1")
return base64.b64encode(signature_bytes).decode()
def validate_response_signature(self, response_dict):
yubico_signature = response_dict["h"]
to_sign = ""
for key in sorted(response_dict.keys()):
if key == "h":
continue
to_sign += f"{key}={response_dict[key]}&"
our_signature = self.calc_signature(to_sign.strip("&"))
return our_signature == yubico_signature
async def validate_yubico_otp(self, otp): async def validate_yubico_otp(self, otp):
nonce = secrets.token_hex(15) # Random number in the valid range nonce = secrets.token_hex(15) # Random number in the valid range
params = f"id={config.yubico_otp_client_id}&nonce={nonce}&otp={otp}"
# If secret is supplied, sign our request
if config.yubico_otp_secret:
params += "&h=" + self.calc_signature(params)
for api_server in self.api_servers: for api_server in self.api_servers:
url = f"{api_server}/wsapi/2.0/verify?id={config.yubico_otp_client_id}&otp={otp}&nonce={nonce}" url = f"{api_server}/wsapi/2.0/verify?{params}"
try: try:
resp = await self.bot.aiosession.get(url) resp = await self.bot.aiosession.get(url)
assert resp.status == 200 assert resp.status == 200
@ -71,7 +94,13 @@ class YubicoOTP(Cog):
datafields = resptext.strip().split("\r\n") datafields = resptext.strip().split("\r\n")
datafields = {line.split("=")[0]: line.split("=")[1] for line in datafields} datafields = {line.split("=")[0]: line.split("=")[1] for line in datafields}
datafields["nonce"] != nonce # Verify nonce
assert datafields["nonce"] == nonce
# Verify signature if secret is present
if config.yubico_otp_secret:
assert self.validate_response_signature(datafields)
# If we got a success, then return True # If we got a success, then return True
if datafields["status"] == "OK": if datafields["status"] == "OK":
return True return True

View file

@ -326,9 +326,9 @@ pingmods_role = 360138431524765707
modtoggle_role = 360138431524765707 modtoggle_role = 360138431524765707
# == Only if you want to use cogs.yubicootp == # == Only if you want to use cogs.yubicootp ==
# Client ID from https://upgrade.yubico.com/getapikey/ # Optiona: Get your own from https://upgrade.yubico.com/getapikey/
yubico_otp_client_id = 1 yubico_otp_client_id = 1
# Note: You can keep client ID on 1, it will function.
yubico_otp_secret = "" yubico_otp_secret = ""
# Note: YOU CAN KEEP THIS ON 1, IT WILL STILL FUNCTION. # Optional: If you provide a secret, requests will be signed
# Note: Secret is not currently used, but it's recommended for you to add # and responses will be verified.
# if you use your own client ID so if I ever implement it, your bot won't break.