proton_rsa

    RSASSA-PKCS1-v1_5 signature verification over SHA-256.

    rsa
    pkcs1
    signature
    verification
    Download zip
    Version
    0.2.5
    License
    Apache-2.0
    Last updated
    10 days ago
    Downloads
    7K

    #moonbit-community/proton_rsa

    RSASSA-PKCS1-v1_5 signature verification over SHA-256, in pure MoonBit.

    This package exists to verify Proton update manifests and artifacts. It is the single point at which an application decides whether foreign code is allowed to run, so it is deliberately small enough to read in full.

    #Scope

    Verification only. There is no key generation and no signing: those happen once per release on a maintainer machine, where a mature tool such as OpenSSL is available and appropriate. Signing is also where RSA's genuinely dangerous operations live — a reused or biased nonce in other schemes, private key handling here — and none of that belongs in code that ships to users.

    Only SHA-256 is supported. A verifier that accepts several digest algorithms has to decide which ones remain acceptable over time, and that decision is easier to get wrong than to leave out.

    #Use

    let key = @rsa.PublicKey::parse("rsa-sha256:<modulus hex>:<exponent hex>")
    let digest = compute_sha256_of_the_artifact()
    if @rsa.verify_pkcs1_sha256(key, digest, signature) {
    // The signature is valid.
    }

    verify_pkcs1_sha256 takes a digest rather than the signed content, so a large artifact can be streamed through a hash instead of being held in memory.

    #Properties this package relies on

    Verification returns a boolean and never raises. A caller that could distinguish "invalid signature" from "failed to check the signature" would eventually treat one as the other. Every rejection — wrong length, a signature not below the modulus, malformed padding, the wrong digest — is false. MoonBit refuses to implicitly discard a non-unit result, so the answer cannot be ignored by accident.

    The encoded block is rebuilt and compared, never parsed. Verification constructs the block that a valid signature must decrypt to and compares it in full. It never walks the padding looking for the digest. This is what defeats Bleichenbacher's 2006 forgery, which works against verifiers that scan for the DigestInfo and ignore whatever follows it. forgery_wbtest.mbt signs exactly such a block with the test key and asserts that it is rejected.

    Signature length must equal the modulus length. A shorter signature is not zero-extended and a longer one is not trimmed, because accepting several encodings of one integer is where signature malleability begins.

    I2OSP is length-checked. BigInt::to_octets(length=) left-pads a short value but does not truncate a long one, so the returned length is the only reliable evidence that a value fits in k bytes. Verification checks it.

    Keys are validated when parsed. A modulus below 2048 bits is refused; so is a modulus written with a leading zero byte, because that would let two different texts denote the same key with different values of k. An exponent below 3, an even exponent, or one not smaller than the modulus is refused. e = 1 would make every signature equal to the encoded block.

    Comparison does not exit early. Everything compared is public, so timing leaks nothing here; the routine is written this way so that it does not become a vulnerability if it is ever reused on secret data.

    #Test vectors

    vectors_wbtest.mbt holds a key and signature produced by OpenSSL, which also verifies them. A disagreement is therefore a defect in this package.

    forgery_wbtest.mbt holds signatures crafted with that key's private exponent, each chosen so that it decrypts to a specific malformed block: padding cut short with trailing data, a missing leading zero, the wrong block type, a padding octet that is not 0xFF, a missing separator, a DigestInfo naming SHA-1, and a well formed block for a different digest. An ordinary wrong signature decrypts to noise and is rejected long before block structure matters, so these are the only inputs that exercise the padding check at all.

    #What this package does not defend against

    It verifies a signature against a key it is given. It has nothing to say about where that key came from, whether the key is still trusted, or whether the signed version is newer than the running one. Key custody, rotation, and rollback protection belong to the caller.

    KeyError

    pub(all) suberror KeyError {
    UnknownAlgorithm(tag~ : String)
    Malformed(detail~ : String)
    NonCanonicalModulus
    ModulusTooSmall(bits~ : Int, minimum~ : Int)
    InvalidExponent(detail~ : String)
    } derive(Eq,
    Debug
    )

    A trusted public key could not be decoded.

    Every variant describes a defect in configuration, not in a signature. Signature verification itself never raises: it answers with a boolean, so that no caller can mistake an error path for a successful check.

    KeyError::equal

    fn KeyError::equal(KeyError, KeyError) -> Bool

    KeyError::message

    fn KeyError::message(self : KeyError) -> String

    KeyError::not_equal

    fn KeyError::not_equal(x : KeyError, y : KeyError) -> Bool

    KeyError::output

    fn KeyError::output(self : KeyError, logger : &Logger) -> Unit

    KeyError::to_repr

    KeyError::to_string

    fn KeyError::to_string(self : KeyError) -> String

    PublicKey

    pub struct PublicKey {
    modulus :
    BigInt

    exponent :
    BigInt

    size : Int
    }

    An RSA public key that verifies RSASSA-PKCS1-v1_5 signatures over SHA-256.

    PublicKey::bits

    fn PublicKey::bits(self : PublicKey) -> Int

    Returns the modulus length in bits.

    PublicKey::parse

    fn PublicKey::parse(text : String) -> PublicKey raise KeyError

    Parses a trusted public key.

    The encoding is rsa-sha256:<modulus hex>:<exponent hex>, big-endian and unsigned. The algorithm tag is part of the stored key so that migrating to a different scheme later does not require a release that existing installations are unable to accept.

    PublicKey::size

    fn PublicKey::size(self : PublicKey) -> Int

    Returns the modulus length in bytes.

    minimum_modulus_bits

    let minimum_modulus_bits : Int

    The smallest modulus this package will accept.

    1024-bit RSA is considered broken for signatures and 2048 is the current floor in every serious guideline. Refusing a short key is a security control in its own right: a key that is too small verifies signatures perfectly well and offers no protection.

    sha256_digest_length

    let sha256_digest_length : Int

    Length of a SHA-256 digest in bytes.

    verify_pkcs1_sha256

    fn verify_pkcs1_sha256(key : PublicKey, digest : Bytes, signature : Bytes) -> Bool

    Verifies an RSASSA-PKCS1-v1_5 signature over a SHA-256 digest.

    digest is the 32-byte SHA-256 of the signed content; the caller hashes, which lets a large artifact be streamed rather than held in memory. signature is the raw big-endian signature, exactly key.size() bytes.

    Returns true only for a signature that verifies. Every other outcome — wrong length, out-of-range signature, malformed padding, wrong digest — is false. Verification deliberately cannot raise: a caller that distinguished "invalid" from "failed to check" would eventually treat one as the other, and this function decides whether foreign code is allowed to run.