Files
stack/tests/api/test_derive.py
kert 2c0a69a4d8 add HKDF credential derivation and auto-rotation
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.
2026-02-28 22:45:14 -05:00

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