kafka_client 0.5.2

A pure Rust Kafka client library with SASL authentication support
Documentation
#!/usr/bin/env python3
"""
Cross-validate krb5-gss cryptography against python-cryptography and OpenSSL.

Usage:
    python3 scripts/verify_crypto.py

Dependencies:
    python-cryptography (install via pacman or pip)
    
This script independently computes all the known-answer test vectors
referenced by the Rust unit tests and prints pass/fail for each.
"""
import hashlib
import hmac
import os
import subprocess
from binascii import hexlify, unhexlify

try:
    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
    from cryptography.hazmat.primitives import hashes
except ImportError:
    print("ERROR: python-cryptography not installed.")
    print("Install: sudo pacman -S python-cryptography  or  pip install cryptography")
    exit(1)

PASS = 0
FAIL = 0

def check(name, expected, actual):
    global PASS, FAIL
    if expected == actual:
        PASS += 1
        print(f"{name}")
    else:
        FAIL += 1
        print(f"{name}")
        print(f"     expected: {expected.hex() if isinstance(expected, bytes) else expected}")
        print(f"     actual:   {actual.hex() if isinstance(actual, bytes) else actual}")

# ==================== 1. HMAC-SHA1 (RFC 2202) ====================
print("\n=== 1. HMAC-SHA1 (RFC 2202) ===")
# Test case 1
mac = hmac.new(bytes([0x0b]*20), b"Hi There", hashlib.sha1).digest()
check("RFC2202 case 1 (20-byte key)",
      unhexlify("b617318655057264e28bc0b6fb378c8ef146be00"), mac)

# Test case 2
mac = hmac.new(b"Jefe", b"what do ya want for nothing?", hashlib.sha1).digest()
check("RFC2202 case 2",
      unhexlify("effcdf6ae5eb2fa2d27416d5f184df9c259a7c79"), mac)

# Test case 4
mac = hmac.new(bytes([0xaa]*20), bytes([0xdd]*50), hashlib.sha1).digest()
check("RFC2202 case 4 (50-byte data)",
      unhexlify("125d7342b9ac11cd91a39af48aa17b4f63f175d3"), mac)

# ==================== 2. PBKDF2 (RFC 6070) ====================
print("\n=== 2. PBKDF2-HMAC-SHA1 (RFC 6070) ===")
kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=20, salt=b"salt", iterations=1)
dk = kdf.derive(b"password")
check("RFC6070 c=1",
      unhexlify("0c60c80f961f0e71f3a9b524af6012062fe037a6"), dk)

kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=20, salt=b"salt", iterations=2)
dk = kdf.derive(b"password")
check("RFC6070 c=2",
      unhexlify("ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957"), dk)

kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=20, salt=b"salt", iterations=4096)
dk = kdf.derive(b"password")
check("RFC6070 c=4096",
      unhexlify("4b007901b765489abead49d926f721d065a429c1"), dk)

# ==================== 3. string_to_key (RFC 3962 Appendix B) ====================
print("\n=== 3. string_to_key (RFC 3962 Appendix B, c=1200) ===")
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

def nfold(data, nbits):
    """RFC 3961 §6.1 n-fold. Ported from the Rust implementation."""
    nbytes = nbits // 8
    slen = len(data)
    if slen == 0:
        return bytes(nbytes)
    import math
    lcm_val = slen * nbytes // math.gcd(slen, nbytes)

    # Build bigstr: concat rotate_right(data, 13*i) for i=0,1,2,...
    def rotate_right(buf, nbits):
        """Rotate buf right by nbits."""
        nbytes = nbits // 8
        remain = nbits % 8
        result = bytearray(len(buf))
        for i in range(len(buf)):
            idx1 = (len(buf) + i - nbytes) % len(buf)
            idx2 = (len(buf) + i - nbytes - 1) % len(buf)
            if remain == 0:
                result[i] = buf[idx1]
            else:
                result[i] = (buf[idx1] >> remain) | ((buf[idx2] << (8 - remain)) & 0xFF)
        return bytes(result)

    bigstr = bytearray()
    for i in range(lcm_val // slen):
        bigstr.extend(rotate_right(data, 13 * i))

    # Process in nbyte-sized chunks with 1's complement addition
    def add_ones_complement(a, b):
        """Add two byte arrays using 1's complement arithmetic."""
        carry = 0
        result = bytearray(len(a))
        for i in range(len(a) - 1, -1, -1):
            s = a[i] + b[i] + carry
            result[i] = s & 0xFF
            carry = s >> 8
        while carry:
            carry = 0
            for i in range(len(a) - 1, -1, -1):
                s = result[i] + 1
                result[i] = s & 0xFF
                if s >> 8 == 0:
                    break
                carry = s >> 8
        return bytes(result)

    result = bytes(nbytes)
    first = True
    for p in range(0, lcm_val, nbytes):
        slice_data = bytes(bigstr[p:p + nbytes])
        if first:
            result = slice_data
            first = False
        else:
            result = add_ones_complement(result, slice_data)
    return result

def dk_aes(key, constant, out_len):
    """RFC 3961 DK using AES encryption (MIT krb5 compatible)."""
    block = nfold(constant, 128)
    result = bytearray()
    cipher = Cipher(algorithms.AES(key), modes.ECB())
    encryptor = cipher.encryptor()
    while len(result) < out_len:
        ct = encryptor.update(block)
        result.extend(ct[:min(16, out_len - len(result))])
        block = ct
    return bytes(result[:out_len])

def string_to_key(password, salt, iter_count, key_len):
    """RFC 3962 string_to_key."""
    kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=key_len, salt=salt, iterations=iter_count)
    tkey = kdf.derive(password)
    return dk_aes(tkey, b"kerberos", key_len)

# AES-128
k = string_to_key(b"password", b"ATHENA.MIT.EDUraeburn", 1200, 16)
check("s2k AES128 c=1200",
      unhexlify("4c01cd46d632d01e6dbe230a01ed642a"), k)

# AES-256
k = string_to_key(b"password", b"ATHENA.MIT.EDUraeburn", 1200, 32)
check("s2k AES256 c=1200",
      unhexlify("55a6ac740ad17b4846941051e1e8b0a7548d93b0ab30a8bc3ff16280382b8c2a"), k)

# ==================== 4. AES-CTS (RFC 3962 Appendix B) ====================
print("\n=== 4. AES-128-CTS (RFC 3962 Appendix B) ===")
key = b"chicken teriyaki"
iv = bytes(16)

def cts_encrypt(key, iv, plaintext):
    """RFC 3962 AES-CTS (CS3 variant) encryption."""
    bs = 16
    if len(plaintext) <= bs:
        # Single block: ECB
        cipher = Cipher(algorithms.AES(key), modes.ECB())
        e = cipher.encryptor()
        if len(plaintext) < bs:
            block = plaintext + bytes(bs - len(plaintext))
        else:
            block = plaintext
        ct = e.update(block) + e.finalize()
        return ct[:len(plaintext)]
    
    if len(plaintext) % bs == 0:
        # Exact block multiple: CBC + swap last 2 blocks
        cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
        e = cipher.encryptor()
        ct = e.update(plaintext) + e.finalize()
        # Swap last two blocks
        ct_list = bytearray(ct)
        last_two = len(ct) - 2*bs
        ct_list[last_two:last_two+bs], ct_list[last_two+bs:] = ct_list[last_two+bs:], ct_list[last_two:last_two+bs]
        return bytes(ct_list)
    else:
        # Standard CTS for non-multiples
        # (using CbcCs3 via python is complex; skip for now)
        print(f"  ⚠ CTS non-multiple ({len(plaintext)}B) not verified in Python")
        return None

# 17 bytes
ct = cts_encrypt(key, iv, b"I would like the ")
if ct:
    check("CTS 17 bytes", unhexlify("c6353568f2bf8cb4d8a580362da7ff7f97"), ct)

# 31 bytes  
ct = cts_encrypt(key, iv, b"I would like the General Gau's ")
if ct:
    check("CTS 31 bytes", unhexlify("fc00783e0efdb2c1d445d4c8eff7ed2297687268d6ecccc0c07b25e25ecfe5"), ct)

# 32 bytes
ct = cts_encrypt(key, iv, b"I would like the General Gau's C")
if ct:
    check("CTS 32 bytes", unhexlify("39312523a78662d5be7fcbcc98ebf5a897687268d6ecccc0c07b25e25ecfe584"), ct)

# 64 bytes
pt64 = b"I would like the General Gau's Chicken, please, and wonton soup."
ct = cts_encrypt(key, iv, pt64)
if ct:
    expected = unhexlify("97687268d6ecccc0c07b25e25ecfe58439312523a78662d5be7fcbcc98ebf5a84807efe836ee89a526730dbc2f7bc8409dad8bbb96c4cdc03bc103e1a194bbd8")
    check("CTS 64 bytes", expected, ct)

# ==================== Summary ====================
print(f"\n{'='*40}")
print(f"Results: {PASS} passed, {FAIL} failed, {PASS+FAIL} total")

# Add more test vectors here as needed