Pull to refresh

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" проверяется наоборот:

  1. взять подпись от sign_data

  2. верифицировать её как PSS - должно пройти

  3. верифицировать её как 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-инвариант

Да, такое не ловится в типах. Жаль. Тогда только тест, лучше параметрический.

Да, написал вам - больше комментаторов тут нету ) Зафиксировал правку отдельным комментарием, не сообразил про уведомление. Согласен, параметрический тест тут в точку

Sign up to leave a comment.

Articles