iyi

Standard library, 0.18.0

std/rsa

RSA signatures (RFC 8017), written in iyi: PKCS #1 v1.5 and PSS, checked over SHA-1, SHA-256, SHA-384 and SHA-512, and made over the last three.

import std/rsa to use it. Source: src/std/rsa.iyi at v0.18.0.

  import std/rsa::{RsaPublicKey, RsaPrivateKey, RsaHash, RsaError}
  key = RsaPublicKey.from_spki(spki_der).or_panic   # what X.509 carries
  key.verify_pkcs1v15(RsaHash::SHA256, message, signature)   # Bool
  key.verify_pss(RsaHash::SHA256, message, signature)        # salt as long as the hash
  key.verify_pss(RsaHash::SHA384, message, signature, salt_length: 0)
  signer = RsaPrivateKey.from_pem(File.read("key.pem")).or_panic
  signer.sign_pss(RsaHash::SHA256, message)       # Array(UInt8)
  signer.sign_pkcs1v15(RsaHash::SHA384, message)

A key is read from DER through std/asn1: a SubjectPublicKeyInfo naming rsaEncryption with NULL parameters (RFC 3279 section 2.3.1), or a bare RSAPublicKey. What is not a key is an RsaError: a modulus that is even, under 1024 bits or over 16384; an exponent that is even, under 3 or over 2^31 - 1, the bound Go's crypto/rsa holds a public exponent to.

verify_pkcs1v15 builds the one encoding a signature over the message can have - 0x00 0x01, the 0xFF padding, 0x00, the hash's DigestInfo and the digest - and compares the whole of it. It does not parse what the signature opens to: the forgeries of Bleichenbacher (2006) and of the BERserk family were parsers that took a DigestInfo with room left over.

verify_pss is EMSA-PSS-VERIFY (section 9.1.2), its mask MGF1 over the same hash. The salt is as long as the digest unless salt_length says otherwise: TLS 1.3 asks for exactly that (RFC 8446 section 4.2.3), and a certificate's RSASSA-PSS parameters name theirs.

A signature that is not one is false, whatever the reason: the wrong length, a value not below the modulus, padding that does not match. A caller acts on the answer alone, and a reason given for a failed check is what the padding-oracle attacks were made of.

A private key is PKCS #1's RSAPrivateKey (RSA PRIVATE KEY, two primes) or a PKCS #8 PrivateKeyInfo around one (PRIVATE KEY, rsaEncryption with NULL parameters), in PEM or DER, with its CRT parameters, and it is read only if they agree: p and q over 1 with p·q = n, e·d = 1 modulo lcm(p - 1, q - 1), dp and dq d's residues modulo p - 1 and q - 1, and qi q's inverse modulo p. A modulus under 2048 bits is refused for signing (NIST SP 800-131A has disallowed less since 2013), where 1024 is still checked; an RSASSA-PSS key (id-RSASSA-PSS, whose parameters restrict it) and an encrypted one are refused by name, as is a key of more than two primes. Those checks run once, reading the key, on std/big's variable-time arithmetic; what runs per signature does not.

Signing is RSASP1 by the CRT (section 5.2.1). The message is blinded - multiplied by r^e for an r drawn by HMAC from the private exponent and the encoded message - so its value, which a caller may choose, does not shape the time; the two halves exponentiate by dp and dq through std/big's pow_mod_secret, a fixed window that reads its whole table and masks its subtractions, so no branch and no memory address follows the private exponents' bits; and the signature is checked against the public key before it is given out, so a fault in one half of the CRT does not hand out the factor it reveals (Boneh, DeMillo and Lipton, 1997) - such a signature is a panic, not an answer. A PSS salt is as long as the digest, RFC 8446's rule, and is HMAC, keyed by a hash of the private exponent, over the message's digest: section 8.1 says its randomness is not critical to PSS's security, and this one is unpredictable to anyone without the key besides, so the module asks the platform for no entropy. Signing over SHA-1 is a misuse, refused with a panic.

Measured here in --release builds (PSS over SHA-256, the check and the blinding included; bench/std_rsa_exercise.sh prints it each run): a 2048-bit signature in 1.16 to 1.33 ms, a 4096-bit one in 6.5 to 6.9.

Pure computation: std/big's arithmetic, std/digest's hashes, std/crypto's HMAC, std/base64 for PEM.

RsaErrorstruct

What reading a key answers for bytes that are not an RSA key.

std/rsaiyi doc src/std/rsa.iyi
pub struct RsaError
  def initialize(message : String)
  def to_s : String
end

RsaHashenum

The hashes a signature is made over.

std/rsaiyi doc src/std/rsa.iyi
pub enum RsaHash : Int32
  SHA1 = 0
  SHA256 = 1
  SHA384 = 2
  SHA512 = 3
  def digest(data : Array(UInt8)) : Array(UInt8)
  # The DER of a DigestInfo up to the digest itself (RFC 8017 section 9.2,
  # note 1): the algorithm's OID with NULL parameters, then the OCTET
  # STRING's header.
  def digest_info_prefix : Array(UInt8)
  def size : Int32
end

RsaPrivateKeystruct

An RSA private key with its CRT parameters (RFC 8017 section 3.2, the first representation and the second together), which signs.

std/rsaiyi doc src/std/rsa.iyi
pub struct RsaPrivateKey
  # The modulus's length in bits, and in bytes: a signature's length.
  def bits : Int32
  # The one key in a PEM text: an `RSA PRIVATE KEY` block (PKCS #1) or a
  # `PRIVATE KEY` block (PKCS #8).
  def self.from_pem(text : String) : RsaPrivateKey | RsaError
  # An RSAPrivateKey (appendix A.1.2) of two primes: version 0 and the
  # eight numbers.
  def self.from_pkcs1(der : Array(UInt8)) : RsaPrivateKey | RsaError
  # A PKCS #8 PrivateKeyInfo or OneAsymmetricKey (RFC 5958) naming
  # rsaEncryption, its privateKey an RSAPrivateKey. Attributes are passed
  # over; a public key, which version 1 may carry, must be the key's own.
  def self.from_pkcs8(der : Array(UInt8)) : RsaPrivateKey | RsaError
  def initialize(public_key : RsaPublicKey, p : BigInt, q : BigInt, dp : BigInt, dq : BigInt, qi : BigInt, prf : Array(UInt8))
  # The key from its eight numbers, each big-endian - n, e, d, p, q, dp,
  # dq, qi - or why they are not one.
  def self.new(modulus : Array(UInt8), exponent : Array(UInt8), d : Array(UInt8), p : Array(UInt8), q : Array(UInt8), dp : Array(UInt8), dq : Array(UInt8), qi : Array(UInt8)) : RsaPrivateKey | RsaError
  def public_key : RsaPublicKey
  # RSASSA-PKCS1-v1_5 (section 8.2.1) of *message* under *hash*: 0x00
  # 0x01, the 0xFF padding, 0x00, the hash's DigestInfo and the digest.
  def sign_pkcs1v15(hash : RsaHash, message : Array(UInt8) | String) : Array(UInt8)
  # RSASSA-PSS (section 8.1.1) of *message* under *hash*, MGF1 over the
  # same hash: EMSA-PSS-ENCODE (section 9.1.1) with a salt as long as the
  # digest, drawn as the module's header says.
  def sign_pss(hash : RsaHash, message : Array(UInt8) | String) : Array(UInt8)
  def size : Int32
end

RsaPublicKeystruct

std/rsaiyi doc src/std/rsa.iyi
pub struct RsaPublicKey
  # The modulus's length in bits, and in bytes: a signature's length.
  def bits : Int32
  def exponent : BigInt
  # An RSAPublicKey (RFC 8017 appendix A.1.1): SEQUENCE { modulus, exponent }.
  def self.from_pkcs1(der : Array(UInt8)) : RsaPublicKey | RsaError
  # A SubjectPublicKeyInfo (RFC 5280 section 4.1.2.7) naming rsaEncryption.
  def self.from_spki(der : Array(UInt8)) : RsaPublicKey | RsaError
  def self.from_spki(value : DerValue) : RsaPublicKey | RsaError
  def initialize(modulus : BigInt, exponent : BigInt, bits : Int32)
  def modulus : BigInt
  # The key from a modulus and an exponent, each big-endian.
  def self.new(modulus : Array(UInt8), exponent : Array(UInt8)) : RsaPublicKey | RsaError
  def size : Int32
  # Whether *signature* is the PKCS #1 v1.5 signature (RSASSA-PKCS1-v1_5,
  # section 8.2.2) of *message* under *hash*.
  def verify_pkcs1v15(hash : RsaHash, message : Array(UInt8) | String, signature : Array(UInt8)) : Bool
  # Whether *signature* is an RSASSA-PSS signature (section 8.1.2) of
  # *message* under *hash*, MGF1 over the same hash, with a salt of
  # *salt_length* bytes (the digest's length when nil).
  def verify_pss(hash : RsaHash, message : Array(UInt8) | String, signature : Array(UInt8), salt_length : Int32 | ::Nil = nil) : Bool
end

Error for RsaErrorimpl

std/rsaiyi doc src/std/rsa.iyi
impl Error for RsaError
  def message : String
end