I searched for Digest authentication and found little information, so I wrote it as a sequel to Basic authentication settings using Python @ Lambda. I have hardly touched Digest authentication itself, and it also serves as a study of the mechanism.
import os
import ctypes
import json
import base64
import time
import hashlib
import copy
from Crypto.Cipher import AES
accounts = [
{
"user": "user1",
"pass": "pass1"
},
{
"user": "user2",
"pass": "pass2"
}
]
realm = "[email protected]"
qop = "auth"
#Unlike Basic authentication, you can set a timeout after authentication, so I put it in
timeout = 30 * (10 ** 9) # 30 seconds
#Preparing information for use with AES encryption
raw_key = "password1234567890"
raw_iv = "12345678"
key = hashlib.sha256(raw_key.encode()).digest()
iv = hashlib.md5(raw_iv.encode()).digest()
def lambda_handler(event, context):
request = event.get("Records")[0].get("cf").get("request")
if not check_authorization_header(request):
return {
'headers': {
'www-authenticate': [
{
'key': 'WWW-Authenticate',
'value': create_digest_header()
}
]
},
'status': 401,
'body': 'Unauthorized'
}
return request
def check_authorization_header(request: dict) -> bool:
headers = request.get("headers")
authorization_header = headers.get("authorization")
if not authorization_header:
return False
data = {
"method": request.get("method"),
"uri": request.get("uri")
}
header_value = authorization_header[0].get("value")
#Digest authentication data comes in the format of "Digest ~", so first delete unnecessary parts
header_value = header_value[len("Digest "):]
#Each value is separated by a comma, so split it
values = header_value.split(",")
data = {
"method": request.get("method"),
"uri": request.get("uri")
}
#Divide each value again to make it easier to handle
for v in values:
#The nonce is Base64 encoded, so it's simply`=`If you divide it with, it will be strange, so we are dealing with this
idx = v.find("=")
vv = [v[0:idx], v[idx+1:]]
#Since there is a half-width space before and after, delete it
vv[0] = vv[0].strip()
vv[1] = vv[1].strip()
#Some values are enclosed in double quotes, so delete them.
if vv[1].startswith("\""):
vv[1] = vv[1][1:]
if vv[1].endswith("\""):
vv[1] = vv[1][:len(vv[1])-1]
data[vv[0]] = vv[1]
for account in accounts:
if account.get("user") != data.get("username"):
continue
d = copy.deepcopy(data)
d["user"] = account.get("user")
d["pass"] = account.get("pass")
encoded_value = create_validation_data(d)
if d.get("response") == encoded_value:
if check_timeout(data.get("nonce")):
return True
return False
def check_timeout(nonce: str) -> bool:
aes = AES.new(key, AES.MODE_CBC, iv)
value = aes.decrypt(base64.b64decode(nonce.encode())).decode()
#With padding when encrypting with AES`_`Is added, so delete that amount
while value.endswith("_"):
value = value[:len(value)-1]
return int(value) + timeout > time.time_ns()
def create_validation_data(data: dict) -> str:
v1 = "{}:{}:{}".format(data.get("user"), realm, data.get("pass"))
vv1 = hashlib.md5(v1.encode()).hexdigest()
v2 = "{}:{}".format(data.get("method"), data.get("uri"))
vv2 = hashlib.md5(v2.encode()).hexdigest()
v3 = "{}:{}:{}:{}:{}:{}".format(vv1, data.get("nonce"), data.get("nc"), data.get("cnonce"), qop, vv2)
return hashlib.md5(v3.encode()).hexdigest()
def create_digest_header() -> str:
aes = AES.new(key, AES.MODE_CBC, iv)
timestamp = "{}".format(time.time_ns()).encode()
#Since the length must be a multiple of 16 when encrypting, it is packed with padding
while len(timestamp) % 16 != 0:
timestamp += "_".encode()
header = "Digest "
values = {
"realm": '"' + realm + '"',
"qop": '"auth,auth-int"',
"algorithm": 'MD5',
"nonce": '"' + base64.b64encode(aes.encrypt(timestamp)).decode() + '"'
}
idx = 0
for k, v in values.items():
if idx != 0:
header += ","
header += '{}={}'.format(k, v)
idx += 1
return header
Since the settings of Lambda and CloudFront are the same as for Basic authentication, there is no description.
However, the AES encryption library needs to be installed with pip
, so a little support is required.
Since you can't run pip
on Lambda, you need to zip up what you did pip install
on your local PC.
One thing to keep in mind at this time is that the OS of AWS Lambda is Amazon Linux. Note that even if you create a zip on your local Mac, it will not work, saying "Oh, I should just zip it."
You can create EC2 of Amazon Linux and create a zip file, but Docker is enough because you only create a zip file at most. So, I created it using Docker.
#Pull and launch the Amazon Linux 2 image
$ docker run -it amazonlinux:2 bash
#Install the required packages on the Docker image
$ yum install -y gcc python3 pip3 python3-devel.x86_64
#Install packages for use on Lambda
$ pip install pycrypto -t ./
#Zip file creation
$ zip -r pycrypto.zip Crypto/
When you upload the zip file, lambda_function.py
is gone, so create lambda_function.py
again and write lambda_handler
.
Recommended Posts