std/signature/rsa

std/signature/src/rsa.trb

RSA signature verification of RFC 8017: RSASSA-PKCS1-v1_5 (section 8.2) and RSASSA-PSS (section 8.1) with SHA-256, what JOSE calls RS256 and PS256 (RFC 7518 sections 3.3 and 3.5). Only the public operation - signature ** exponent mod modulus - is here, so nothing it computes is secret and it takes variable time; there is no private key and no signing.

The arithmetic is ordinary TorbScript over Int: a number is a list of 16-bit limbs, least significant first, so a product of two limbs plus the carries stays far below 2^63, and the modular multiplication is Montgomery's, which needs no division. The limbs are 16 bits and not the 30 of ./montgomery because an RSA modulus has thousands of bits and the modulus is only known at run time. Both back ends compute the same bits.

type RsaPublicKey

type RsaPublicKey with Show, Equals, Hash

A public RSA key: the modulus n and the public exponent e, kept as unsigned big-endian bytes without leading zeros (a JSON Web Key's n and e, RFC 7518 section 6.3.1, or a COSE key's). Made only by RsaPublicKey.fromComponents, which refuses what no verification should accept: a modulus under 2048 bits or even, an exponent below 3 or even.

Examples

// A 2048-bit modulus (all bits set but the lowest of the top byte) and the usual exponent 65537
const modulus: Bytes = Buffer.from List.filled(256, 255)
const key = RsaPublicKey.fromComponents modulus, [1, 0, 1]
print key.isOk()
print key.expect("a key").verifiesPkcs1Sha256("hello".bytes(), modulus)

Pitfalls

  • Verification takes variable time. That is right for a public key and a public signature; do not use this for anything secret.
  • Keys under 2048 bits are refused, however a protocol would have them: they are not safe, and RS256 of RFC 7518 section 3.3 requires 2048 bits at least.
  • A signature has to be exactly as long as the modulus: JOSE and X.509 both write it that way, and a shorter or longer one is not "the same number" here but a signature that does not verify.

fn fromComponents

static fn fromComponents(modulus: Bytes, exponent: Bytes): Result<Self, SignatureError>

The key of a modulus and an exponent, unsigned big-endian bytes, leading zeros allowed (a JSON Web Key's n and e after Base64url).

Errors

fn modulus

fn modulus(): Bytes

The modulus n: unsigned big-endian bytes without leading zeros.

fn exponent

fn exponent(): Bytes

The public exponent e: unsigned big-endian bytes without leading zeros.

fn bits

fn bits(): Int

The size of the modulus in bits: eight times the bytes of a signature, less the unused top bits of its first.

fn show

fn show(): String

One line: the size of the key and its exponent.

fn equals

fn equals(other: Self): Bool

Whether the two keys have the same modulus and the same exponent.

fn hash

fn hash(): Int

The hash of the modulus and the exponent, so that a key is a key of a Map or a member of a Set.

fn verifiesPkcs1Sha256

fn verifiesPkcs1Sha256(message: Bytes, signature: Bytes): Bool

Whether signature is this key's RSASSA-PKCS1-v1_5 signature with SHA-256 over message (RFC 8017 section 8.2.2): what JOSE calls RS256. The signature, as many bytes as the modulus, is raised to the exponent modulo n and the result compared whole with the one encoding of the hash (section 9.2) - 00 01 FF ... FF 00, the DigestInfo, the hash - which is what rules out the forgeries of a parser that tolerates other paddings or trailing bytes.

A signature of the wrong length or not below the modulus does not verify. It takes variable time - everything it reads is public.

Examples

const modulus: Bytes = Buffer.from List.filled(256, 255)
const key = RsaPublicKey.fromComponents(modulus, [1, 0, 1]).expect("a key")
print key.verifiesPkcs1Sha256("hello".bytes(), modulus)

Pitfalls

  • Only SHA-256: a signature over SHA-384 or SHA-512 (RS384, RS512) does not verify.

fn verifiesPssSha256

fn verifiesPssSha256(message: Bytes, signature: Bytes): Bool

Whether signature is this key's RSASSA-PSS signature with SHA-256, MGF1 with SHA-256 and a salt of 32 bytes over message (RFC 8017 sections 8.1.2 and 9.1.2): what JOSE calls PS256, which fixes the salt length (RFC 7518 section 3.5). The encoded message is taken apart whole: the trailer byte BC, the unused top bits, the zero padding and the 01 in front of the salt are each checked, and the hash is recomputed and compared.

A signature of the wrong length or not below the modulus does not verify. It takes variable time - everything it reads is public.

Examples

const modulus: Bytes = Buffer.from List.filled(256, 255)
const key = RsaPublicKey.fromComponents(modulus, [1, 0, 1]).expect("a key")
print key.verifiesPssSha256("hello".bytes(), modulus)

Pitfalls

  • Only a salt of 32 bytes: a signature made with another salt length (OpenSSL's default for -pss is the hash length, but "maximum" is common too) does not verify.