Comments 6
Пардон, в криптографии не разбираюсь. Но знаю принципы make illegal states impossible, parse don’t validate. Поэтому ответ сильно обработанный нейросетью
Важно разделить 2 случая:
В коде зашито - ловится compile time. Пришло снаружи - ловится на границе один раз, потом внутри уже живут только безопасные типы. Это и есть parse, don’t validate.
Что хорошо ложится на make illegal states impossible.
На входе из сети/конфига/БД можно парсить сразу в разрешённые типы:
Алгоритм подписи
снаружи: “rsa-pss” / “rsa-pkcs1v15”
внутри: AllowedSigAlg
pkcs1v15 просто не парсится
Режим шифрования
снаружи: “aes-256-gcm” / “aes-cbc”
внутри: AllowedAead cbc отбрасывается на входе
Ключи
не Vec, а PublicKey, AeadKey при парсинге проверяются формат, длина, алгоритм, иногда usage
CSR / сертификаты / JWT
RawCsr -> VerifiedCsr
RawJwt -> VerifiedJwt
дальше нельзя случайно использовать непроверенный объект
Nonce/IV длина
Nonce12 вместо просто &[u8], неверная длина отсекается сразу
Cipher suite / версия протокола
NegotiatedStrongSuite
слабые наборы не проходят парсинг/согласование
Что можно поймать только частично
Срок действия сертификата
можно проверить на входе относительно now но через 5 минут факт может стать ложным значит это не “вечный” инвариант типа без привязки ко времени
Уникальность nonce
длину поймать легко, уникальность можно частично зашить в stateful API, но после рестарта/в кластере это уже не только типы
Что так не ловится
Это уже не “форма данных”, а “факт о мире”:
качество RNG, side-channel, отозван ли сертификат прямо сейчас, компрометация ключа, баг в криптобиблиотеке, downgrade, если протокол плохо спроектирован, повтор nonce между машинами/после crash recovery
Практически Самый полезный паттерн такой:
rust
// снаружи
let sig_alg: AllowedSigAlg = parse_sig_alg(input.sig_alg)?;
let aead: AllowedAead = parse_aead(input.aead)?;
let csr: VerifiedCsr = parse_and_verify_csr(input.csr)?;
// внутри
issue(sig_alg, csr);
encrypt(aead, key, plaintext);
То есть:
сырой внешний ввод -> один раз парсим и проверяем внутри домена плохих состояний уже нет
Для твоих примеров: Да, оба отлично подходят:
PKCS1 v1.5 вместо RSA-PSS
AES-CBC вместо AEAD
Если они приходят снаружи, их не надо тащить дальше как строки/enum “всех возможных алгоритмов”. Надо сразу парсить в AllowedSigAlg / AllowedAead, где плохих вариантов просто нет.
Про пример кода на питон
тест "PSS != PKCS1v15" это тавтология про библиотеку, не про твой контракт.
Надо было проверить
Инвариант "мы подписываем PSS" проверяется наоборот:
взять подпись от sign_data
верифицировать её как PSS - должно пройти
верифицировать её как PKCS1v15 - должно упасть
Это пример где тестом пытаются починить то, что должно быть в типах. Если бы sign_data возвращала Signature<RsaPss>, а API верификации не принимал Pkcs1v15, такой тест был бы не нужен. Тест тут - костыль вместо make illegal states impossible
Конкретно для питон лучше что-то такое
# mycrypto.py - единственная точка входа в криптографию
from dataclasses import dataclass
from typing import NewType
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.exceptions import InvalidSignature
# --- доменные типы: снаружи сырых bytes нет ---
@dataclass(frozen=True)
class SigningKey:
_key: rsa.RSAPrivateKey
@dataclass(frozen=True)
class VerifyingKey:
_key: rsa.RSAPublicKey
Signature = NewType("Signature", bytes) # не перепутать с обычными bytes
# --- политика захардкожена: только RSA-PSS + SHA-256 ---
_PSS = padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
)
_HASH = hashes.SHA256()
def generate_keypair() -> tuple[SigningKey, VerifyingKey]:
sk = rsa.generate_private_key(public_exponent=65537, key_size=3072)
return SigningKey(sk), VerifyingKey(sk.public_key())
def sign(key: SigningKey, data: bytes) -> Signature:
return Signature(key._key.sign(data, _PSS, _HASH))
def verify(key: VerifyingKey, sig: Signature, data: bytes) -> None:
# бросает InvalidSignature если не PSS/не та подпись
key._key.verify(sig, data, _PSS, _HASH)
# --- тест политики (а не математики) ---
# test_mycrypto.py
import pytest
import mycrypto
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
def test_facade_signs_with_pss_and_rejects_pkcs1v15():
sk, vk = mycrypto.generate_keypair()
data = b"payload"
# 1. Наша подпись верифицируется как PSS - политика соблюдена
sig = mycrypto.sign(sk, data)
mycrypto.verify(vk, sig, data)
# 2. Подпись, сделанная PKCS1v15 напрямую, отвергается нашим verify
legacy_sig = sk._key.sign(data, padding.PKCS1v15(), hashes.SHA256())
with pytest.raises(InvalidSignature):
mycrypto.verify(vk, legacy_sig, data)- прикладной код знает только SigningKey, VerifyingKey, Signature, sign, verify
- выбора padding/hash снаружи нет вообще
- тест проверяет политику фасада: PSS принимается, PKCS1v15 отвергается даже когда подпись валидна математически
И про ваш тест
Про "отвергает PKCS1v15" ваш тест молчит - потому что в него никто не подсовывает настоящую PKCS1v15-подпись
Про типы согласен. Для padding и cipher suite “parse, don’t validate” работает. AllowedSigAlg вместо строки, плохих вариантов нет на входе. Ваш mycrypto.py ровно это делает, и для Python это хороший паттерн.
Но вы сами написали где типы заканчиваются: качество RNG, revocation, nonce после рестарта. Вот тут контракты и живут. DRBG.seeded == true, reseed_counter < max_requests, entropy прошла health check. Это не тип, это runtime-инвариант. Его надо зафиксировать явно, иначе он только в голове.
Про тест. Да, в статье упрощённый пример. В репо sign_data использует PKCS1v15 для RSA, можно глянуть crypto_engine.py. X.509 экосистема пока на PKCS1v15, PSS для сертификатов поддерживается, но на практике ломает совместимость. В статье контракт описывает как должно быть, в репо пока компромисс с экосистемой. Надо было написать это явно в статье. Поправлю в репо.
В целом “make illegal states impossible” и DbC не конкурируют. Первое про форму данных на входе, второе про поведение на стыках. Оба нужны.
UPD: поправил — sign_data теперь RSA-PSS, тест в статье обновлён. Спасибо за замечание про PKCS1v15-подпись в тесте
Это вы мне отвечаете?
Если не ответом а просто рядом написать, то уведомление не придёт. Я случайно прочитал, мог и не заметить
тут контракты и живут... runtime-инвариант
Да, такое не ловится в типах. Жаль. Тогда только тест, лучше параметрический.
Design by Contract в эпоху AI: как контракты Мейера защищают криптографию там, где тесты молчат