Single root key derives all 18 service credentials via HKDF-SHA256. Bootstrap/service tiers rotate on key change vs every commit. Provisioners update PostgreSQL roles and Gitea tokens automatically. CI pipeline runs `api.auth provision` after each deploy.
93 lines
2.8 KiB
Python
93 lines
2.8 KiB
Python
"""Tests for HKDF credential derivation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from api.auth.derive import derive, derive_hex, derive_password
|
|
|
|
ROOT = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
|
|
SALT = b"test-salt"
|
|
INFO = b"test-info"
|
|
|
|
|
|
class TestDerive:
|
|
def test_deterministic(self):
|
|
a = derive(ROOT, SALT, INFO)
|
|
b = derive(ROOT, SALT, INFO)
|
|
assert a == b
|
|
|
|
def test_output_length(self):
|
|
assert len(derive(ROOT, SALT, INFO, 16)) == 16
|
|
assert len(derive(ROOT, SALT, INFO, 32)) == 32
|
|
assert len(derive(ROOT, SALT, INFO, 64)) == 64
|
|
|
|
def test_different_salt_different_output(self):
|
|
a = derive(ROOT, b"salt-a", INFO)
|
|
b = derive(ROOT, b"salt-b", INFO)
|
|
assert a != b
|
|
|
|
def test_different_info_different_output(self):
|
|
a = derive(ROOT, SALT, b"info-a")
|
|
b = derive(ROOT, SALT, b"info-b")
|
|
assert a != b
|
|
|
|
def test_different_key_different_output(self):
|
|
key2 = bytes(32)
|
|
a = derive(ROOT, SALT, INFO)
|
|
b = derive(key2, SALT, INFO)
|
|
assert a != b
|
|
|
|
|
|
class TestDeriveHex:
|
|
def test_hex_format(self):
|
|
h = derive_hex(ROOT, SALT, INFO)
|
|
assert len(h) == 64
|
|
bytes.fromhex(h) # must not raise
|
|
|
|
def test_hex_deterministic(self):
|
|
assert derive_hex(ROOT, SALT, INFO) == derive_hex(ROOT, SALT, INFO)
|
|
|
|
def test_hex_matches_raw(self):
|
|
assert derive_hex(ROOT, SALT, INFO) == derive(ROOT, SALT, INFO).hex()
|
|
|
|
def test_hex_custom_length(self):
|
|
h = derive_hex(ROOT, SALT, INFO, length=16)
|
|
assert len(h) == 32
|
|
|
|
|
|
class TestDerivePassword:
|
|
def test_password_url_safe(self):
|
|
pw = derive_password(ROOT, SALT, INFO)
|
|
assert "+" not in pw
|
|
assert "/" not in pw
|
|
assert "=" not in pw
|
|
|
|
def test_password_deterministic(self):
|
|
a = derive_password(ROOT, SALT, INFO)
|
|
b = derive_password(ROOT, SALT, INFO)
|
|
assert a == b
|
|
|
|
def test_password_length(self):
|
|
pw = derive_password(ROOT, SALT, INFO, length=24)
|
|
assert len(pw) == 32 # 24 bytes -> 32 base64url chars
|
|
|
|
def test_password_different_from_hex(self):
|
|
pw = derive_password(ROOT, SALT, INFO)
|
|
h = derive_hex(ROOT, SALT, INFO)
|
|
assert pw != h
|
|
|
|
|
|
class TestRFCVector:
|
|
"""Verify against RFC 5869 Test Case 1."""
|
|
|
|
def test_rfc5869_case1(self):
|
|
ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b")
|
|
salt = bytes.fromhex("000102030405060708090a0b0c")
|
|
info = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9")
|
|
expected = bytes.fromhex(
|
|
"3cb25f25faacd57a90434f64d0362f2a"
|
|
"2d2d0a90cf1a5a4c5db02d56ecc4c5bf"
|
|
"34007208d5b887185865"
|
|
)
|
|
result = derive(ikm, salt, info, length=42)
|
|
assert result == expected
|