Setting up Digest authentication using Python @Lambda

Introduction

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.

code

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

Ready to move

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.

Make the library a zip file

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/

Creating lambda_handler

When you upload the zip file, lambda_function.py is gone, so create lambda_function.py again and write lambda_handler.

Other referenced sites

Recommended Posts

Setting up Digest authentication using Python @Lambda
Setting up Basic authentication using Python @Lambda
Summary if using AWS Lambda (Python)
Authentication using tweepy-User authentication and application authentication (Python)
[SAM] Try using RDS Proxy with Lambda (Python) [user/pass, IAM authentication]
[AWS] Using ini files with Lambda [Python]
[Python] Speeding up processing using cache tools
Install python library on Lambda using [/ tmp]
A memo when setting up a Docker container for using JUMAN ++, KNP, python
Beware of disable_existing_loggers when setting up Python logging
Achieve Basic Authentication with CloudFront Lambda @ Edge with Python 3.8
Start using Python
Python Https Authentication
Scraping using Python
How to set up a Python environment using pyenv
Pass the authentication proxy through communication using python urllib3
Setting up Jupyter Lab in a Python 3.9 venv environment
Operate Redmine using Python Redmine
Fibonacci sequence using Python
Data analysis using Python 0
A python lambda expression ...
Data cleaning using Python
Using Python #external packages
WiringPi-SPI communication using Python
Age calculation using python
Search Twitter using Python
Example of using lambda
Name identification using python
Notes using Python subprocesses
python Environmentally-friendly Japanese setting
Try using Tweepy [Python2.7]
Create API with Python, lambda, API Gateway quickly using AWS SAM