From e1d32cb19fc213cdb09ac7cb685acb6a34cc0f81 Mon Sep 17 00:00:00 2001 From: Yawning Angel Date: Thu, 9 Jul 2026 03:43:33 +0900 Subject: [PATCH] core/cryto/rsa: Initial import --- core/crypto/rsa/doc.odin | 7 + core/crypto/rsa/rsa.odin | 444 ++++++++++++++++ core/crypto/rsa/rsa_dec_oaep.odin | 197 +++++++ core/crypto/rsa/rsa_dec_tls_pms.odin | 55 ++ core/crypto/rsa/rsa_enc_oaep.odin | 105 ++++ core/crypto/rsa/rsa_int.odin | 110 ++++ core/crypto/rsa/rsa_keygen.odin | 367 +++++++++++++ core/crypto/rsa/rsa_mgf1.odin | 49 ++ core/crypto/rsa/rsa_modpow_priv.odin | 165 ++++++ core/crypto/rsa/rsa_modpow_pub.odin | 89 ++++ core/crypto/rsa/rsa_sig_pkcs1.odin | 233 +++++++++ core/crypto/rsa/rsa_sig_pss.odin | 293 +++++++++++ core/crypto/rsa/rsa_test_key.odin | 180 +++++++ examples/all/all_js.odin | 1 + examples/all/all_main.odin | 1 + tests/benchmark/crypto/benchmark_rsa.odin | 160 ++++++ tests/core/crypto/common/common.odin | 11 + tests/core/crypto/test_core_crypto_rsa.odin | 419 +++++++++++++++ tests/core/crypto/wycheproof/main.odin | 39 ++ tests/core/crypto/wycheproof/rsa.odin | 541 ++++++++++++++++++++ tests/core/crypto/wycheproof/schemas.odin | 101 +++- tests/core/speed.odin | 1 + 22 files changed, 3564 insertions(+), 4 deletions(-) create mode 100644 core/crypto/rsa/doc.odin create mode 100644 core/crypto/rsa/rsa.odin create mode 100644 core/crypto/rsa/rsa_dec_oaep.odin create mode 100644 core/crypto/rsa/rsa_dec_tls_pms.odin create mode 100644 core/crypto/rsa/rsa_enc_oaep.odin create mode 100644 core/crypto/rsa/rsa_int.odin create mode 100644 core/crypto/rsa/rsa_keygen.odin create mode 100644 core/crypto/rsa/rsa_mgf1.odin create mode 100644 core/crypto/rsa/rsa_modpow_priv.odin create mode 100644 core/crypto/rsa/rsa_modpow_pub.odin create mode 100644 core/crypto/rsa/rsa_sig_pkcs1.odin create mode 100644 core/crypto/rsa/rsa_sig_pss.odin create mode 100644 core/crypto/rsa/rsa_test_key.odin create mode 100644 tests/benchmark/crypto/benchmark_rsa.odin create mode 100644 tests/core/crypto/test_core_crypto_rsa.odin create mode 100644 tests/core/crypto/wycheproof/rsa.odin diff --git a/core/crypto/rsa/doc.odin b/core/crypto/rsa/doc.odin new file mode 100644 index 000000000..537333cfc --- /dev/null +++ b/core/crypto/rsa/doc.odin @@ -0,0 +1,7 @@ +/* +RSA (Rivest–Shamir–Adleman) cryptosystem. + +See: +- [[ https://www.rfc-editor.org/info/rfc8017/ ]] +*/ +package rsa diff --git a/core/crypto/rsa/rsa.odin b/core/crypto/rsa/rsa.odin new file mode 100644 index 000000000..29ef4dac8 --- /dev/null +++ b/core/crypto/rsa/rsa.odin @@ -0,0 +1,444 @@ +package rsa + +// Copyright (c) 2016 Thomas Pornin +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS “AS IS” AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import "core:bytes" +import "core:crypto" +import subtle "core:crypto/_subtle" +import "core:encoding/endian" + +// Minimum size for a RSA modulus (in bits). +// +// Note: 1024-bits is arguably insufficient as of this writing, with +// 2048-bits being a more sensible value, however 1024-bits is likely +// still in frequent enough use. +// +// Note: CA signed TLS certificates have a strict requirement of a modulus +// size that is at least 2048-bits [[ https://cabforum.org/working-groups/server/baseline-requirements/documents/]]. +MODULUS_MIN_SIZE :: 1024 + +// Maximum size for a RSA modulus (in bits). +// +// This value MUST be a multiple of 64. This value MUST NOT exceed 47666 +// (some computations in RSA key generation rely on the factor size being +// no more than 23833 bits). RSA key sizes beyond 3072 bits don't make a +// lot of sense anyway. +MODULUS_MAX_SIZE :: 4096 + +// Maxmimum size for a RSA public exponent (in bits). +// +// Note: This implementation supports arbitrary size exponents, however +// limit it to something sensible (some implementations are known to +// choke on exponents >= 2^32), with the most common choice being +// `65537`. +EXPONENT_MAX_SIZE :: 32 + +// Maximum size for a RSA factor (in bits). This is for RSA private-key +// operations. Default is to support factors up to a bit more than half +// the maximum modulus size. +// +// This value MUST be a multiple of 32. +FACTOR_MAX_SIZE :: (MODULUS_MAX_SIZE + 64) >> 1 + +// Default size for a RSA key (in bits). +DEFAULT_MODULUS_SIZE :: 2048 + +// RSA public exponent used for key generation. This MUST be a prime +// number greater than 2. +@(private) +PUBLIC_EXPONENT :: 65537 + +#assert(EXPONENT_MAX_SIZE <= 32) + +// Private_Key is a RSA private key. +Private_Key :: struct { + _pub_key: Public_Key, + _d: Modulus, // Private exponent has the same size as n. + _p: Factor, + _q: Factor, + + // CRT coefficients. + _dp: Factor, // d % (p - 1) + _dq: Factor, // d % (q - 1) + _iq: Factor, // q^(-1) mod p + + _is_initialized: bool, +} + +// Public_Key is a RSA public key. +Public_Key :: struct { + _n: Modulus, + _e: u32, + _is_initialized: bool, +} + +// private_key_generate uses the system entropy source to generate a new +// Private_Key. The key size is specified in bits, and must be a multiple +// of 8. +@(require_results) +private_key_generate :: proc(priv_key: ^Private_Key, key_size := DEFAULT_MODULUS_SIZE) -> bool { + if !crypto.HAS_RAND_BYTES { + return false + } + if key_size < MODULUS_MIN_SIZE || key_size > MODULUS_MAX_SIZE { + return false + } + if key_size % 8 != 0 { + return false + } + + private_key_clear(priv_key) + defer if !priv_key._is_initialized { + private_key_clear(priv_key) + } + + for { + // The only way this can fail is if we get extremely unlucky + // and we fail to derive `iq` (1/d mod p). + if keygen_inner(priv_key, key_size) == 1 { + break + } + } + priv_key._is_initialized = true + priv_key._pub_key._is_initialized = true + + // Self-test the key. + priv_key._is_initialized = pkcs1_sig_selftest(priv_key) + + return priv_key._is_initialized +} + +// private_key_n copies the private key's public modulus to dst if dst is +// non-nil and of sufficient size, and returns the number of bytes +// copied/would be copied (ie: calling with `dst = nil` gets the required +// size). +@(require_results) +private_key_n :: proc(priv_key: ^Private_Key, dst: []byte) -> (n_len: int) { + ensure(priv_key._is_initialized, "crypto/rsa: uninitialized private key") + + return public_key_n(&priv_key._pub_key, dst) +} + +// private_key_e returns the private key's public exponent as a u32. +@(require_results) +private_key_e :: proc(priv_key: ^Private_Key) -> u32 { + ensure(priv_key._is_initialized, "crypto/rsa: uninitialized private key") + + return public_key_e(&priv_key._pub_key) +} + +// private_key_d copies the private key's private exponent `d` to dst if +// dst is non-nil and of sufficient size, and returns the number of bytes +// copied/would be copied (ie: calling with `dst = nil` gets the required +// size). +// +// Note: The data returned MUST be kept confidential. +@(require_results) +private_key_d :: proc(priv_key: ^Private_Key, dst: []byte) -> (n_len: int) { + ensure(priv_key._is_initialized, "crypto/rsa: uninitialized private key") + + return modulus_copyout(&priv_key._d, dst) +} + +// private_key_p copies the private key's first prime factor `p` to dst +// if dst is non-nil and of sufficient size, and returns the number of +// bytes copied/would be copied (ie: calling with `dst = nil` gets the +// required size). +// +// Note: The data returned MUST be kept confidential. +@(require_results) +private_key_p :: proc(priv_key: ^Private_Key, dst: []byte) -> (n_len: int) { + ensure(priv_key._is_initialized, "crypto/rsa: uninitialized private key") + + return factor_copyout(&priv_key._p, dst) +} + +// private_key_q copies the private key's second prime factor `q` to dst +// if dst is non-nil and of sufficient size, and returns the number of +// bytes copied/would be copied (ie: calling with `dst = nil` gets the +// required size). +// +// Note: The data returned MUST be kept confidential. +@(require_results) +private_key_q :: proc(priv_key: ^Private_Key, dst: []byte) -> (n_len: int) { + ensure(priv_key._is_initialized, "crypto/rsa: uninitialized private key") + + return factor_copyout(&priv_key._q, dst) +} + +// private_key_dp copies the private key's first reduced exponent +// `d % (p-1)` to dst if dst is non-nil and of sufficient size, and +// returns the number of bytes copied/would be copied (ie: calling with +//`dst = nil` gets the required size). +// +// Note: The data returned MUST be kept confidential. +@(require_results) +private_key_dp :: proc(priv_key: ^Private_Key, dst: []byte) -> (n_len: int) { + ensure(priv_key._is_initialized, "crypto/rsa: uninitialized private key") + + return factor_copyout(&priv_key._dp, dst) +} + +// private_key_dq copies the private key's second reduced exponent +// `d % (q-1)` to dst if dst is non-nil and of sufficient size, and +// returns the number of bytes copied/would be copied (ie: calling with +//`dst = nil` gets the required size). +// +// Note: The data returned MUST be kept confidential. +@(require_results) +private_key_dq :: proc(priv_key: ^Private_Key, dst: []byte) -> (n_len: int) { + ensure(priv_key._is_initialized, "crypto/rsa: uninitialized private key") + + return factor_copyout(&priv_key._dq, dst) +} + +// private_key_iq copies the private key's CRT coefficient `iq` to dst if +// dst is non-nil and of sufficient size, and returns the number of bytes +// copied/would be copied (ie: calling with`dst = nil` gets the required +// size). +// +// Note: The data returned MUST be kept confidential. +@(require_results) +private_key_iq :: proc(priv_key: ^Private_Key, dst: []byte) -> (n_len: int) { + ensure(priv_key._is_initialized, "crypto/rsa: uninitialized private key") + + return factor_copyout(&priv_key._iq, dst) +} + +// private_key_size returns the size of the private key's public modulus +// in bytes. All ciphertexts and signatures will also be this size. +@(require_results) +private_key_size :: proc(priv_key: ^Private_Key) -> int { + ensure(priv_key._is_initialized, "crypto/rsa: uninitialized private key") + + return priv_key._pub_key._n.v_len +} + +// private_key_set_bytes sets a private key from byte-encoded components, +// and returns true if and only if (⟺) the operation was successful. +// +// Note: All values are mandatory, and match the values included in the +// PKCS private key format. +// +// WARNING: This routine validates that it is possible to sign/verify with +// the deserialized values, however d is not checked at all, nor is the +// primality of p and q. +@(require_results) +private_key_set_bytes :: proc( + priv_key: ^Private_Key, + n: []byte, + e: []byte, + d: []byte, + p: []byte, + q: []byte, + dp: []byte, + dq: []byte, + iq: []byte, +) -> bool { + private_key_clear(priv_key) + defer if !priv_key._is_initialized { + private_key_clear(priv_key) + } + + if !public_key_set_bytes(&priv_key._pub_key, n, e) { + return false + } + + if !modulus_set_bytes(&priv_key._d, d) { + return false + } + if !factor_set_bytes(&priv_key._p, p) { + return false + } + if !factor_set_bytes(&priv_key._q, q) { + return false + } + if !factor_set_bytes(&priv_key._dp, dp) { + return false + } + if !factor_set_bytes(&priv_key._dq, dq) { + return false + } + if !factor_set_bytes(&priv_key._iq, iq) { + return false + } + + priv_key._is_initialized = true + + // Test the key. + // + // Note: This DOES NOT check that p/q are prime and if d is + // consistent (as it is not used by our implementation). + priv_key._is_initialized = pkcs1_sig_selftest(priv_key) + + return priv_key._is_initialized +} + +// private_key_set sets priv_key to src. +private_key_set :: proc(priv_key, src: ^Private_Key) { + if src == nil || !src._is_initialized { + private_key_clear(priv_key) + return + } + + public_key_set(&priv_key._pub_key, &src._pub_key) + modulus_set(&priv_key._d, &src._d) + factor_set(&priv_key._p, &src._p) + factor_set(&priv_key._q, &src._q) + factor_set(&priv_key._dp, &src._dp) + factor_set(&priv_key._dq, &src._dq) + factor_set(&priv_key._iq, &src._iq) + + priv_key._is_initialized = true +} + +// private_key_equal returns true if and only if (⟺) priv_key is equal to other. +@(require_results) +private_key_equal :: proc(priv_key, other: ^Private_Key) -> bool { + ensure(priv_key._is_initialized && other._is_initialized, "crypto/rsa: uninitialized private key") + + pk_eq := public_key_equal(&priv_key._pub_key, &other._pub_key) + + eq := crypto.compare_constant_time(modulus_bytes(&priv_key._d), modulus_bytes(&other._d)) + eq &= crypto.compare_constant_time(factor_bytes(&priv_key._p), factor_bytes(&other._p)) + eq &= crypto.compare_constant_time(factor_bytes(&priv_key._q), factor_bytes(&other._q)) + eq &= crypto.compare_constant_time(factor_bytes(&priv_key._dp), factor_bytes(&other._dp)) + eq &= crypto.compare_constant_time(factor_bytes(&priv_key._dq), factor_bytes(&other._dq)) + eq &= crypto.compare_constant_time(factor_bytes(&priv_key._iq), factor_bytes(&other._iq)) + + return pk_eq & (eq == 1) +} + +// private_key_clear clears priv_key to the uninitialized state. +private_key_clear :: proc "contextless" (priv_key: ^Private_Key) { + crypto.zero_explicit(priv_key, size_of(Private_Key)) +} + +// public_key_n copies the public key's modulus `n` to dst if dst is +// non-nil and of sufficient size, and returns the number of bytes +// copied/would be copied (ie: calling with `dst = nil` gets the +// required size). +@(require_results) +public_key_n :: proc(pub_key: ^Public_Key, dst: []byte) -> (n_len: int) { + ensure(pub_key._is_initialized, "crypto/rsa: uninitialized public key") + + return modulus_copyout(&pub_key._n, dst) +} + +// public_key_e returns the public key's exponent `e` as a u32. +@(require_results) +public_key_e :: proc(pub_key: ^Public_Key) -> u32 { + ensure(pub_key._is_initialized, "crypto/rsa: uninitialized public key") + + return pub_key._e +} + +// public_key_size returns the size of the public key's modulus in bytes. +// All ciphertexts and signatures will also be this size. +@(require_results) +public_key_size :: proc(pub_key: ^Public_Key) -> int { + ensure(pub_key._is_initialized, "crypto/rsa: uninitialized public key") + + return pub_key._n.v_len +} + +// public_key_set_bytes sets a public key from byte-encoded components, +// and returns true if and only if (⟺) the operation was successful. +@(require_results) +public_key_set_bytes :: proc(pub_key: ^Public_Key, n, e: []byte) -> bool { + public_key_clear(pub_key) + defer if !pub_key._is_initialized { + public_key_clear(pub_key) + } + + ok := modulus_set_bytes(&pub_key._n, n) + if !ok { + return false + } + if modulus_len(&pub_key._n) < MODULUS_MIN_SIZE >> 3 { + return false + } + if !modulus_is_odd(&pub_key._n) { + return false + } + + e_ := bytes.trim_left(e, []byte{0x00}) + e_len := len(e_) + if e_len > EXPONENT_MAX_SIZE >> 3 { + return false + } + e_buf: [4]byte + copy(e_buf[4 - e_len:], e) + e_u32 := endian.unchecked_get_u32be(e_buf[:]) + if e_u32 < 3 || e_u32 & 1 == 0 { + return false + } + pub_key._e = e_u32 + + pub_key._is_initialized = true + + return true +} + +// public_key_set sets pub_key to src. +public_key_set :: proc(pub_key, src: ^Public_Key) { + if src == nil || !src._is_initialized { + public_key_clear(pub_key) + return + } + + modulus_set(&pub_key._n, &src._n) + pub_key._e = src._e + pub_key._is_initialized = true +} + +// public_key_set_priv sets pub_key to the public component of priv_key. +public_key_set_priv :: proc(pub_key: ^Public_Key, priv_key: ^Private_Key) { + ensure(priv_key._is_initialized, "crypto/rsa: uninitialized private key") + pub_key^ = priv_key._pub_key +} + +// public_key_equal returns true if and only if (⟺) pub_key is equal to other. +public_key_equal :: proc(pub_key, other: ^Public_Key) -> bool { + ensure(pub_key._is_initialized && other._is_initialized, "crypto/rsa: uninitialized public key") + + eq := crypto.compare_constant_time(modulus_bytes(&pub_key._n), modulus_bytes(&other._n)) + eq &= int(subtle.eq(pub_key._e, other._e)) + + return eq == 1 +} + +// public_key_clear clears pub_key to the uninitialized state. +public_key_clear :: proc "contextless" (pub_key: ^Public_Key) { + crypto.zero_explicit(pub_key, size_of(Public_Key)) +} + +// size returns the size of the key's public modulus in bytes. +// All ciphertexts and signatures will also be this size. +size :: proc "contextless" (key: ^$T) -> int where T == Private_Key || T == Private_Key { + when T == Private_Key { + return private_key_size(key) + } else { + return public_key_size(key) + } +} diff --git a/core/crypto/rsa/rsa_dec_oaep.odin b/core/crypto/rsa/rsa_dec_oaep.odin new file mode 100644 index 000000000..37f48fea9 --- /dev/null +++ b/core/crypto/rsa/rsa_dec_oaep.odin @@ -0,0 +1,197 @@ +package rsa + +// Copyright (c) 2018 Thomas Pornin +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS “AS IS” AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import "core:crypto" +import subtle "core:crypto/_subtle" +import "core:crypto/hash" + +// decrypt_oaep returns the plaintext and true if and only if (⟺) it +// successfully decrypts the ciphertext with OAEP parameterized by +// label, hash_algo, and mgf1_algo, and writes the plaintext into dst. +// If mgf1_algo is unspecified, hash_algo will be used. +// +// Note: dst MUST be large enough to contain the plaintext. +@(require_results) +decrypt_oaep :: proc( + priv_key: ^Private_Key, + hash_algo: hash.Algorithm, + ciphertext: []byte, + dst: []byte, + label: []byte = nil, + mgf1_algo := hash.Algorithm.Invalid, +) -> (plaintext: []byte, ok: bool) { + if !priv_key._is_initialized { + return + } + ct_len := len(ciphertext) + if ct_len != modulus_len(&priv_key._pub_key._n) { + return + } + if hash_algo == .Invalid { + return + } + mgf1_algo_ := mgf1_algo + if mgf1_algo == .Invalid { + mgf1_algo_ = hash_algo + } + + tmp: [MODULUS_MAX_SIZE >> 3]byte + pt_buf := tmp[:ct_len] + defer crypto.zero_explicit(raw_data(pt_buf), ct_len) + + copy(pt_buf, ciphertext) + r := private_modpow(pt_buf, priv_key) + r_, l := oaep_dec_unpad(hash_algo, mgf1_algo_, label, pt_buf) + + // Conditional branches are ok as we are past the padding + // verification. + if ok = r & r_ == 1; ok { + if l <= len(dst) { + copy(dst, pt_buf[:l]) + plaintext = dst[:l] + } else { + ok = false + } + } + + return +} + +// oaep_max_plaintext_size returns the maximum supported plaintext size +// for a given key, with OAEP parameterized by hash_algo and mgf1_algo. +// If mgf1_algo is unspecified, hash_algo will be used. +@(require_results) +oaep_max_plaintext_size :: proc( + k: ^$T, + hash_algo: hash.Algorithm, + mgf1_algo := hash.Algorithm.Invalid, +) -> int where T == Private_Key || T == Public_Key { + if !k._is_initialized { + return 0 + } + if hash_algo == .Invalid { + return 0 + } + mgf1_algo_ := mgf1_algo + if mgf1_algo == .Invalid { + mgf1_algo_ = hash_algo + } + + overhead := 2 + hash.DIGEST_SIZES[hash_algo] + hash.DIGEST_SIZES[mgf1_algo_] + + pub_key: ^Public_Key + when T == Private_Key { + pub_keyk = &k._pub_key + } else { + pub_key = k + } + return modulus_len(&k._n) - overhead +} + +@(private="file") +xor_hash_data :: proc(hash_algo: hash.Algorithm, dst: []byte, src: []byte) { + tmp: [hash.MAX_DIGEST_SIZE]byte = --- + hash_len := hash.DIGEST_SIZES[hash_algo] + digest := tmp[:hash_len] + defer crypto.zero_explicit(raw_data(digest), hash_len) + + hash.hash_bytes_to_buffer(hash_algo, src, digest) + for v, u in digest { + dst[u] ~= v + } +} + +@(private="file") +oaep_dec_unpad :: proc( + hash_algo: hash.Algorithm, + mgf1_algo: hash.Algorithm, + label: []byte, + data: []byte, +) -> (u32, int) { + hash_len := hash.DIGEST_SIZES[hash_algo] + k := len(data) + buf := data + + // There must be room for the padding. + if k < (hash_len << 1) + 2 { + return 0, 0 + } + + // Unmask the seed, then the DB value. + seed, db := buf[1:1+hash_len], buf[1+hash_len:] + mgf1_xor(seed, mgf1_algo, db) + mgf1_xor(db, mgf1_algo, seed) + + // Hash the label and XOR it with the value in the array; if + // they are equal then these should yield only zeros. + xor_hash_data(hash_algo, db, label) + + // At that point, if the padding was correct, when we should + // have: 0x00 || seed || 0x00 ... 0x00 0x01 || M + // Padding is valid as long as: + // - There is at least hlen+1 leading bytes of value 0x00. + // - There is at least one non-zero byte. + // - The first (leftmost) non-zero byte has value 0x01. + // + // Ultimately, we may leak the resulting message length, i.e. + // the position of the byte of value 0x01, but we must take care + // to do so only if the number of zero bytes has been verified + // to be at least hlen+1. + // + // The loop below counts the number of bytes of value 0x00, and + // checks that the next byte has value 0x01, in constant-time. + // + // - If the initial byte (before the seed) is not 0x00, then + // r and s are set to 0, and stay there. + // - Value r is 1 until the first non-zero byte is reached + // (after the seed); it switches to 0 at that point. + // - Value s is set to 1 if and only if the data encountered + // at the time of the transition of r from 1 to 0 has value + // exactly 0x01. + // - Value zlen counts the number of leading bytes of value zero + // (after the seed). + r := u32(subtle.eq(buf[0], 0)) + s, zlen: u32 + for u in hash_len + 1..> 8) + s |= nz & subtle.eq(w, 0x01) + r &= subtle.not(nz) + zlen += r + } + + // Padding is correct only if s == 1, _and_ zlen >= hlen. + s &= subtle.ge(zlen, u32(hash_len)) + + // At that point, padding was verified, and we are now allowed + // to make conditional jumps. + if s != 0 { + plen := 2 + hash_len + int(zlen) + k -= plen + copy(buf[:k], buf[plen:]) + } + return s, k +} diff --git a/core/crypto/rsa/rsa_dec_tls_pms.odin b/core/crypto/rsa/rsa_dec_tls_pms.odin new file mode 100644 index 000000000..cedd9032d --- /dev/null +++ b/core/crypto/rsa/rsa_dec_tls_pms.odin @@ -0,0 +1,55 @@ +package rsa + +// Copyright (c) 2016 Thomas Pornin +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS “AS IS” AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import subtle "core:crypto/_subtle" + +// unsafe_decrypt_tls_pms decrypts a TLS RSA-Encrypted Premaster Secret +// Message, unconditionally moves the decrypted plaintext to `data[:48]`, +// and returns 1 if and only if (⟺) the operation was successful. +// +// WARNING: This routine MUST only be used when implementing server-side +// support for TLS 1.2's Client Key Exchange message, and extreme care +// MUST be taken when handling failures. This key exchange scheme was +// removed in TLS 1.3, and not implementing support in the first place +// is strongly RECOMMENDED even for TLS 1.2 servers. +@(require_results) +unsafe_decrypt_tls_pms :: proc(priv_key: ^Private_Key, data: []byte) -> u32 { + // A first check on length. Since this test works only on the + // buffer length, it needs not (and cannot) be constant-time. + _len := len(data) + if _len < 59 || _len != priv_key._pub_key._n.v_len { + return 0 + } + x := private_modpow(data, priv_key) + + x &= u32(subtle.eq(data[0], 0x00)) + x &= u32(subtle.eq(data[1], 0x02)) + for u in 2..<(_len-49) { + x &= u32(subtle.neq(data[u], 0)) + } + x &= u32(subtle.eq(data[_len - 49], 0x00)) + copy(data[:48], data[_len - 48:]) + + return x +} diff --git a/core/crypto/rsa/rsa_enc_oaep.odin b/core/crypto/rsa/rsa_enc_oaep.odin new file mode 100644 index 000000000..7013b823b --- /dev/null +++ b/core/crypto/rsa/rsa_enc_oaep.odin @@ -0,0 +1,105 @@ +package rsa + +// Copyright (c) 2018 Thomas Pornin +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS “AS IS” AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import "base:intrinsics" +import "core:crypto" +import "core:crypto/hash" + +// encrypt_oaep returns true if and only if (⟺) it successfully +// encrypts the plaintext with OAEP parameterized by label, hash_algo, +// and mgf1_algo, and writes the cipherttext into dst. If mgf1_algo is +// unspecified, hash_algo will be used. +// +// This routine will fail if the system entropy source is unavailable. +encrypt_oaep :: proc( + pub_key: ^Public_Key, + hash_algo: hash.Algorithm, + plaintext: []byte, + dst: []byte, + label: []byte = nil, + mgf1_algo := hash.Algorithm.Invalid, +) -> bool { + if !pub_key._is_initialized { + return false + } + if hash_algo == .Invalid { + return false + } + mgf1_algo_ := mgf1_algo + if mgf1_algo == .Invalid { + mgf1_algo_ = hash_algo + } + if len(dst) != modulus_len(&pub_key._n) { + return false + } + if len(plaintext) > oaep_max_plaintext_size(pub_key, hash_algo, mgf1_algo_) { + return false + } + + if oaep_enc_pad(hash_algo, mgf1_algo_, label, dst, plaintext) != 1 { + return false + } + + return public_modpow(dst, pub_key) == 1 +} + +@(private="file") +oaep_enc_pad :: proc( + hash_algo: hash.Algorithm, + mgf1_algo: hash.Algorithm, + label: []byte, + dst: []byte, + src: []byte, +) -> u32 { + hash_len := hash.DIGEST_SIZES[hash_algo] + src_len := len(src) + k := len(dst) + + // Note: Length checks are handled by the caller. + + // Apply padding. At this point, things cannot fail. + buf := dst + + // Assemble: DB = lHash || PS || 0x01 || M + // We first place the source message M with copy(), so that + // overlaps between source and destination buffers are supported. + copy(buf[k - src_len:], src) + hash.hash_bytes_to_buffer(hash_algo, label, buf[1+hash_len:1+hash_len << 1]) + intrinsics.mem_zero(raw_data(buf[1 + hash_len << 1:]), k - src_len - (hash_len << 1) - 2) + buf[k - src_len - 1] = 0x01 + + // Make the random seed. + seed, db := buf[1:1+hash_len], buf[1+hash_len:] + crypto.rand_bytes(seed) + + // Mask DB with the mask generated from the seed. + mgf1_xor(db, mgf1_algo, seed) + + // Mask the seed with the mask generated from the masked DB. + mgf1_xor(seed, mgf1_algo, db) + + // Padding result: EM = 0x00 || maskedSeed || maskedDB. + buf[0] = 0x00 + return 1 +} diff --git a/core/crypto/rsa/rsa_int.odin b/core/crypto/rsa/rsa_int.odin new file mode 100644 index 000000000..1e84c1fcb --- /dev/null +++ b/core/crypto/rsa/rsa_int.odin @@ -0,0 +1,110 @@ +#+private +package rsa + +import "core:bytes" + +Big_Int :: struct($N: int) { + v: [N]byte, + v_len: int, +} + +Modulus :: Big_Int(MODULUS_MAX_SIZE >> 3) +Factor :: Big_Int(FACTOR_MAX_SIZE >> 3) + +@(require_results) +modulus_set_bytes :: proc(n: ^Modulus, b: []byte) -> bool { + b_ := bytes.trim_left(b, []byte{0x00}) + b_len := len(b_) + + if b_len > size_of(n.v) || b_len == 0 { + return false + } + + copy(n.v[:], b_) + n.v_len = b_len + + return true +} + +modulus_set :: proc "contextless" (n, other: ^Modulus) { + // Copy the full thing. + copy(n.v[:], other.v[:]) + n.v_len = other.v_len +} + +@(require_results) +modulus_bytes :: #force_inline proc "contextless" (n: ^Modulus) -> []byte { + return n.v[:n.v_len] +} + +@(require_results) +modulus_len :: #force_inline proc "contextless" (n: ^Modulus) -> int { + return n.v_len +} + +@(require_results) +modulus_copyout :: proc(n: ^Modulus, dst: []byte) -> (n_len: int) { + if n_len = modulus_len(n); n_len == 0 { + return + } + + if len(dst) > 0 { + ensure(len(dst) >= n_len, "crypto/rsa: insufficent buffer size") + copy(dst, modulus_bytes(n)) + } + + return +} + +@(require_results) +modulus_is_odd :: proc "contextless" (n: ^Modulus) -> bool { + if n.v_len == 0 || n.v[n.v_len-1] & 1 == 0 { + return false + } + return true +} + +@(require_results) +factor_set_bytes :: proc(n: ^Factor, b: []byte) -> bool { + b_ := bytes.trim_left(b, []byte{0x00}) + b_len := len(b_) + + if b_len > size_of(n.v) || b_len == 0 { + return false + } + + copy(n.v[:], b_) + n.v_len = b_len + + return true +} + +factor_set :: proc "contextless" (n, other: ^Factor) { + // Copy the full thing. + copy(n.v[:], other.v[:]) + n.v_len = other.v_len +} + +@(require_results) +factor_bytes :: #force_inline proc "contextless" (n: ^Factor) -> []byte { + return n.v[:n.v_len] +} + +@(require_results) +factor_len :: #force_inline proc "contextless" (n: ^Factor) -> int { + return n.v_len +} + +@(require_results) +factor_copyout :: proc(n: ^Factor, dst: []byte) -> (n_len: int) { + if n_len = factor_len(n); n_len == 0 { + return + } + + if len(dst) > 0 { + ensure(len(dst) >= n_len, "crypto/rsa: insufficent buffer size") + copy(dst, factor_bytes(n)) + } + + return +} diff --git a/core/crypto/rsa/rsa_keygen.odin b/core/crypto/rsa/rsa_keygen.odin new file mode 100644 index 000000000..c0ca12043 --- /dev/null +++ b/core/crypto/rsa/rsa_keygen.odin @@ -0,0 +1,367 @@ +package rsa + +// Copyright (c) 2018 Thomas Pornin +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS “AS IS” AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import "core:crypto" +import bigint "core:crypto/_bigint" +import subtle "core:crypto/_subtle" +import "core:slice" + +// Swap two buffers in RAM. They must be disjoint. +@(private="file") +bufswap_u32 :: proc "contextless" (b1, b2: []u32) { + l := len(b1) + + for u in 0.. u32 { + // We need temporary values for at least 7 integers of the same size + // as a factor (including header word); more space helps with performance + // (in modular exponentiations), but we much prefer to remain under + // 2 kilobytes in total, to save stack space. The macro TEMPS below + // exceeds 512 (which is a count in 32-bit words) when MODULUS_MAX_SIZE + // is greater than 4464 (default value is 4096, so the 2-kB limit is + // maintained unless MODULUS_MAX_SIZE was modified). + TEMPS :: max(512, ((((7 * ((((MODULUS_MAX_SIZE + 1) >> 1) + 61) / 31))) + 1) >> 1) << 1) + + assert(key_size >= MODULUS_MIN_SIZE && key_size <= MODULUS_MAX_SIZE) + + t64: [TEMPS >> 1]u64 + t32 := slice.reinterpret([]u32, t64[:]) + defer crypto.zero_explicit(&t64, size_of(t64)) + + esize_p := u32(key_size + 1) >> 1 + esize_q := u32(key_size) - esize_p + sk._p.v_len = int((esize_p + 7) >> 3) + sk._q.v_len = int((esize_q + 7) >> 3) + sk._dp.v_len = sk._p.v_len + sk._dq.v_len = sk._q.v_len + sk._iq.v_len = sk._p.v_len + + pk := &sk._pub_key + pk._n.v_len = (key_size + 7) >> 3 + pk._e = PUBLIC_EXPONENT + + sk._d.v_len = pk._n.v_len // Private exponent length is that of the modulus. + + // We now switch to encoded sizes. + // + // floor((x * 16913) / (2^19)) is equal to floor(x/31) for all + // integers x from 0 to 34966; the intermediate product fits on + // 30 bits, thus we can use MUL31(). + esize_p += u32(bigint._mul31(esize_p, 16913) >> 19) + esize_q += u32(bigint._mul31(esize_q, 16913) >> 19) + plen := (esize_p + 31) >> 5 + qlen := (esize_q + 31) >> 5 + p := t32 + q := p[1 + plen:] + t := q[1 + qlen:] + + // Since we use a prime exponent, when searching for candidate primes, + // checking if `GCD(e, prime - 1) = 1` is a simple matter of euclidian + // division. + for { + bigint.i62_mkprime(p, esize_p, PUBLIC_EXPONENT, t) + p[1] -= 1 + if bigint.i31_rem(p, PUBLIC_EXPONENT) != 0 { + p[1] += 1 + break + } + } + + for { + bigint.i62_mkprime(q, esize_q, PUBLIC_EXPONENT, t) + q[1] -= 1 + if bigint.i31_rem(q, PUBLIC_EXPONENT) != 0 { + q[1] += 1 + break + } + } + + // If p and q have the same size, then it is possible that q > p + // (when the target modulus size is odd, we generate p with a + // greater bit length than q). If q > p, we want to swap p and q + // for two reasons: + // - The final step below (inversion of q modulo p) is easier if + // p > q. + // - While BearSSL's RSA code is perfectly happy with RSA keys such + // that p < q, some other implementations have restrictions and + // require p > q. + // + // Note that we can do a simple non-constant-time swap here, + // because the only information we leak here is that we insist on + // returning p and q such that p > q, which is not a secret. + if esize_p == esize_q && bigint.i31_sub(p, q, 0) == 1 { + bufswap_u32(p[:1+plen], q) + } + + sk_p, sk_q := factor_bytes(&sk._p), factor_bytes(&sk._q) + bigint.i31_encode(sk_p, p) + bigint.i31_encode(sk_q, q) + // The odds of this happening are infinitesimally small, however + // checking for it is cheap. + if crypto.compare_constant_time(sk_p, sk_q) == 1 { + return 0 + } + + // Compute the public modulus too. + bigint.i31_zero(t, p[0]) + bigint.i31_mulacc(t, p, q) + bigint.i31_encode(modulus_bytes(&pk._n), t) + + // Compute the private exponent. + // + // Computing p - 1 and q - 1 this way is safe as p and q + // are guaranteed to be odd, thus the LSB will always be + // set. + p[1], q[1] = p[1] - 1, q[1] - 1 // p = p - 1, q = q - 1 + if compute_privexp(sk, p, q, pk._e, t) != 1 { + return 0 + } + + // Compute `d % (p - 1)`. + d_mod := t[:1+plen] + bigint.i31_decode_reduce(d_mod, modulus_bytes(&sk._d), p) + bigint.i31_encode(factor_bytes(&sk._dp), d_mod) + + // Compute `d % (q - 1)`. + bigint.i31_decode_reduce(d_mod, modulus_bytes(&sk._d), q) + bigint.i31_encode(factor_bytes(&sk._dq), d_mod) + + // Compute `q^(-1) mod p`. + p[1], q[1] = p[1] + 1, q[1] + 1 // Restore p, q. + return compute_qinv(sk, p, q, plen, t) +} + +@(private="file") +compute_qinv :: proc "contextless" (sk: ^Private_Key, p, q: []u32, plen: u32, t: []u32) -> u32 { + // Per Fermat's Little Theorem, `q^(-1) mod p = q^(p-2) mod p`. + // + // Note: p is guaranteed to be odd as it is a large prime. + + // Compute and encode `p-2`. + p_minus_two := t[:1+plen] + copy(p_minus_two, p[:1+plen]) + two := t[1+plen:] // Temporarily use this for 2. + bigint.i31_zero(two, p[0]) + bigint.i31_decode(two, []byte{2}) + _ = bigint.i31_sub(p_minus_two, two, 1) + iq := factor_bytes(&sk._iq) // Temporarily use this for p - 2. + bigint.i31_encode(iq, p_minus_two) + + // Enforce 64-bit alignment. + t_ := t + if len(t_) & 1 != 0 { + t_ = t_[1:] + } + + m0i := bigint.i31_ninv31(p[1]) + ret := bigint.i62_modpow_opt_as_i31(q, iq, p, m0i, t) + if ret != 0 { + bigint.i31_encode(iq, q) + } + + return ret +} + +@(private="file") +compute_privexp :: proc "contextless" (sk: ^Private_Key, p_minus_one, q_minus_one: []u32, e: u32, tmp: []u32) -> u32 { + // Compute phi = (p-1)*(q-1). The mulacc function sets the announced + // bit length of t to be the sum of the announced bit lengths of + // p-1 and q-1, which is usually exact but may overshoot by one 1 + // bit in some cases; we readjust it to its true length. + phi := tmp + bigint.i31_zero(phi, p_minus_one[0]) + bigint.i31_mulacc(phi, p_minus_one, q_minus_one) + _len := (phi[0] + 31) >> 5 + phi[0] = bigint.i31_bit_length(phi[1:1+_len]) + _len = (phi[0] + 31) >> 5 + + // Divide phi by public exponent e. The final remainder r must be + // non-zero (otherwise, the key is invalid). The quotient is k, + // which we write over phi, since we don't need phi after that. + r: u32 + for u := _len; u >= 1; u -= 1 { + // Upon entry, r < e, and phi[u] < 2^31; hence, + // hi:lo < e*2^31. Thus, the produced word k[u] + // must be lower than 2^31, and the new remainder r + // is lower than e. + hi := r >> 1 + lo := (r << 31) + phi[u] + phi[u], r = bigint.div_rem_u32(hi, lo, e) + } + if r == 0 { + return 0 + } + k := phi + + // Compute u and v such that u*e - v*r = GCD(e,r). We use + // a binary GCD algorithm, with 6 extra integers a, b, + // u0, u1, v0 and v1. Initial values are: + // a = e u0 = 1 v0 = 0 + // b = r u1 = r v1 = e-1 + // The following invariants are maintained: + // a = u0*e - v0*r + // b = u1*e - v1*r + // 0 < a <= e + // 0 < b <= r + // 0 <= u0 <= r + // 0 <= v0 <= e + // 0 <= u1 <= r + // 0 <= v1 <= e + // + // At each iteration, we reduce either a or b by one bit, and + // adjust u0, u1, v0 and v1 to maintain the invariants: + // - if a is even, then a <- a/2 + // - otherwise, if b is even, then b <- b/2 + // - otherwise, if a > b, then a <- (a-b)/2 + // - otherwise, if b > a, then b <- (b-a)/2 + // Algorithm stops when a = b. At that point, the common value + // is the GCD of e and r; it must be 1 (otherwise, the private + // key or public exponent is not valid). The (u0,v0) or (u1,v1) + // pairs are the solution we are looking for. + // + // Since either a or b is reduced by at least 1 bit at each + // iteration, 62 iterations are enough to reach the end + // condition. + // + // To maintain the invariants, we must compute the same operations + // on the u* and v* values that we do on a and b: + // - When a is divided by 2, u0 and v0 must be divided by 2. + // - When b is divided by 2, u1 and v1 must be divided by 2. + // - When b is subtracted from a, u1 and v1 are subtracted from + // u0 and v0, respectively. + // - When a is subtracted from b, u0 and v0 are subtracted from + // u1 and v1, respectively. + // + // However, we want to keep the u* and v* values in their proper + // ranges. The following remarks apply: + // + // - When a is divided by 2, then a is even. Therefore: + // + // * If r is odd, then u0 and v0 must have the same parity; + // if they are both odd, then adding r to u0 and e to v0 + // makes them both even, and the division by 2 brings them + // back to the proper range. + // + // * If r is even, then u0 must be even; if v0 is odd, then + // adding r to u0 and e to v0 makes them both even, and the + // division by 2 brings them back to the proper range. + // + // Thus, all we need to do is to look at the parity of v0, + // and add (r,e) to (u0,v0) when v0 is odd. In order to avoid + // a 32-bit overflow, we can add ((r+1)/2,(e/2)+1) after the + // division (r+1 does not overflow since r < e; and (e/2)+1 + // is equal to (e+1)/2 since e is odd). + // + // - When we subtract b from a, three cases may occur: + // + // * u1 <= u0 and v1 <= v0: just do the subtractions + // + // * u1 > u0 and v1 > v0: compute: + // (u0, v0) <- (u0 + r - u1, v0 + e - v1) + // + // * u1 <= u0 and v1 > v0: compute: + // (u0, v0) <- (u0 + r - u1, v0 + e - v1) + // + // The fourth case (u1 > u0 and v1 <= v0) is not possible + // because it would contradict "b < a" (which is the reason + // why we subtract b from a). + // + // The tricky case is the third one: from the equations, it + // seems that u0 may go out of range. However, the invariants + // and ranges of other values imply that, in that case, the + // new u0 does not actually exceed the range. + // + // We can thus handle the subtraction by adding (r,e) based + // solely on the comparison between v0 and v1. + a, b: u32 = e, r + u0, v0: u32 = 1, 0 + u1, v1: u32 = r, e - 1 + hr, he := (r + 1) >> 1, (e >> 1) + 1 + for _ in 0..<62 { + oa := a & 1 // 1 if a is odd + ob := b & 1 // 1 if b is odd + agtb := subtle.gt(a, b) // 1 if a > b + bgta := subtle.gt(b, a) // 1 if b > a + + sab := oa & ob & agtb // 1 if a <- a-b + sba := oa & ob & bgta // 1 if b <- b-a + + // a <- a-b, u0 <- u0-u1, v0 <- v0-v1 + ctl := subtle.gt(v1, v0) + a -= b & -sab + u0 -= (u1 - (r & -ctl)) & -sab + v0 -= (v1 - (e & -ctl)) & -sab + + // b <- b-a, u1 <- u1-u0 mod r, v1 <- v1-v0 mod e + ctl = subtle.gt(v0, v1) + b -= a & -sba + u1 -= (u0 - (r & -ctl)) & -sba + v1 -= (v0 - (e & -ctl)) & -sba + + da := subtle.not(oa) | sab // 1 if a <- a/2 + db := (oa & subtle.not(ob)) | sba // 1 if b <- b/2 + + // a <- a/2, u0 <- u0/2, v0 <- v0/2 + ctl = v0 & 1 + a ~= (a ~ (a >> 1)) & -da + u0 ~= (u0 ~ ((u0 >> 1) + (hr & -ctl))) & -da + v0 ~= (v0 ~ ((v0 >> 1) + (he & -ctl))) & -da + + // b <- b/2, u1 <- u1/2 mod r, v1 <- v1/2 mod e + ctl = v1 & 1 + b ~= (b ~ (b >> 1)) & -db + u1 ~= (u1 ~ ((u1 >> 1) + (hr & -ctl))) & -db + v1 ~= (v1 ~ ((v1 >> 1) + (he & -ctl))) & -db + } + + // Check that the GCD is indeed 1. If not, then the key is invalid + // (and there's no harm in leaking that piece of information). + if (a != 1) { + return 0 + } + + // Now we have u0*e - v0*r = 1. Let's compute the result as: + // d = u0 + v0*k + // We still have k in the tmp[] array, and its announced bit + // length is that of phi. + m := k[1+_len:] + m[0] = (1 << 5) + 1 // bit length is 32 bits, encoded + m[1] = v0 & bigint.I31_MASK + m[2] = v0 >> 31 + z := m[3:] + bigint.i31_zero(z, k[0]) + z[1] = u0 & bigint.I31_MASK + z[2] = u0 >> 31 + bigint.i31_mulacc(z, k, m) + + // Encode the result. + bigint.i31_encode(modulus_bytes(&sk._d), z) + + return 1 +} diff --git a/core/crypto/rsa/rsa_mgf1.odin b/core/crypto/rsa/rsa_mgf1.odin new file mode 100644 index 000000000..70500c65a --- /dev/null +++ b/core/crypto/rsa/rsa_mgf1.odin @@ -0,0 +1,49 @@ +package rsa + +// Copyright (c) 2018 Thomas Pornin +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS “AS IS” AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import "core:crypto/hash" +import "core:encoding/endian" + +@(private) +mgf1_xor :: proc(data: []byte, hash_algo: hash.Algorithm, seed: []byte) { + tmp: [hash.MAX_DIGEST_SIZE]byte = --- + ctx: hash.Context = --- + + buf, blen := data, len(data) + hlen := hash.DIGEST_SIZES[hash_algo] + digest := tmp[:hlen] + for u, c := int(0), u32(0); u < blen; u, c = u + hlen, c + 1 { + hash.init(&ctx, hash_algo) + hash.update(&ctx, seed) + endian.unchecked_put_u32be(tmp[:], c) + hash.update(&ctx, tmp[:4]) + hash.final(&ctx, digest) + for v in 0..= blen { + break + } + buf[u + v] ~= digest[v] + } + } +} diff --git a/core/crypto/rsa/rsa_modpow_priv.odin b/core/crypto/rsa/rsa_modpow_priv.odin new file mode 100644 index 000000000..2de4ffb39 --- /dev/null +++ b/core/crypto/rsa/rsa_modpow_priv.odin @@ -0,0 +1,165 @@ +package rsa + +// Copyright (c) 2016 Thomas Pornin +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS “AS IS” AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import "core:crypto" +import bigint "core:crypto/_bigint" +import "core:slice" + +@(private, require_results) +private_modpow :: proc(x: []byte, sk: ^Private_Key) -> u32 { + U :: (2 + ((FACTOR_MAX_SIZE + 30) / 31)) + TLEN :: (4 * U) // TLEN is counted in 64-bit words + + ensure(sk._is_initialized, "crypto/rsa: uninitialized private key") + + // Compute the actual lengths of p and q, in bytes. + // These lengths are not considered secret (we cannot really hide + // them anyway in constant-time code). + // + // Note/yawning: The factors should already be the correct size, + // with leading `0x00`s stripped. + p := factor_bytes(&sk._p) + plen := len(p) + for plen > 0 && p[0] == 0 { + p = p[1:] + plen -= 1 + } + q := factor_bytes(&sk._q) + qlen := len(q) + for qlen > 0 && q[0] == 0 { + q = q[1:] + qlen -= 1 + } + + // Compute the maximum factor length, in 31-bit words. + z := max(plen, qlen) << 3 + fwlen := 1 + for z > 0 { + z -= 31 + fwlen += 1 + } + + // Convert size to 62-bit words. + fwlen = (fwlen + 1) >> 1 + + // We need to fit at least 6 values in the stack buffer. + if 6 * fwlen > TLEN { + return 0 + } + + // Compute signature length (in bytes). + xlen := modulus_len(&sk._pub_key._n) + + tmp_: [TLEN]u64 // WARNING: This must be zeroed out. + defer crypto.zero_explicit(&tmp_, size_of(tmp_)) + tmp := tmp_[:] + + // Decode q. + mq := slice.reinterpret([]u32, tmp) + bigint.i31_decode(mq, q) + + // Decode p. + t1 := slice.reinterpret([]u32, tmp[fwlen:]) + bigint.i31_decode(t1, p) + + // Upstream recomputes the public modulus n, but we can just + // decode it as our key representation stores all PKCS#1 + // private key values, + t2 := slice.reinterpret([]u32, tmp[2*fwlen:]) + bigint.i31_decode(t2, modulus_bytes(&sk._pub_key._n)) + + // We encode the modulus into bytes, to perform the comparison + // with bytes. We know that the product length, in bytes, is + // exactly xlen. + // The comparison actually computes the carry when subtracting + // the modulus from the source value; that carry must be 1 for + // a value in the correct range. We keep it in r, which is our + // accumulator for the error code. + m_buf := slice.reinterpret([]byte, tmp[4*fwlen:]) + bigint.i31_encode(m_buf[:xlen], t2) + u := xlen + r: u32 + for u > 0 { + u -= 1 + wn := u32(m_buf[u]) + wx := u32(x[u]) + r = ((wx - (wn + r)) >> 8) & 1 + } + + // Move the decoded p to another temporary buffer. + mp := t2 + copy(mp, t1[:2*fwlen]) + + // Compute s2 = x^dq mod q. + q0i := bigint.i31_ninv31(mq[1]) + s2 := t1 + bigint.i31_decode_reduce(s2, x, mq) + r &= bigint.i62_modpow_opt(s2, factor_bytes(&sk._dq), mq, q0i, tmp[3*fwlen:]) + + // Compute s1 = x^dp mod p. + p0i := bigint.i31_ninv31(mp[1]) + s1 := slice.reinterpret([]u32, tmp[3*fwlen:]) + bigint.i31_decode_reduce(s1, x, mp) + r &= bigint.i62_modpow_opt(s1, factor_bytes(&sk._dp), mp, p0i, tmp[4*fwlen:]) + + // Compute: + // h = (s1 - s2)*(1/q) mod p + // s1 is an integer modulo p, but s2 is modulo q. PKCS#1 is + // unclear about whether p may be lower than q (some existing, + // widely deployed implementations of RSA don't tolerate p < q), + // but we want to support that occurrence, so we need to use the + // reduction function. + // + // Since we use br_i31_decode_reduce() for iq (purportedly, the + // inverse of q modulo p), we also tolerate improperly large + // values for this parameter. + t1 = slice.reinterpret([]u32, tmp[4*fwlen:]) + t2 = slice.reinterpret([]u32, tmp[5*fwlen:]) + bigint.i31_reduce(t2, s2, mp) + _ = bigint.i31_add(s1, mp, bigint.i31_sub(s1, t2, 1)) + bigint.i31_to_monty(s1, mp) + bigint.i31_decode_reduce(t1, factor_bytes(&sk._iq), mp) + bigint.i31_montymul(t2, s1, t1, mp, p0i) + + // h is now in t2. We compute the final result: + // s = s2 + q*h + // All these operations are non-modular. + // + // We need mq, s2 and t2. We use the t3 buffer as destination. + // The buffers mp, s1 and t1 are no longer needed, so we can + // reuse them for t3. Moreover, the first step of the computation + // is to copy s2 into t3, after which s2 is not needed. Right + // now, mq is in slot 0, s2 is in slot 1, and t2 is in slot 5. + // Therefore, we have ample room for t3 by simply using s2. + t3 := s2 + bigint.i31_mulacc(t3, mq, t2) + + // Encode the result. Since we already checked the value of xlen, + // we can just use it right away. + bigint.i31_encode(x, t3) + + // The only error conditions remaining at that point are invalid + // values for p and q (even integers). + return p0i & q0i & r +} diff --git a/core/crypto/rsa/rsa_modpow_pub.odin b/core/crypto/rsa/rsa_modpow_pub.odin new file mode 100644 index 000000000..f40bddf29 --- /dev/null +++ b/core/crypto/rsa/rsa_modpow_pub.odin @@ -0,0 +1,89 @@ +package rsa + +// Copyright (c) 2016 Thomas Pornin +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS “AS IS” AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import bigint "core:crypto/_bigint" +import "core:encoding/endian" +import "core:slice" + +@(private, require_results) +public_modpow :: proc(x: []byte, pk: ^Public_Key) -> u32 { + TLEN :: (2 * (2 + ((MODULUS_MAX_SIZE + 30) / 31))) + + ensure(pk._is_initialized, "crypto/rsa: uninitialized public key") + + // Get the actual length of the modulus, and see if it fits within + // our stack buffer. We also check that the length of x[] is valid. + // + // Note/yawning: The modulus should already be the correct size, + // with leading `0x00`s stripped. + n := modulus_bytes(&pk._n) + nlen := modulus_len(&pk._n) + for nlen > 0 && n[0] == 0 { + n = n[1:] + nlen -= 1 + } + if nlen == 0 || nlen > (MODULUS_MAX_SIZE >> 3) || len(x) != nlen { + return 0 + } + z := nlen << 3 + fwlen := 1 + for z > 0 { + z -= 31 + fwlen += 1 + } + // Convert fwlen to a count in 62-bit words. + fwlen = (fwlen + 1) >> 1 + + // The modulus gets decoded into m[]. + // The value to exponentiate goes into a[]. + tmp: [TLEN]u64 // WARNING: This must be zeroed out. + m := slice.reinterpret([]u32, tmp[:fwlen]) + a := slice.reinterpret([]u32, tmp[fwlen:2*fwlen]) + + // Decode the modulus. + bigint.i31_decode(m, n) + m0i := bigint.i31_ninv31(m[1]) + + // Note: if m[] is even, then m0i == 0. Otherwise, m0i must be + // an odd integer. + r := m0i & 1 + + // Decode x[] into a[]; we also check that its value is proper. + r &= bigint.i31_decode_mod(a, x, m) + + // Compute the modular exponentiation. + e_: [EXPONENT_MAX_SIZE >> 3]byte + e_off: int + endian.unchecked_put_u32be(e_[:], pk._e) + if e_[0] == 0 { + // `e = 65537` is the most common and sensible value, so this + // is the most sensible value. + e_off = 1 + } + bigint.i62_modpow_opt(a, e_[e_off:], m, m0i, tmp[2*fwlen:]) + + // Encode the result. + bigint.i31_encode(x, a) + return r +} diff --git a/core/crypto/rsa/rsa_sig_pkcs1.odin b/core/crypto/rsa/rsa_sig_pkcs1.odin new file mode 100644 index 000000000..dd01c654b --- /dev/null +++ b/core/crypto/rsa/rsa_sig_pkcs1.odin @@ -0,0 +1,233 @@ +package rsa + +// Copyright (c) 2017 Thomas Pornin +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS “AS IS” AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import "core:bytes" +import "core:crypto" +import "core:crypto/hash" + +// PKCS1_HASH_OIDS maps common hash algorithms to the OIDs for +// use with PKCS#1 signatures. +@(rodata) +PKCS1_HASH_OIDS := #partial [hash.Algorithm][]byte { + // WARNING: Legacy verification ONLY. + .Insecure_SHA1 = []byte{ + 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A, + }, + .SHA224 = []byte{ + 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, + }, + .SHA256 = []byte{ + 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, + }, + .SHA384 = []byte{ + 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, + }, + .SHA512 = []byte{ + 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, + }, + .SHA512_256 = []byte{ + 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x06, + }, +} + +@(private="file", rodata) +PKCS1_SELFTEST_DIGEST_SHA256 := []byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, +} + +// verify_pkcs1 returns true if and only if (⟺) sig is a valid PKCS#1 +// signature by pub_key over msg, hased using hash_algo. If pre_hashed +// is set to true, it is assumed that msg is already hashed. +@(require_results) +verify_pkcs1 :: proc(pub_key: ^Public_Key, hash_algo: hash.Algorithm, msg, sig: []byte, is_prehashed := false) -> bool { + if !pub_key._is_initialized { + return false + } + if len(sig) != modulus_len(&pub_key._n) { + return false + } + + // Lookup the OID. + oid := PKCS1_HASH_OIDS[hash_algo] + if oid == nil { + return false + } + hash_len := hash.DIGEST_SIZES[hash_algo] + + // Compute the message hash. + msg_hash_buf: [hash.MAX_DIGEST_SIZE]byte = --- + msg_hash: []byte + switch is_prehashed { + case true: + if len(msg) != hash_len { + return false + } + msg_hash = msg + case false: + msg_hash = hash.hash_bytes_to_buffer(hash_algo, msg, msg_hash_buf[:]) + } + + // PKCS #1 V2.2 (RFC 8017) 8.2.2 specifies this as computing + // and comparing the padded hash, with unpadding and extracting + // the hash being an alternative. Upstream BearSSL implements + // the latter, which is not a problem if done correctly (which + // it does), however we will opt to go for implementing this + // as specified as it is more robust against implementation + // errors. + + // Compute the expected hash. + sig_buf, padded_hash_buf: [MODULUS_MAX_SIZE >> 3]byte = ---, --- + if len(sig) > len(sig_buf) { + return false + } + padded_hash_ := padded_hash_buf[:len(sig)] + if pkcs1_sig_pad(oid, msg_hash, padded_hash_) != 1 { + return false + } + + // Compute the signature's padded hash. + sig_ := sig_buf[:len(sig)] + copy(sig_, sig) + if public_modpow(sig_, pub_key) != 1 { + return false + } + + return bytes.equal(sig_, padded_hash_) +} + +// sign_pkcs1 returns true if and only if (⟺) it successfully writes +// the PKCS#1 signature by priv_key over msg, hashed using hash_algo. +// If pre_hashed is set to true, it is assumed that msg is already hashed. +@(require_results) +sign_pkcs1 :: proc(priv_key: ^Private_Key, hash_algo: hash.Algorithm, msg, sig: []byte, is_prehashed := false) -> bool { + if !priv_key._is_initialized { + return false + } + if len(sig) != modulus_len(&priv_key._pub_key._n) { + return false + } + + // Lookup the OID. + oid := PKCS1_HASH_OIDS[hash_algo] + if oid == nil { + return false + } + + // Compute the message hash. + msg_hash_buf: [hash.MAX_DIGEST_SIZE]byte = --- + msg_hash: []byte + switch is_prehashed { + case true: + if len(msg) != hash.DIGEST_SIZES[hash_algo] { + return false + } + msg_hash = msg + case false: + msg_hash = hash.hash_bytes_to_buffer(hash_algo, msg, msg_hash_buf[:]) + } + + if pkcs1_sig_pad(oid, msg_hash, sig) != 1 { + return false + } + + return private_modpow(sig, priv_key) == 1 +} + +@(private="file", require_results) +pkcs1_sig_pad :: proc "contextless" (hash_oid, hash, x: []byte) -> u32 { + // Padded hash value has format: + // 00 01 FF .. FF 00 30 x1 30 x2 06 x3 OID 05 00 04 x4 HASH + // + // with the following rules: + // + // -- Total length is equal to the modulus length (unsigned + // encoding). + // + // -- There must be at least eight bytes of value 0xFF. + // + // -- x4 is equal to the hash length (hash_len). + // + // -- x3 is equal to the encoded OID value length (hash_oid[0]). + // + // -- x2 = x3 + 4. + // + // -- x1 = x2 + x4 + 4 = x3 + x4 + 8. + // + // Note: the "05 00" is optional (signatures with and without + // that sequence exist in practice), but notes in PKCS#1 seem to + // indicate that the presence of that sequence (specifically, + // an ASN.1 NULL value for the hash parameters) may be slightly + // more "standard" than the opposite. + xlen, hash_len := len(x), len(hash) + + // Note/yawning: The hash OID is mandatory, as is the "05 00". + x3 := hash_oid[0] + + // Check that there is enough room for all the elements, + // including at least eight bytes of value 0xFF. + if xlen < int(x3) + hash_len + 21 { + return 0 + } + x[0] = 0x00 + x[1] = 0x01 + u := xlen - int(x3) - hash_len - 11 + for i in 2..< u { + x[i] = 0xff + } + x[u] = 0x00 + x[u + 1] = 0x30 + x[u + 2] = x3 + byte(hash_len) + 8 + x[u + 3] = 0x30 + x[u + 4] = x3 + 4 + x[u + 5] = 0x06 + copy(x[u+6:], hash_oid) + u += int(x3) + 7 + x[u] = 0x05 + u += 1 + x[u] = 0x00 + u += 1 + x[u] = 0x04 + u += 1 + x[u] = byte(hash_len) + u += 1 + copy(x[u:], hash) + + return 1 +} + +@(private) +pkcs1_sig_selftest :: proc(priv_key: ^Private_Key) -> bool { + sig_buf: [MODULUS_MAX_SIZE >> 3]byte = --- + defer crypto.zero_explicit(&sig_buf, size_of(sig_buf)) + + sig := sig_buf[:private_key_size(priv_key)] + if !sign_pkcs1(priv_key, .SHA256, PKCS1_SELFTEST_DIGEST_SHA256, sig, true) { + return false + } + + return verify_pkcs1(&priv_key._pub_key, .SHA256, PKCS1_SELFTEST_DIGEST_SHA256, sig, true) +} diff --git a/core/crypto/rsa/rsa_sig_pss.odin b/core/crypto/rsa/rsa_sig_pss.odin new file mode 100644 index 000000000..a2b19e509 --- /dev/null +++ b/core/crypto/rsa/rsa_sig_pss.odin @@ -0,0 +1,293 @@ +package rsa + +// Copyright (c) 2018 Thomas Pornin +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS “AS IS” AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import "base:intrinsics" +import "core:crypto" +import bigint "core:crypto/_bigint" +import subtle "core:crypto/_subtle" +import "core:crypto/hash" + +// verify_pss returns true if and only if (⟺) sig is a valid PSS +// signature by pub_key over msg, hashed using hash_algo, and MGF1 +// parameterized by mgf1_algo and salt_len. If mgf1_algo is +// unspecified, hash_algo will be used. If pre_hashed is set +// to true, it is assumed that msg is already hashed. +@(require_results) +verify_pss :: proc( + pub_key: ^Public_Key, + hash_algo: hash.Algorithm, + salt_len: int, + msg: []byte, + sig: []byte, + is_prehashed := false, + mgf1_algo := hash.Algorithm.Invalid, +) -> bool { + if !pub_key._is_initialized { + return false + } + if hash_algo == .Invalid { + return false + } + mgf1_algo_ := mgf1_algo + if mgf1_algo == .Invalid { + mgf1_algo_ = hash_algo + } + if len(sig) != modulus_len(&pub_key._n) { + return false + } + + // Compute the message hash. + msg_hash_buf: [hash.MAX_DIGEST_SIZE]byte = --- + hash_len := hash.DIGEST_SIZES[hash_algo] + msg_hash: []byte + switch is_prehashed { + case true: + if len(msg) != hash_len { + return false + } + msg_hash = msg + case false: + msg_hash = hash.hash_bytes_to_buffer(hash_algo, msg, msg_hash_buf[:]) + } + + sig_buf: [MODULUS_MAX_SIZE >> 3]byte = --- + sig_ := sig_buf[:len(sig)] + copy(sig_, sig) + if public_modpow(sig_, pub_key) != 1 { + return false + } + + return pss_sig_unpad(hash_algo, mgf1_algo_, msg_hash, salt_len, pub_key, sig_) == 1 +} + +// sign_pss returns true if and only if (⟺) it successfully writes +// the PKCS#1 signature by priv_key over msg, hashed using hash_algo, and +// MGF1 parameterized by mgf1_algo and salt_len. If mgf1_algo is +// unspecified, hash_algo will be used. If pre_hashed is set to true, +// it is assumed that msg is already hashed. A reasonable choice for +// salt_len is the digest size of hash_algo, and FIPS 140-3 mandates +// that as the maximum permissible size. +// +// This routine will fail if the system entropy source is unavailable. +@(require_results) +sign_pss :: proc( + priv_key: ^Private_Key, + hash_algo: hash.Algorithm, + salt_len: int, + msg: []byte, + sig: []byte, + is_prehashed := false, + mgf1_algo := hash.Algorithm.Invalid, +) -> bool { + if !priv_key._is_initialized { + return false + } + if len(sig) != modulus_len(&priv_key._pub_key._n) { + return false + } + if hash_algo == .Invalid { + return false + } + mgf1_algo_ := mgf1_algo + if mgf1_algo == .Invalid { + mgf1_algo_ = hash_algo + } + if !crypto.HAS_RAND_BYTES && salt_len != 0 { + return false + } + + // Compute the message hash. + msg_hash_buf: [hash.MAX_DIGEST_SIZE]byte = --- + hash_len := hash.DIGEST_SIZES[hash_algo] + msg_hash: []byte + switch is_prehashed { + case true: + if len(msg) != hash_len { + return false + } + msg_hash = msg + case false: + msg_hash = hash.hash_bytes_to_buffer(hash_algo, msg, msg_hash_buf[:]) + } + + // Work out the exact length of n in bits. + n := modulus_bytes(&priv_key._pub_key._n) + assert(len(n) > 0 && n[0] != 0) + n_bitlen := int(bigint._u32_bit_length(u32(n[0]))) + (len(n) - 1) * 8 + + if pss_sig_pad(hash_algo, mgf1_algo_, msg_hash, salt_len, n_bitlen, sig) != 1 { + return false + } + + return private_modpow(sig, priv_key) == 1 +} + +@(private="file", require_results) +pss_sig_unpad :: proc( + data_algo: hash.Algorithm, + mgf1_algo: hash.Algorithm, + digest: []byte, + salt_len: int, + pk: ^Public_Key, + sig: []byte, +) -> u32 { + hash_len := hash.DIGEST_SIZES[data_algo] + x := sig + + // Value r will be set to a non-zero value is any test fails. + r: u32 + + // The value bit length (as an integer) must be strictly less than + // that of the modulus. + // + // Note/yawning: The modulus should already be the correct size, + // with leading `0x00`s stripped. + n := modulus_bytes(&pk._n) + nlen := modulus_len(&pk._n) + u: int + for u = 0; u < nlen; u += 1 { + if n[u] != 0 { + break + } + } + if u == nlen { + return 0 + } + n_bitlen := bigint._u32_bit_length(u32(n[u])) + (u32(nlen - u - 1) << 3) + n_bitlen -= 1 + if (n_bitlen & 7) == 0 { + r |= u32(x[0]) + x = x[1:] + } else { + r |= u32(x[0] & (0xFF << (n_bitlen & 7))) + } + xlen := int((n_bitlen + 7) >> 3) + + // Check that the modulus is large enough for the hash value + // length combined with the intended salt length. + if hash_len > xlen || salt_len > xlen || (hash_len + salt_len + 2) > xlen { + return 0 + } + + // Check value of rightmost byte. + r |= u32(x[xlen - 1] ~ 0xBC) + + // Generate the mask and XOR it into the first bytes to reveal PS; + // we must also mask out the leading bits. + seed := x[xlen - hash_len - 1:] + mgf1_xor(x[:xlen - hash_len - 1], mgf1_algo, seed[:hash_len]) + if (n_bitlen & 7) != 0 { + x[0] &= 0xFF >> (8 - (n_bitlen & 7)) + } + + // Check that all padding bytes have the expected value. + for u = 0; u < (xlen - hash_len - salt_len - 2); u += 1 { + r |= u32(x[u]) + } + r |= u32(x[xlen - hash_len - salt_len - 2] ~ 0x01) + + // Recompute H. + salt := x[xlen - hash_len - salt_len - 1:] + tmp: [hash.MAX_DIGEST_SIZE]byte + h := tmp[:hash_len] + ctx: hash.Context = --- + hash.init(&ctx, data_algo) + hash.update(&ctx, tmp[:8]) + hash.update(&ctx, digest) + hash.update(&ctx, salt[:salt_len]) + hash.final(&ctx, h) + + // Check that the recomputed H value matches the one appearing + // in the string. + x = x[xlen - hash_len - 1:] + r |= subtle.eq0(u32(crypto.compare_constant_time(h, x[:hash_len]))) + + return subtle.eq0(r) +} + +@(private="file", require_results) +pss_sig_pad :: proc( + data_algo: hash.Algorithm, + mgf1_algo: hash.Algorithm, + digest: []byte, + salt_len: int, + n_bitlen_: int, + sig: []byte, +) -> u32 { + x, n_bitlen := sig, n_bitlen_ + hash_len := hash.DIGEST_SIZES[data_algo] + + // The padded string is one bit smaller than the modulus; + // notably, if the modulus length is equal to 1 modulo 8, then + // the padded string will be one _byte_ smaller, and the first + // byte will be set to 0. We apply these transformations here. + n_bitlen -= 1 + if (n_bitlen & 7) == 0 { + x[0] = 0 + x = x[1:] + } + xlen := int((n_bitlen + 7) >> 3) + + // Check that the modulus is large enough for the hash value + // length combined with the intended salt length. + if hash_len > xlen || salt_len > xlen || (hash_len + salt_len + 2) > xlen { + return 0 + } + + // Produce a random salt. + salt := x[xlen - hash_len - salt_len - 1:] + salt = salt[:salt_len] + if salt_len != 0 { + crypto.rand_bytes(salt) + } + + // Compute the seed for MGF1. + seed := x[xlen - hash_len - 1:] + seed = seed[:hash_len] + ctx: hash.Context = --- + hash.init(&ctx, data_algo) + intrinsics.mem_zero(raw_data(seed), 8) + hash.update(&ctx, seed[:8]) + hash.update(&ctx, digest) + hash.update(&ctx, salt) + hash.final(&ctx, seed) + + // Prepare string PS (padded salt). The salt is already at the + // right place. + intrinsics.mem_zero(raw_data(x), xlen - salt_len - hash_len - 2) + x[xlen - salt_len - hash_len - 2] = 0x01 + + // Generate the mask and XOR it into PS. + mgf1_xor(x[:xlen - hash_len - 1], mgf1_algo, seed) + + // Clear the top bits to ensure the value is lower than the + // modulus. + x[0] &= 0xFF >> ((u32(xlen) << 3) - u32(n_bitlen)) + + // The seed (H) is already in the right place. We just set the + // last byte. + x[xlen - 1] = 0xBC + + return 1 +} diff --git a/core/crypto/rsa/rsa_test_key.odin b/core/crypto/rsa/rsa_test_key.odin new file mode 100644 index 000000000..28020d21f --- /dev/null +++ b/core/crypto/rsa/rsa_test_key.odin @@ -0,0 +1,180 @@ +package rsa + +// private_key_set_insecure_test sets the private key to the +// pregenerated INSECURE test key "testRSA2048" from RFC 9500 2.1. +// +// WARNING: This key MUST only be used for testing purposes. +@(require_results) +private_key_set_insecure_test :: proc(priv_key: ^Private_Key) -> bool { + // RFC 9500 2.1 "testRSA2048" + return private_key_set_bytes( + priv_key, + // n + []byte{ + 0xB0, 0xF9, 0xE8, 0x19, 0x43, 0xA7, 0xAE, 0x98, + 0x92, 0xAA, 0xDE, 0x17, 0xCA, 0x7C, 0x40, 0xF8, + 0x74, 0x4F, 0xED, 0x2F, 0x81, 0x48, 0xE6, 0xC8, + 0xEA, 0xA2, 0x7B, 0x7D, 0x00, 0x15, 0x48, 0xFB, + 0x51, 0x92, 0xAB, 0x28, 0xB5, 0x6C, 0x50, 0x60, + 0xB1, 0x18, 0xCC, 0xD1, 0x31, 0xE5, 0x94, 0x87, + 0x4C, 0x6C, 0xA9, 0x89, 0xB5, 0x6C, 0x27, 0x29, + 0x6F, 0x09, 0xFB, 0x93, 0xA0, 0x34, 0xDF, 0x32, + 0xE9, 0x7C, 0x6F, 0xF0, 0x99, 0x8C, 0xFD, 0x8E, + 0x6F, 0x42, 0xDD, 0xA5, 0x8A, 0xCD, 0x1F, 0xA9, + 0x79, 0x86, 0xF1, 0x44, 0xF3, 0xD1, 0x54, 0xD6, + 0x76, 0x50, 0x17, 0x5E, 0x68, 0x54, 0xB3, 0xA9, + 0x52, 0x00, 0x3B, 0xC0, 0x68, 0x87, 0xB8, 0x45, + 0x5A, 0xC2, 0xB1, 0x9F, 0x7B, 0x2F, 0x76, 0x50, + 0x4E, 0xBC, 0x98, 0xEC, 0x94, 0x55, 0x71, 0xB0, + 0x78, 0x92, 0x15, 0x0D, 0xDC, 0x6A, 0x74, 0xCA, + 0x0F, 0xBC, 0xD3, 0x54, 0x97, 0xCE, 0x81, 0x53, + 0x4D, 0xAF, 0x94, 0x18, 0x84, 0x4B, 0x13, 0xAE, + 0xA3, 0x1F, 0x9D, 0x5A, 0x6B, 0x95, 0x57, 0xBB, + 0xDF, 0x61, 0x9E, 0xFD, 0x4E, 0x88, 0x7F, 0x2D, + 0x42, 0xB8, 0xDD, 0x8B, 0xC9, 0x87, 0xEA, 0xE1, + 0xBF, 0x89, 0xCA, 0xB8, 0x5E, 0xE2, 0x1E, 0x35, + 0x63, 0x05, 0xDF, 0x6C, 0x07, 0xA8, 0x83, 0x8E, + 0x3E, 0xF4, 0x1C, 0x59, 0x5D, 0xCC, 0xE4, 0x3D, + 0xAF, 0xC4, 0x91, 0x23, 0xEF, 0x4D, 0x8A, 0xBB, + 0xA9, 0x3D, 0x39, 0x05, 0xE4, 0x02, 0x8D, 0x7B, + 0xA9, 0x14, 0x84, 0xA2, 0x75, 0x96, 0xE0, 0x7B, + 0x4B, 0x6E, 0xD9, 0x92, 0xF0, 0x77, 0xB5, 0x24, + 0xD3, 0xDC, 0xFE, 0x7D, 0xDD, 0x55, 0x49, 0xBE, + 0x7C, 0xCE, 0x8D, 0xA0, 0x35, 0xCF, 0xA0, 0xB3, + 0xFB, 0x8F, 0x9E, 0x46, 0xF7, 0x32, 0xB2, 0xA8, + 0x6B, 0x46, 0x01, 0x65, 0xC0, 0x8F, 0x53, 0x13, + }, + // e + []byte{0x01, 0x00, 0x01}, + // d + []byte{ + 0x41, 0x18, 0x8B, 0x20, 0xCF, 0xDB, 0xDB, 0xC2, + 0xCF, 0x1F, 0xFE, 0x75, 0x2D, 0xCB, 0xAA, 0x72, + 0x39, 0x06, 0x35, 0x2E, 0x26, 0x15, 0xD4, 0x9D, + 0xCE, 0x80, 0x59, 0x7F, 0xCF, 0x0A, 0x05, 0x40, + 0x3B, 0xEF, 0x00, 0xFA, 0x06, 0x51, 0x82, 0xF7, + 0x2D, 0xEC, 0xFB, 0x59, 0x6F, 0x4B, 0x0C, 0xE8, + 0xFF, 0x59, 0x70, 0xBA, 0xF0, 0x7A, 0x89, 0xA5, + 0x19, 0xEC, 0xC8, 0x16, 0xB2, 0xF4, 0xFF, 0xAC, + 0x50, 0x69, 0xAF, 0x1B, 0x06, 0xBF, 0xEF, 0x7B, + 0xF6, 0xBC, 0xD7, 0x9E, 0x4E, 0x81, 0xC8, 0xC5, + 0xA3, 0xA7, 0xD9, 0x13, 0x0D, 0xC3, 0xCF, 0xBA, + 0xDA, 0xE5, 0xF6, 0xD2, 0x88, 0xF9, 0xAE, 0xE3, + 0xF6, 0xFF, 0x92, 0xFA, 0xE0, 0xF8, 0x1A, 0xF5, + 0x97, 0xBE, 0xC9, 0x6A, 0xE9, 0xFA, 0xB9, 0x40, + 0x2C, 0xD5, 0xFE, 0x41, 0xF7, 0x05, 0xBE, 0xBD, + 0xB4, 0x7B, 0xB7, 0x36, 0xD3, 0xFE, 0x6C, 0x5A, + 0x51, 0xE0, 0xE2, 0x07, 0x32, 0xA9, 0x7B, 0x5E, + 0x46, 0xC1, 0xCB, 0xDB, 0x26, 0xD7, 0x48, 0x54, + 0xC6, 0xB6, 0x60, 0x4A, 0xED, 0x46, 0x37, 0x35, + 0xFF, 0x90, 0x76, 0x04, 0x65, 0x57, 0xCA, 0xF9, + 0x49, 0xBF, 0x44, 0x88, 0x95, 0xC2, 0x04, 0x32, + 0xC1, 0xE0, 0x9C, 0x01, 0x4E, 0xA7, 0x56, 0x60, + 0x43, 0x4F, 0x1A, 0x0F, 0x3B, 0xE2, 0x94, 0xBA, + 0xBC, 0x5D, 0x53, 0x0E, 0x6A, 0x10, 0x21, 0x3F, + 0x53, 0xB6, 0x03, 0x75, 0xFC, 0x84, 0xA7, 0x57, + 0x3F, 0x2A, 0xF1, 0x21, 0x55, 0x84, 0xF5, 0xB4, + 0xBD, 0xA6, 0xD4, 0xE8, 0xF9, 0xE1, 0x7A, 0x78, + 0xD9, 0x7E, 0x77, 0xB8, 0x6D, 0xA4, 0xA1, 0x84, + 0x64, 0x75, 0x31, 0x8A, 0x7A, 0x10, 0xA5, 0x61, + 0x01, 0x4E, 0xFF, 0xA2, 0x3A, 0x81, 0xEC, 0x56, + 0xE9, 0xE4, 0x10, 0x9D, 0xEF, 0x8C, 0xB3, 0xF7, + 0x97, 0x22, 0x3F, 0x7D, 0x8D, 0x0D, 0x43, 0x51, + }, + // p + []byte{ + 0xDD, 0x10, 0x57, 0x02, 0x38, 0x2F, 0x23, 0x2B, + 0x36, 0x81, 0xF5, 0x37, 0x91, 0xE2, 0x26, 0x17, + 0xC7, 0xBF, 0x4E, 0x9A, 0xCB, 0x81, 0xED, 0x48, + 0xDA, 0xF6, 0xD6, 0x99, 0x5D, 0xA3, 0xEA, 0xB6, + 0x42, 0x83, 0x9A, 0xFF, 0x01, 0x2D, 0x2E, 0xA6, + 0x28, 0xB9, 0x0A, 0xF2, 0x79, 0xFD, 0x3E, 0x6F, + 0x7C, 0x93, 0xCD, 0x80, 0xF0, 0x72, 0xF0, 0x1F, + 0xF2, 0x44, 0x3B, 0x3E, 0xE8, 0xF2, 0x4E, 0xD4, + 0x69, 0xA7, 0x96, 0x13, 0xA4, 0x1B, 0xD2, 0x40, + 0x20, 0xF9, 0x2F, 0xD1, 0x10, 0x59, 0xBD, 0x1D, + 0x0F, 0x30, 0x1B, 0x5B, 0xA7, 0xA9, 0xD3, 0x63, + 0x7C, 0xA8, 0xD6, 0x5C, 0x1A, 0x98, 0x15, 0x41, + 0x7D, 0x8E, 0xAB, 0x73, 0x4B, 0x0B, 0x4F, 0x3A, + 0x2C, 0x66, 0x1D, 0x9A, 0x1A, 0x82, 0xF3, 0xAC, + 0x73, 0x4C, 0x40, 0x53, 0x06, 0x69, 0xAB, 0x8E, + 0x47, 0x30, 0x45, 0xA5, 0x8E, 0x65, 0x53, 0x9D, + }, + // q + []byte{ + 0xCC, 0xF1, 0xE5, 0xBB, 0x90, 0xC8, 0xE9, 0x78, + 0x1E, 0xA7, 0x5B, 0xEB, 0xF1, 0x0B, 0xC2, 0x52, + 0xE1, 0x1E, 0xB0, 0x23, 0xA0, 0x26, 0x0F, 0x18, + 0x87, 0x55, 0x2A, 0x56, 0x86, 0x3F, 0x4A, 0x64, + 0x21, 0xE8, 0xC6, 0x00, 0xBF, 0x52, 0x3D, 0x6C, + 0xB1, 0xB0, 0xAD, 0xBD, 0xD6, 0x5B, 0xFE, 0xE4, + 0xA8, 0x8A, 0x03, 0x7E, 0x3D, 0x1A, 0x41, 0x5E, + 0x5B, 0xB9, 0x56, 0x48, 0xDA, 0x5A, 0x0C, 0xA2, + 0x6B, 0x54, 0xF4, 0xA6, 0x39, 0x48, 0x52, 0x2C, + 0x3D, 0x5F, 0x89, 0xB9, 0x4A, 0x72, 0xEF, 0xFF, + 0x95, 0x13, 0x4D, 0x59, 0x40, 0xCE, 0x45, 0x75, + 0x8F, 0x30, 0x89, 0x80, 0x90, 0x89, 0x56, 0x58, + 0x8E, 0xEF, 0x57, 0x5B, 0x3E, 0x4B, 0xC4, 0xC3, + 0x68, 0xCF, 0xE8, 0x13, 0xEE, 0x9C, 0x25, 0x2C, + 0x2B, 0x02, 0xE0, 0xDF, 0x91, 0xF1, 0xAA, 0x01, + 0x93, 0x8D, 0x38, 0x68, 0x5D, 0x60, 0xBA, 0x6F, + }, + // dp + []byte{ + 0x09, 0xED, 0x54, 0xEA, 0xED, 0x98, 0xF8, 0x4C, + 0x55, 0x7B, 0x4A, 0x86, 0xBF, 0x4F, 0x57, 0x84, + 0x93, 0xDC, 0xBC, 0x6B, 0xE9, 0x1D, 0xA1, 0x89, + 0x37, 0x04, 0x04, 0xA9, 0x08, 0x72, 0x76, 0xF4, + 0xCE, 0x51, 0xD8, 0xA1, 0x00, 0xED, 0x85, 0x7D, + 0xC2, 0xB0, 0x64, 0x94, 0x74, 0xF3, 0xF1, 0x5C, + 0xD2, 0x4C, 0x54, 0xDB, 0x28, 0x71, 0x10, 0xE5, + 0x6E, 0x5C, 0xB0, 0x08, 0x68, 0x2F, 0x91, 0x68, + 0xAA, 0x81, 0xF3, 0x14, 0x58, 0xB7, 0x43, 0x1E, + 0xCC, 0x1C, 0x44, 0x90, 0x6F, 0xDA, 0x87, 0xCA, + 0x89, 0x47, 0x10, 0xC3, 0x71, 0xE9, 0x07, 0x6C, + 0x1D, 0x49, 0xFB, 0xAE, 0x51, 0x27, 0x69, 0x34, + 0xF2, 0xAD, 0x78, 0x77, 0x89, 0xF4, 0x2D, 0x0F, + 0xA0, 0xB4, 0xC9, 0x39, 0x85, 0x5D, 0x42, 0x12, + 0x09, 0x6F, 0x70, 0x28, 0x0A, 0x4E, 0xAE, 0x7C, + 0x8A, 0x27, 0xD9, 0xC8, 0xD0, 0x77, 0x2E, 0x65, + }, + // dq + []byte{ + 0x8C, 0xB6, 0x85, 0x7A, 0x7B, 0xD5, 0x46, 0x5F, + 0x80, 0x04, 0x7E, 0x9B, 0x87, 0xBC, 0x00, 0x27, + 0x31, 0x84, 0x05, 0x81, 0xE0, 0x62, 0x61, 0x39, + 0x01, 0x2A, 0x5B, 0x50, 0x5F, 0x0A, 0x33, 0x84, + 0x7E, 0xB7, 0xB8, 0xC3, 0x28, 0x99, 0x49, 0xAD, + 0x48, 0x6F, 0x3B, 0x4B, 0x3D, 0x53, 0x9A, 0xB5, + 0xDA, 0x76, 0x30, 0x21, 0xCB, 0xC8, 0x2C, 0x1B, + 0xA2, 0x34, 0xA5, 0x66, 0x8D, 0xED, 0x08, 0x01, + 0xB8, 0x59, 0xF3, 0x43, 0xF1, 0xCE, 0x93, 0x04, + 0xE6, 0xFA, 0xA2, 0xB0, 0x02, 0xCA, 0xD9, 0xB7, + 0x8C, 0xDE, 0x5C, 0xDC, 0x2C, 0x1F, 0xB4, 0x17, + 0x1C, 0x42, 0x42, 0x16, 0x70, 0xA6, 0xAB, 0x0F, + 0x50, 0xCC, 0x4A, 0x19, 0x4E, 0xB3, 0x6D, 0x1C, + 0x91, 0xE9, 0x35, 0xBA, 0x01, 0xB9, 0x59, 0xD8, + 0x72, 0x8B, 0x9E, 0x64, 0x42, 0x6B, 0x3F, 0xC3, + 0xA7, 0x50, 0x6D, 0xEB, 0x52, 0x39, 0xA8, 0xA7, + }, + // iq (aka u) + []byte{ + 0x0A, 0x81, 0xD8, 0xA6, 0x18, 0x31, 0x4A, 0x80, + 0x3A, 0xF6, 0x1C, 0x06, 0x71, 0x1F, 0x2C, 0x39, + 0xB2, 0x66, 0xFF, 0x41, 0x4D, 0x53, 0x47, 0x6D, + 0x1D, 0xA5, 0x2A, 0x43, 0x18, 0xAA, 0xFE, 0x4B, + 0x96, 0xF0, 0xDA, 0x07, 0x15, 0x5F, 0x8A, 0x51, + 0x34, 0xDA, 0xB8, 0x8E, 0xE2, 0x9E, 0x81, 0x68, + 0x07, 0x6F, 0xCD, 0x78, 0xCA, 0x79, 0x1A, 0xC6, + 0x34, 0x42, 0xA8, 0x1C, 0xD0, 0x69, 0x39, 0x27, + 0xD8, 0x08, 0xE3, 0x35, 0xE8, 0xD8, 0xCB, 0xF2, + 0x12, 0x19, 0x07, 0x50, 0x9A, 0x57, 0x75, 0x9B, + 0x4F, 0x9A, 0x18, 0xFA, 0x3A, 0x7B, 0x33, 0x37, + 0x79, 0xED, 0xDE, 0x7A, 0x45, 0x93, 0x84, 0xF8, + 0x44, 0x4A, 0xDA, 0xEC, 0xFF, 0xEC, 0x95, 0xFD, + 0x55, 0x2B, 0x0C, 0xFC, 0xB6, 0xC7, 0xF6, 0x92, + 0x62, 0x6D, 0xDE, 0x1E, 0xF2, 0x68, 0xA4, 0x0D, + 0x2F, 0x67, 0xB5, 0xC8, 0xAA, 0x38, 0x7F, 0xF7, + }, + ) +} \ No newline at end of file diff --git a/examples/all/all_js.odin b/examples/all/all_js.odin index 8dbc320d0..10d2595a0 100644 --- a/examples/all/all_js.odin +++ b/examples/all/all_js.odin @@ -49,6 +49,7 @@ package all @(require) import "core:crypto/pbkdf2" @(require) import "core:crypto/poly1305" @(require) import "core:crypto/ristretto255" +@(require) import "core:crypto/rsa" @(require) import "core:crypto/sha2" @(require) import "core:crypto/sha3" @(require) import "core:crypto/shake" diff --git a/examples/all/all_main.odin b/examples/all/all_main.odin index b8655a89e..50466173e 100644 --- a/examples/all/all_main.odin +++ b/examples/all/all_main.odin @@ -54,6 +54,7 @@ package all @(require) import "core:crypto/pbkdf2" @(require) import "core:crypto/poly1305" @(require) import "core:crypto/ristretto255" +@(require) import "core:crypto/rsa" @(require) import "core:crypto/sha2" @(require) import "core:crypto/sha3" @(require) import "core:crypto/shake" diff --git a/tests/benchmark/crypto/benchmark_rsa.odin b/tests/benchmark/crypto/benchmark_rsa.odin new file mode 100644 index 000000000..5954662fc --- /dev/null +++ b/tests/benchmark/crypto/benchmark_rsa.odin @@ -0,0 +1,160 @@ +package benchmark_core_crypto + +import "base:runtime" +import "core:log" +import "core:testing" +import "core:text/table" +import "core:time" + +import "core:crypto" +import "core:crypto/rsa" + +// RSA key generation is time consuming and high variance, so it takes +// an unreasonable amount of time to get a semi-sensible value, so this +// is skipped by default. +RSA_BENCH_KEYGEN: bool : #config(ODIN_BENCHMARK_RSA_KEYGEN, false) + +@(private = "file") +KEYGEN_ITERS :: 100 +@(private = "file") +SIGN_ITERS :: 5000 +@(private = "file") +ENCRYPT_ITERS :: 5000 + +@(test) +benchmark_crypto_rsa :: proc(t: ^testing.T) { + runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD() + + tbl: table.Table + table.init(&tbl) + defer table.destroy(&tbl) + + table.caption(&tbl, "RSA") + table.aligned_header_of_values(&tbl, .Right, "Operation", "Avg. Time") + + if RSA_BENCH_KEYGEN { + bench_keygen_2048(&tbl) + table.row_of_values(&tbl) + } + bench_pkcs1_2048(&tbl) + table.row_of_values(&tbl) + bench_pss_2048(&tbl) + table.row_of_values(&tbl) + bench_oaep_2048(&tbl) + + log_table(&tbl) +} + +@(private="file") +bench_keygen_2048 :: proc(tbl: ^table.Table) { + if !crypto.HAS_RAND_BYTES { + log.warnf("rsa: keygen benchmarks skipped, no system entropy source") + } + + priv_key: rsa.Private_Key + start := time.tick_now() + for _ in 0 ..< KEYGEN_ITERS { + ok := rsa.private_key_generate(&priv_key, 2048) + assert(ok, "keygen should succeed") + } + taken := time.tick_since(start) / KEYGEN_ITERS + + append_tbl(tbl, "Keygen/2048", taken) +} + +@(private="file") +bench_pkcs1_2048 :: proc(tbl: ^table.Table) { + priv_key: rsa.Private_Key + _ = rsa.private_key_set_insecure_test(&priv_key) + + msg_bytes := transmute([]byte)(SIG_MSG) + sig_bytes: [2048 >> 3]byte + + start := time.tick_now() + for _ in 0 ..< SIGN_ITERS { + ok := rsa.sign_pkcs1(&priv_key, .SHA256, msg_bytes, sig_bytes[:]) + assert(ok, "signing should succeed") + } + taken := time.tick_since(start) / SIGN_ITERS + + append_tbl(tbl, "PKCS1/2048/SHA256/sign", taken) + + start = time.tick_now() + for _ in 0 ..< KEYGEN_ITERS { + ok := rsa.verify_pkcs1(&priv_key._pub_key, .SHA256, msg_bytes, sig_bytes[:]) + assert(ok, "verify should succeed") + } + taken = time.tick_since(start) / SIGN_ITERS + + append_tbl(tbl, "PKCS1/2048/SHA256/verify", taken) +} + +@(private="file") +bench_pss_2048 :: proc(tbl: ^table.Table) { + priv_key: rsa.Private_Key + _ = rsa.private_key_set_insecure_test(&priv_key) + + msg_bytes := transmute([]byte)(SIG_MSG) + sig_bytes: [2048 >> 3]byte + + start := time.tick_now() + for _ in 0 ..< SIGN_ITERS { + ok := rsa.sign_pss(&priv_key, .SHA256, 32, msg_bytes, sig_bytes[:]) + assert(ok, "signing should succeed") + } + taken := time.tick_since(start) / SIGN_ITERS + + append_tbl(tbl, "PSS/2048/SHA256/sign", taken) + + start = time.tick_now() + for _ in 0 ..< KEYGEN_ITERS { + ok := rsa.verify_pss(&priv_key._pub_key, .SHA256, 32, msg_bytes, sig_bytes[:]) + assert(ok, "verify should succeed") + } + taken = time.tick_since(start) / SIGN_ITERS + + append_tbl(tbl, "PSS/2048/SHA256/verify", taken) +} + +@(private="file") +bench_oaep_2048 :: proc(tbl: ^table.Table) { + if !crypto.HAS_RAND_BYTES { + log.info("rand_bytes not supported - skipping") + return + } + + priv_key: rsa.Private_Key + _ = rsa.private_key_set_insecure_test(&priv_key) + + msg_bytes := transmute([]byte)(SIG_MSG) + ciphertext_bytes: [2048 >> 3]byte + buf: [32]byte + + start := time.tick_now() + for _ in 0 ..< SIGN_ITERS { + ok := rsa.encrypt_oaep(&priv_key._pub_key, .SHA256, msg_bytes, ciphertext_bytes[:]) + assert(ok, "encryption should succeed") + } + taken := time.tick_since(start) / ENCRYPT_ITERS + + append_tbl(tbl, "OAEP/2048/SHA256/encrypt", taken) + + start = time.tick_now() + for _ in 0 ..< KEYGEN_ITERS { + _, ok := rsa.decrypt_oaep(&priv_key, .SHA256, ciphertext_bytes[:], buf[:]) + assert(ok, "decrypt should succeed") + } + taken = time.tick_since(start) / ENCRYPT_ITERS + + append_tbl(tbl, "OAEP/2048/SHA256/decrypt", taken) +} + +@(private="file") +append_tbl :: proc(tbl: ^table.Table, op_name: string, avg_time: time.Duration) { + table.aligned_row_of_values( + tbl, + .Right, + op_name, + table.format(tbl, "%8M", avg_time), + ) +} diff --git a/tests/core/crypto/common/common.odin b/tests/core/crypto/common/common.odin index 8cef8ce86..a1fdef824 100644 --- a/tests/core/crypto/common/common.odin +++ b/tests/core/crypto/common/common.odin @@ -1,6 +1,7 @@ package test_crypto_common import "core:bytes" +import "core:encoding/base64" import "core:encoding/hex" // Common helpers for cryptography tests. @@ -23,3 +24,13 @@ hexbytes_decode :: proc(x: Hex_Bytes, allocator := context.allocator) -> []byte return dst } +Jwk_Bytes :: string + +jwkbytes_decode :: proc(s: Jwk_Bytes, allocator := context.allocator) -> []byte { + dst, err := base64.decode(s, base64.DEC_URL_TABLE, allocator = allocator) + if err != nil { + panic("Jwk_Bytes: invalid hex encoding") + } + + return dst +} diff --git a/tests/core/crypto/test_core_crypto_rsa.odin b/tests/core/crypto/test_core_crypto_rsa.odin new file mode 100644 index 000000000..525c440e3 --- /dev/null +++ b/tests/core/crypto/test_core_crypto_rsa.odin @@ -0,0 +1,419 @@ +#+build !riscv64 +package test_core_crypto + +import "core:bytes" +import "core:encoding/hex" +import "core:log" +import "core:testing" + +import "core:crypto" +import "core:crypto/rsa" + +// BUG/yawning: RISC-V fails the PSS test in CI with a nonsensical +// bounds-checking error suggesting something spooky, as the failure +// was not reproducible on qemu whole system emulation. + +@(private="file") +TEST_MSG: string : "don't let them immanentize the eschaton" +@(private="file", rodata) +TEST_MSG_SHA256 := []byte{ + 0x50, 0x95, 0x66, 0x8e, 0x7c, 0xd0, 0xd5, 0x8e, + 0x9d, 0x59, 0xf8, 0x4a, 0x1a, 0x46, 0x5a, 0x8a, + 0x2e, 0x69, 0xcc, 0xad, 0x6a, 0xca, 0x8b, 0xb7, + 0x55, 0x55, 0x15, 0x71, 0x9b, 0xc7, 0x50, 0x88, +} +@(private="file", rodata) +TEST_TLS_PMS := []byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, +} + +@(test) +test_rsa_private_key :: proc(t: ^testing.T) { + priv_key: rsa.Private_Key + if !testing.expectf( + t, + rsa.private_key_set_insecure_test(&priv_key), + "rsa: failed to set test key", + ) { + return + } + + if !crypto.HAS_RAND_BYTES { + log.info("rand_bytes not supported - skipping generate") + return + } + + if !testing.expectf( + t, + rsa.private_key_generate(&priv_key), + "rsa: failed to generate private key", + ) { + return + } + + log.debugf("n=0x%s", hex.encode(priv_key._pub_key._n.v[:priv_key._pub_key._n.v_len], context.temp_allocator)) + log.debugf("e=%d", priv_key._pub_key._e) + log.debugf("d=0x%s", hex.encode(priv_key._d.v[:priv_key._d.v_len], context.temp_allocator)) + log.debugf("p=0x%s", hex.encode(priv_key._p.v[:priv_key._p.v_len], context.temp_allocator)) + log.debugf("q=0x%s", hex.encode(priv_key._q.v[:priv_key._q.v_len], context.temp_allocator)) + log.debugf("dp=0x%s", hex.encode(priv_key._dp.v[:priv_key._dp.v_len], context.temp_allocator)) + log.debugf("dq=0x%s", hex.encode(priv_key._dq.v[:priv_key._dq.v_len], context.temp_allocator)) + log.debugf("iq=0x%s", hex.encode(priv_key._iq.v[:priv_key._iq.v_len], context.temp_allocator)) + + pub_key: rsa.Public_Key + rsa.public_key_set_priv(&pub_key, &priv_key) + + if !testing.expectf( + t, + rsa.public_key_equal(&priv_key._pub_key, &pub_key), + "rsa: failed clone public key", + ) { + return + } +} + +@(test) +test_rsa_sign_pkcs1 :: proc(t: ^testing.T) { + test_msg := transmute([]byte)(TEST_MSG) + + priv_key: rsa.Private_Key + _ = rsa.private_key_set_insecure_test(&priv_key) + + pub_key: rsa.Public_Key + rsa.public_key_set_priv(&pub_key, &priv_key) + + // Generated with Go's crypto code. Signatures are + // deterministic. + expected_sig := []byte{ + 0x65, 0x0e, 0xa9, 0xd1, 0x66, 0xb3, 0x61, 0x4c, + 0x67, 0x8f, 0xd6, 0x9d, 0x2e, 0xc9, 0x24, 0xdd, + 0xcc, 0xa1, 0x0b, 0xdc, 0xbb, 0x35, 0x60, 0xc5, + 0x03, 0xb1, 0xa7, 0x10, 0x64, 0x53, 0x83, 0x02, + 0x7a, 0x8b, 0xc2, 0x83, 0x7f, 0xd6, 0xfc, 0xc3, + 0xe1, 0x4d, 0x33, 0x57, 0x90, 0x81, 0x52, 0xea, + 0xcd, 0x7c, 0xaa, 0xa5, 0x98, 0x59, 0x90, 0xd1, + 0x88, 0x21, 0x87, 0xc2, 0x9d, 0x51, 0x4d, 0x45, + 0x18, 0x06, 0xa2, 0xde, 0x7a, 0xc9, 0xc9, 0x1b, + 0x3d, 0x27, 0x07, 0xe4, 0xad, 0x46, 0xe8, 0x09, + 0xe1, 0xd5, 0xbd, 0x33, 0x1a, 0x9c, 0x7f, 0x3e, + 0x1b, 0x22, 0xc0, 0xfa, 0xa1, 0x30, 0xfb, 0xda, + 0x3b, 0x09, 0x9f, 0x6f, 0x44, 0x0e, 0xa8, 0x9b, + 0x65, 0x36, 0x48, 0x07, 0x95, 0xfc, 0xf7, 0x2b, + 0x61, 0xa0, 0x95, 0x36, 0x94, 0x66, 0x01, 0x90, + 0x5f, 0xf6, 0x72, 0x05, 0x64, 0x0c, 0x50, 0x8a, + 0xce, 0xfc, 0x75, 0xc7, 0x1e, 0x05, 0x62, 0xd9, + 0x8e, 0xdf, 0x85, 0x7e, 0x5d, 0x84, 0x8c, 0x0b, + 0x18, 0x88, 0x71, 0x4a, 0x94, 0x11, 0x5e, 0x16, + 0x53, 0xd0, 0xdc, 0x8e, 0x62, 0x73, 0x9d, 0x94, + 0x66, 0x1a, 0xdf, 0xf4, 0x01, 0xaa, 0xa5, 0x33, + 0xb3, 0x0c, 0x14, 0x53, 0x5d, 0xe7, 0xdb, 0x66, + 0xf0, 0x3e, 0xf0, 0x08, 0x81, 0x77, 0x40, 0x79, + 0xa5, 0xc6, 0x48, 0x68, 0xcc, 0xa2, 0xb6, 0xf3, + 0x1f, 0xf9, 0xd5, 0xb5, 0x26, 0x04, 0x97, 0xbc, + 0x93, 0xa3, 0x19, 0x29, 0x26, 0x86, 0x89, 0xf8, + 0x11, 0xe5, 0xdd, 0xbf, 0x35, 0x6b, 0x96, 0x25, + 0xa5, 0x78, 0xab, 0x02, 0x69, 0xdf, 0x54, 0x95, + 0xc5, 0x6b, 0x95, 0xcc, 0x38, 0x7b, 0x11, 0xfa, + 0x8c, 0x98, 0xa9, 0x95, 0x7e, 0x39, 0x7c, 0xf5, + 0x2f, 0xea, 0x42, 0x6d, 0xf5, 0xaa, 0xb3, 0x16, + 0x10, 0x2f, 0x29, 0xf3, 0x4a, 0xbc, 0x47, 0xfb, + } + + // Note: The key generate/set routines use the prehashed + // mode. + sig: [2048 >> 3]byte + ok := rsa.sign_pkcs1( + &priv_key, + .SHA256, + test_msg, + sig[:], + ) + if !testing.expectf(t, ok, "rsa/pkcs1: signing failed") { + return + } + + if !testing.expectf( + t, + bytes.equal(sig[:], expected_sig), + "rsa/pkcs1: signature mismatch: %x (expected %x)", + sig[:], + expected_sig, + ) { + return + } + + ok = rsa.verify_pkcs1( + &pub_key, + .SHA256, + test_msg, + expected_sig, + ) + if !testing.expectf(t, ok, "rsa/pkcs1: verify failed") { + return + } +} + +@(test) +test_rsa_sign_pss :: proc(t: ^testing.T) { + test_msg := transmute([]byte)(TEST_MSG) + + priv_key: rsa.Private_Key + _ = rsa.private_key_set_insecure_test(&priv_key) + + pub_key: rsa.Public_Key + rsa.public_key_set_priv(&pub_key, &priv_key) + + // Generated with Go's crypto code. Signatures are + // NOT deterministic. + test_sig := []byte{ + 0x46, 0xd3, 0x7e, 0xb2, 0x2c, 0x82, 0xe0, 0x68, + 0x80, 0x58, 0x8c, 0x07, 0x0e, 0xe9, 0x41, 0x3f, + 0xde, 0x45, 0x4a, 0xa0, 0x3f, 0xff, 0xa3, 0xa8, + 0xc2, 0x76, 0x74, 0xf0, 0xe8, 0x44, 0x07, 0x2d, + 0xdb, 0x12, 0xd5, 0x57, 0xcc, 0x28, 0x15, 0xfa, + 0xeb, 0xb6, 0x14, 0x76, 0x10, 0x3f, 0xfc, 0xba, + 0xb1, 0x6e, 0x7f, 0x65, 0x6c, 0x9b, 0x1a, 0x62, + 0x60, 0xc4, 0xfa, 0xb0, 0x03, 0x07, 0x2b, 0x6b, + 0x6a, 0x5a, 0x84, 0x10, 0xe4, 0xf5, 0x64, 0xad, + 0xfa, 0xd3, 0x5f, 0xc9, 0xf4, 0xac, 0x70, 0xde, + 0x54, 0x06, 0x14, 0x59, 0xf7, 0x60, 0x15, 0xc9, + 0xa2, 0xe2, 0x54, 0xbc, 0x79, 0xa8, 0x02, 0x72, + 0xba, 0x6a, 0x68, 0x08, 0x15, 0x6b, 0xcb, 0xc8, + 0x55, 0x83, 0x63, 0x06, 0xe4, 0x28, 0x36, 0x6b, + 0xe6, 0x15, 0x2a, 0x21, 0xc9, 0x4f, 0xc0, 0x0e, + 0x30, 0x54, 0x3b, 0xf1, 0xa4, 0x79, 0x83, 0xb9, + 0x02, 0xd9, 0x2e, 0xa1, 0x31, 0xf4, 0x5b, 0x1b, + 0xbe, 0x44, 0x17, 0xd8, 0x42, 0x6d, 0xb3, 0x38, + 0x3f, 0x23, 0x82, 0x9e, 0x76, 0x52, 0x0c, 0x5e, + 0xa0, 0xcd, 0xd1, 0xcc, 0xe2, 0x5b, 0x71, 0xb5, + 0xca, 0x28, 0x33, 0xc3, 0x03, 0x30, 0xc5, 0xa6, + 0xdc, 0x6e, 0xfd, 0xd7, 0x34, 0x0a, 0xd2, 0x30, + 0x2c, 0x80, 0x2d, 0x31, 0xea, 0xe9, 0x44, 0x2a, + 0x7e, 0x1d, 0xde, 0x71, 0xa3, 0x7e, 0xe2, 0x5e, + 0x8a, 0x91, 0x61, 0x23, 0x5b, 0x26, 0x6d, 0x3b, + 0x44, 0xfc, 0x6b, 0x36, 0x40, 0xb1, 0xdb, 0xe7, + 0xf9, 0xe7, 0x8a, 0x12, 0x7c, 0xba, 0xd8, 0x33, + 0x1b, 0xac, 0x70, 0x59, 0x24, 0x83, 0x3a, 0x8b, + 0x2a, 0x51, 0x1b, 0x97, 0xa3, 0x8e, 0x34, 0xd1, + 0xbb, 0xc1, 0x4e, 0x00, 0xab, 0x21, 0x12, 0x53, + 0xeb, 0xda, 0x36, 0x77, 0x30, 0xea, 0x82, 0x92, + 0xb5, 0xfb, 0x07, 0xb1, 0x34, 0x37, 0x78, 0x69, + } + + ok := rsa.verify_pss( + &pub_key, + .SHA256, + 32, + test_msg, + test_sig, + ) + if !testing.expectf(t, ok, "rsa/pss: verify (pregenerated) failed") { + return + } + + if !crypto.HAS_RAND_BYTES { + log.info("rand_bytes not supported - skipping round trip tests") + return + } + + // Just do a simple round-trip test, as the failure + // cases are covered by wycheproof, and de-randomizing + // PSS is an API nightmare. + + sig: [2048 >> 3]byte + ok = rsa.sign_pss( + &priv_key, + .SHA256, + 32, + TEST_MSG_SHA256, + sig[:], + true, + ) + if !testing.expectf(t, ok, "rsa/pss: signing failed") { + return + } + + ok = rsa.verify_pss( + &pub_key, + .SHA256, + 32, + TEST_MSG_SHA256, + sig[:], + true, + ) + if !testing.expectf(t, ok, "rsa/pss: verify failed") { + return + } +} + +@(test) +test_rsa_enc_dec_oaep :: proc(t: ^testing.T) { + test_msg := transmute([]byte)(TEST_MSG) + + priv_key: rsa.Private_Key + _ = rsa.private_key_set_insecure_test(&priv_key) + + pub_key: rsa.Public_Key + rsa.public_key_set_priv(&pub_key, &priv_key) + + // Generated with Go's crypto code. Ciphertexts are + // NOT deterministic. + test_ciphertext := []byte{ + 0x51, 0x7e, 0x55, 0xe8, 0xf8, 0x69, 0x9e, 0x68, + 0x8e, 0x2f, 0x38, 0xec, 0x11, 0xfb, 0x5f, 0x1e, + 0xf1, 0x9b, 0x2c, 0xea, 0x8a, 0xfb, 0x13, 0x04, + 0x2a, 0xbd, 0x4f, 0x69, 0xca, 0x11, 0x31, 0x1b, + 0xb0, 0x00, 0x18, 0x69, 0x32, 0x88, 0xe3, 0x07, + 0x82, 0xe8, 0x1d, 0x34, 0x09, 0x26, 0xdf, 0x41, + 0x1c, 0xc3, 0xf1, 0x39, 0x31, 0x45, 0xb6, 0x67, + 0xa5, 0x7c, 0xa4, 0xaf, 0x48, 0x5e, 0x96, 0x26, + 0x9b, 0x78, 0x76, 0x4e, 0xd2, 0x6e, 0x53, 0xd0, + 0x51, 0xc9, 0x80, 0x71, 0xea, 0x67, 0x8a, 0x44, + 0x1e, 0xb0, 0x81, 0x2e, 0xce, 0x43, 0x9a, 0xd9, + 0x1c, 0xea, 0x5c, 0x8b, 0x94, 0x3e, 0x1e, 0x5c, + 0xb0, 0x17, 0xb9, 0x50, 0x44, 0x22, 0xc9, 0x17, + 0xd0, 0x73, 0x54, 0x2b, 0x15, 0x68, 0xe2, 0xcf, + 0xbe, 0x0b, 0xef, 0x91, 0x11, 0xfc, 0xa6, 0x78, + 0x14, 0xdd, 0x62, 0xf3, 0xba, 0x8c, 0x8d, 0x4b, + 0x7f, 0x4b, 0xfa, 0x8b, 0x9c, 0x91, 0x08, 0x9f, + 0x39, 0x47, 0x27, 0xba, 0x9a, 0xfd, 0x2a, 0xb8, + 0x1e, 0x70, 0xa3, 0x9c, 0xe1, 0x23, 0x21, 0xc5, + 0xca, 0x00, 0x2a, 0x9b, 0x23, 0x0f, 0x15, 0xe2, + 0x9a, 0x62, 0xc2, 0x20, 0xb6, 0xe8, 0x85, 0x5f, + 0x94, 0xba, 0x72, 0x06, 0x55, 0xcf, 0x5a, 0xd6, + 0xc6, 0xc0, 0x89, 0xff, 0xd3, 0x72, 0xf9, 0x34, + 0x7a, 0x12, 0xfc, 0xe3, 0x74, 0x64, 0x00, 0xfe, + 0xa1, 0x35, 0x78, 0x66, 0x56, 0x1a, 0xde, 0x6a, + 0x83, 0x6b, 0x20, 0x06, 0xe2, 0x51, 0xae, 0xc7, + 0x27, 0x44, 0x5b, 0x21, 0x4f, 0xdf, 0xf6, 0x52, + 0x8e, 0x3a, 0x84, 0x07, 0x26, 0xc8, 0xe3, 0x6a, + 0x18, 0xd4, 0x49, 0x44, 0xd8, 0x24, 0x08, 0x94, + 0xe1, 0x67, 0xde, 0x4a, 0x8e, 0x6a, 0x2a, 0x28, + 0x72, 0x0e, 0x68, 0x9c, 0x7f, 0x55, 0x13, 0x54, + 0x13, 0x32, 0xdb, 0xe7, 0x31, 0x84, 0x90, 0xaf, + } + + buf: [2048 >> 3]byte + + derived_msg, ok := rsa.decrypt_oaep( + &priv_key, + .SHA256, + test_ciphertext, + buf[:], + ) + if !testing.expectf(t, ok, "rsa/oaep: decryption (pregenerated) failed") { + return + } + if !testing.expectf( + t, + bytes.equal(test_msg, derived_msg), + "rsa/oaep: unexpected plaintext: %x", + derived_msg, + ) { + return + } + + if !crypto.HAS_RAND_BYTES { + log.info("rand_bytes not supported - skipping") + return + } + + // Just do a simple round-trip test, as the failure + // cases are covered by wycheproof, and de-randomizing + // OAEP is an API nightmare. + + ok = rsa.encrypt_oaep( + &pub_key, + .SHA512, + test_msg, + buf[:], + ) + if !testing.expectf(t, ok, "rsa/oaep: encryption failed") { + return + } + + derived_msg, ok = rsa.decrypt_oaep( + &priv_key, + .SHA512, + buf[:], + buf[:], + ) + if !testing.expectf(t, ok, "rsa/oaep: decryption failed") { + return + } + if !testing.expectf( + t, + bytes.equal(test_msg, derived_msg), + "rsa/oaep: unexpected plaintext: %x", + derived_msg, + ) { + return + } +} + +@(test) +test_rsa_dec_pms :: proc(t: ^testing.T) { + priv_key: rsa.Private_Key + _ = rsa.private_key_set_insecure_test(&priv_key) + + // Generated with Go's crypto code. Go's PKCS1v15 + // encryption output is NOT deterministic. + test_ciphertext := []byte{ + 0x0c, 0x18, 0x48, 0x23, 0x0b, 0x4a, 0xa9, 0x20, + 0xbb, 0xa6, 0x38, 0xbf, 0x25, 0xda, 0x16, 0xd7, + 0x92, 0x72, 0x3a, 0x81, 0xbc, 0xee, 0x74, 0x6b, + 0xd8, 0x55, 0x26, 0x72, 0x81, 0x70, 0x73, 0x32, + 0xd4, 0x30, 0x56, 0xe2, 0xeb, 0x1b, 0x57, 0xf3, + 0xf5, 0x7d, 0xea, 0x3e, 0x3c, 0x62, 0x35, 0x78, + 0x06, 0x88, 0x27, 0x4d, 0x6e, 0xa0, 0x45, 0x91, + 0x62, 0x8c, 0xe9, 0x93, 0xd0, 0xde, 0x85, 0x71, + 0x01, 0x24, 0x7a, 0x1a, 0x5d, 0x03, 0x49, 0x92, + 0x73, 0x5a, 0x5b, 0x1e, 0x1e, 0x3b, 0xf0, 0xf2, + 0xea, 0x04, 0x9a, 0x87, 0x32, 0x20, 0x52, 0xbd, + 0x72, 0xc8, 0xa8, 0x38, 0xd1, 0x29, 0x97, 0x87, + 0x0b, 0x76, 0xc1, 0x68, 0xe5, 0x05, 0x72, 0xb4, + 0x4d, 0xd1, 0x95, 0xb2, 0xa8, 0x19, 0x3f, 0xc3, + 0x1e, 0xee, 0x34, 0x19, 0x72, 0xac, 0x1e, 0x4b, + 0x01, 0xd7, 0x60, 0xeb, 0x27, 0xf1, 0x12, 0x7c, + 0xbc, 0x07, 0x5d, 0xf6, 0xb9, 0xac, 0xf9, 0xdb, + 0x1a, 0xaf, 0x47, 0x13, 0x22, 0x16, 0xb5, 0x05, + 0x5a, 0x9c, 0x45, 0x69, 0x0a, 0xf1, 0x36, 0x6b, + 0xab, 0x96, 0xd7, 0x7f, 0x66, 0xff, 0x16, 0x3c, + 0x29, 0xc4, 0x10, 0x03, 0xe1, 0x35, 0xcc, 0xae, + 0x71, 0x08, 0x14, 0xff, 0x57, 0x4f, 0x3d, 0x79, + 0x7a, 0xa7, 0x19, 0x2e, 0x23, 0x08, 0xad, 0xb2, + 0xe5, 0xb1, 0xe8, 0x47, 0x6d, 0xe1, 0x24, 0x4a, + 0xd8, 0x1f, 0xe4, 0x52, 0x21, 0x3e, 0xf1, 0xcd, + 0x07, 0x72, 0xa4, 0xb8, 0x06, 0x98, 0x3a, 0x17, + 0xfd, 0xca, 0x74, 0x93, 0xb1, 0x2b, 0xd8, 0x76, + 0x7c, 0x6f, 0x71, 0xfc, 0x16, 0xef, 0x99, 0xa1, + 0xf9, 0x13, 0xeb, 0xfc, 0x34, 0x90, 0xb5, 0x00, + 0xbf, 0xdc, 0x19, 0x99, 0xb4, 0x12, 0x85, 0x25, + 0x3a, 0x49, 0x70, 0x63, 0x2f, 0xfc, 0xbc, 0xca, + 0x38, 0x2f, 0x7a, 0x0e, 0x78, 0x8d, 0x7b, 0x87, + } + + ok := rsa.unsafe_decrypt_tls_pms(&priv_key, test_ciphertext) + if !testing.expectf( + t, + ok == 1, + "rsa/tls_pms: decryption failed: %x", + test_ciphertext[:48], + ) { + return + } + + if !testing.expectf( + t, + bytes.equal(test_ciphertext[:48], TEST_TLS_PMS), + "rsa/tls_pms: unexpected plaintext: %x", + test_ciphertext[:48], + ) { + return + } +} diff --git a/tests/core/crypto/wycheproof/main.odin b/tests/core/crypto/wycheproof/main.odin index bfb4884cd..3c7e765e2 100644 --- a/tests/core/crypto/wycheproof/main.odin +++ b/tests/core/crypto/wycheproof/main.odin @@ -51,6 +51,45 @@ import "core:testing" // - pbkdf2_hmacsha256_test.json // - pbkdf2_hmacsha384_test.json // - pbkdf2_hmacsha512_test.json +// - crypto/rsa +// - rsa_pkcs1_1024_sig_gen_test.json +// - rsa_pkcs1_1536_sig_gen_test.json +// - rsa_pkcs1_2048_sig_gen_test.json +// - rsa_pkcs1_3072_sig_gen_test.json +// - rsa_pkcs1_4096_sig_gen_test.json +// - rsa_pss_2048_sha1_mgf1_20_test.json +// - rsa_pss_2048_sha256_mgf1_0_test.json +// - rsa_pss_2048_sha256_mgf1_32_test.json +// - rsa_pss_2048_sha256_mgf1sha1_20_test.json +// - rsa_pss_2048_sha384_mgf1_48_test.json +// - rsa_pss_2048_sha512_256_mgf1_32_test.json +// - rsa_pss_3072_sha256_mgf1_32_test.json +// - rsa_pss_4096_sha256_mgf1_32_test.json +// - rsa_pss_4096_sha384_mgf1_48_test.json +// - rsa_pss_4096_sha512_mgf1_32_test.json +// - rsa_pss_4096_sha512_mgf1_64_test.json +// - rsa_pss_misc_test.json +// - rsa_oaep_2048_sha1_mgf1sha1_test.json +// - rsa_oaep_2048_sha224_mgf1sha1_test.json +// - rsa_oaep_2048_sha224_mgf1sha224_test.json +// - rsa_oaep_2048_sha256_mgf1sha1_test.json +// - rsa_oaep_2048_sha256_mgf1sha256_test.json +// - rsa_oaep_2048_sha384_mgf1sha1_test.json +// - rsa_oaep_2048_sha384_mgf1sha384_test.json +// - rsa_oaep_2048_sha512_224_mgf1sha1_test.json +// - rsa_oaep_2048_sha512_mgf1sha1_test.json +// - rsa_oaep_2048_sha512_mgf1sha512_test.json +// - rsa_oaep_3072_sha256_mgf1sha1_test.json +// - rsa_oaep_3072_sha256_mgf1sha256_test.json +// - rsa_oaep_3072_sha512_256_mgf1sha1_test.json +// - rsa_oaep_3072_sha512_256_mgf1sha512_256_test.json +// - rsa_oaep_3072_sha512_mgf1sha1_test.json +// - rsa_oaep_3072_sha512_mgf1sha512_test.json +// - rsa_oaep_4096_sha256_mgf1sha1_test.json +// - rsa_oaep_4096_sha256_mgf1sha256_test.json +// - rsa_oaep_4096_sha512_mgf1sha1_test.json +// - rsa_oaep_4096_sha512_mgf1sha512_test.json +// - rsa_oaep_misc_test.json // - crypto/siphash // - siphash_1_3_test.json // - siphash_2_4_test.json diff --git a/tests/core/crypto/wycheproof/rsa.odin b/tests/core/crypto/wycheproof/rsa.odin new file mode 100644 index 000000000..00561c407 --- /dev/null +++ b/tests/core/crypto/wycheproof/rsa.odin @@ -0,0 +1,541 @@ +package test_wycheproof + +import "core:encoding/hex" +import "core:fmt" +import "core:log" +import "core:mem" +import "core:os" +import "core:testing" + +import "core:crypto/rsa" + +import "../common" + +@(test) +test_rsa_pkcs1_signature :: proc(t: ^testing.T) { + arena: mem.Arena + arena_backing := make([]byte, ARENA_SIZE) + defer delete(arena_backing) + mem.arena_init(&arena, arena_backing) + context.allocator = mem.arena_allocator(&arena) + + log.debug("rsa/pkcs1/signatures: starting") + + files := []string { + "rsa_pkcs1_1024_sig_gen_test.json", + "rsa_pkcs1_1536_sig_gen_test.json", + "rsa_pkcs1_2048_sig_gen_test.json", + "rsa_pkcs1_3072_sig_gen_test.json", + "rsa_pkcs1_4096_sig_gen_test.json", + } + for f in files { + mem.free_all() + + fn, _ := os.join_path([]string{BASE_PATH, f}, context.allocator) + + test_vectors: Test_Vectors(Rsa_Pkcs1_Sig_Test_Group) + load_ok := load(&test_vectors, fn) + if !testing.expectf(t, load_ok, "Unable to load {}", f) { + continue + } + + testing.expectf(t, test_rsa_pkcs1_sig(t, &test_vectors), "RSA PKCS1 signature failed") + } +} + +test_rsa_pkcs1_sig :: proc(t: ^testing.T, test_vectors: ^Test_Vectors(Rsa_Pkcs1_Sig_Test_Group)) -> bool { + JWK_KTY :: "RSA" + + params_str := fmt.aprintf("RSA-%d/PKCS1/Signature", test_vectors.test_groups[0].key_size) + log.debugf("%s: starting", params_str) + + num_ran, num_passed, num_failed, num_skipped: int + for &test_group, tg_id in test_vectors.test_groups { + hash_str := test_group.sha + hash_alg, _ := hash_name_to_algorithm(hash_str) + if hash_alg == .Invalid { + log.infof("%s: unsupported hash: %s", params_str, hash_str) + num_ran += len(test_group.tests) + num_skipped += len(test_group.tests) + continue + } + + priv_key: rsa.Private_Key + pub_key := &priv_key._pub_key + + ok, have_priv: bool + if test_group.private_key_jwk.kty == JWK_KTY { + ok = jwk_to_private_key(&test_group.private_key_jwk, &priv_key) + have_priv = true + } else { + ok = rsa.public_key_set_bytes( + pub_key, + common.hexbytes_decode(test_group.private_key.modulus), + common.hexbytes_decode(test_group.private_key.public_exponent), + ) + } + + if !testing.expectf(t, ok, "%s/%d: invalid RSA key: %v", params_str, tg_id, test_group.private_key) { + num_ran += len(test_group.tests) + num_failed += len(test_group.tests) + continue + } + + for &test_vector in test_group.tests { + num_ran += 1 + + if comment := test_vector.comment; comment != "" { + log.debugf( + "%s/%s/%d/%d: %s: %+v", + params_str, + hash_str, + tg_id, + test_vector.tc_id, + comment, + test_vector.flags, + ) + } else { + log.debugf("%s/%s/%d/%d: %+v", params_str, hash_str, tg_id, test_vector.tc_id, test_vector.flags) + } + + msg := common.hexbytes_decode(test_vector.msg) + sig := common.hexbytes_decode(test_vector.sig) + + verify_ok := rsa.verify_pkcs1(pub_key, hash_alg, msg, sig) + if !testing.expectf( + t, + result_check(test_vector.result, verify_ok, false), + "%s/%s/%d/%d: verify failed: expected %s actual %v", + params_str, + hash_str, + tg_id, + test_vector.tc_id, + test_vector.result, + verify_ok, + ) { + num_failed += 1 + continue + } + + if have_priv && verify_ok { + sign_ok := rsa.sign_pkcs1(&priv_key, hash_alg, msg, sig) + if !testing.expectf( + t, + sign_ok, + "%s/%s/%d/%d: sign failed", + params_str, + hash_str, + tg_id, + test_vector.tc_id, + ) { + num_failed += 1 + continue + } + if !testing.expectf( + t, + common.hexbytes_compare(test_vector.sig, sig), + "%s/%s/%d/%d: sign failed: expected %s actual %s", + params_str, + hash_str, + tg_id, + test_vector.tc_id, + test_vector.sig, + hex.encode(sig), + ) { + num_failed += 1 + continue + } + } + + num_passed += 1 + } + } + + assert(num_ran == test_vectors.number_of_tests) + assert(num_passed + num_failed + num_skipped == num_ran) + + log.infof( + "%s: ran %d, passed %d, failed %d, skipped %d", + params_str, + num_ran, + num_passed, + num_failed, + num_skipped, + ) + + return num_failed == 0 +} + +@(test) +test_rsa_pss_signature :: proc(t: ^testing.T) { + arena: mem.Arena + arena_backing := make([]byte, ARENA_SIZE) + defer delete(arena_backing) + mem.arena_init(&arena, arena_backing) + context.allocator = mem.arena_allocator(&arena) + + log.debug("rsa/pss/signatures: starting") + + files := []string { + "rsa_pss_2048_sha1_mgf1_20_test.json", + "rsa_pss_2048_sha256_mgf1_0_test.json", + "rsa_pss_2048_sha256_mgf1_32_test.json", + "rsa_pss_2048_sha256_mgf1sha1_20_test.json", + "rsa_pss_2048_sha384_mgf1_48_test.json", + "rsa_pss_2048_sha512_256_mgf1_32_test.json", + "rsa_pss_3072_sha256_mgf1_32_test.json", + "rsa_pss_4096_sha256_mgf1_32_test.json", + "rsa_pss_4096_sha384_mgf1_48_test.json", + "rsa_pss_4096_sha512_mgf1_32_test.json", + "rsa_pss_4096_sha512_mgf1_64_test.json", + "rsa_pss_misc_test.json", + + // These tests include the MGF1 parameters in the public key, + // which we do not support with our existing API. + // + // "rsa_pss_2048_sha1_mgf1_20_params_test.json", + // "rsa_pss_2048_sha256_mgf1_0_params_test.json", + // "rsa_pss_2048_sha256_mgf1_32_params_test.json", + // "rsa_pss_2048_sha512_mgf1sha256_32_params_test.json", + // "rsa_pss_3072_sha256_mgf1_32_params_test.json", + // "rsa_pss_4096_sha512_mgf1_32_params_test.json", + // "rsa_pss_4096_sha512_mgf1_64_params_test.json", + // "rsa_pss_misc_params_test.json", + + // Unsupported hash algorithm: + // "rsa_pss_2048_sha512_224_mgf1_28_test.json", + } + for f in files { + mem.free_all() + + fn, _ := os.join_path([]string{BASE_PATH, f}, context.allocator) + + test_vectors: Test_Vectors(Rsa_Pss_Sig_Test_Group) + load_ok := load(&test_vectors, fn) + if !testing.expectf(t, load_ok, "Unable to load {}", f) { + continue + } + + testing.expectf(t, test_rsa_pss_sig(t, &test_vectors), "RSA PSS signature failed") + } +} + +test_rsa_pss_sig :: proc(t: ^testing.T, test_vectors: ^Test_Vectors(Rsa_Pss_Sig_Test_Group)) -> bool { + MGF1 :: "MGF1" + + params_str := fmt.aprintf("RSA-%d/PSS/Signature", test_vectors.test_groups[0].key_size) + log.debugf("%s: starting", params_str) + + num_ran, num_passed, num_failed, num_skipped: int + for &test_group, tg_id in test_vectors.test_groups { + if test_group.mgf != MGF1 { + log.infof("%s: unsupported MGF: %s", params_str, test_group.mgf) + num_ran += len(test_group.tests) + num_skipped += len(test_group.tests) + continue + } + + hash_str := test_group.sha + hash_alg, _ := hash_name_to_algorithm(hash_str) + if hash_alg == .Invalid { + log.infof("%s: unsupported hash: %s", params_str, hash_str) + num_ran += len(test_group.tests) + num_skipped += len(test_group.tests) + continue + } + + mgf_hash_str := test_group.mfg_sha + mgf_hash_alg, _ := hash_name_to_algorithm(mgf_hash_str) + if mgf_hash_alg == .Invalid { + log.infof("%s: unsupported MGF hash: %s", params_str, mgf_hash_str) + num_ran += len(test_group.tests) + num_skipped += len(test_group.tests) + continue + } + + hash_params_str := fmt.aprintf("%s/%s(%s)", hash_str, test_group.mgf, mgf_hash_str) + + pub_key: rsa.Public_Key + ok := rsa.public_key_set_bytes( + &pub_key, + common.hexbytes_decode(test_group.public_key.modulus), + common.hexbytes_decode(test_group.public_key.public_exponent), + ) + if !testing.expectf(t, ok, "%s/%d: invalid RSA key: %v", params_str, tg_id, test_group.public_key) { + num_ran += len(test_group.tests) + num_failed += len(test_group.tests) + continue + } + + for &test_vector in test_group.tests { + num_ran += 1 + + if comment := test_vector.comment; comment != "" { + log.debugf( + "%s/%s/%d/%d: %s: %+v", + params_str, + hash_params_str, + tg_id, + test_vector.tc_id, + comment, + test_vector.flags, + ) + } else { + log.debugf("%s/%s/%d/%d: %+v", params_str, hash_str, tg_id, test_vector.tc_id, test_vector.flags) + } + + msg := common.hexbytes_decode(test_vector.msg) + sig := common.hexbytes_decode(test_vector.sig) + + verify_ok := rsa.verify_pss( + &pub_key, + hash_alg, + test_group.s_len, + msg, + sig, + false, + mgf_hash_alg, + ) + if !testing.expectf( + t, + result_check(test_vector.result, verify_ok, false), + "%s/%s/%d/%d: verify failed: expected %s actual %v", + params_str, + hash_params_str, + tg_id, + test_vector.tc_id, + test_vector.result, + verify_ok, + ) { + num_failed += 1 + continue + } + + num_passed += 1 + } + } + + assert(num_ran == test_vectors.number_of_tests) + assert(num_passed + num_failed + num_skipped == num_ran) + + log.infof( + "%s: ran %d, passed %d, failed %d, skipped %d", + params_str, + num_ran, + num_passed, + num_failed, + num_skipped, + ) + + return num_failed == 0 +} + +@(test) +test_rsa_oaep_decryption :: proc(t: ^testing.T) { + arena: mem.Arena + arena_backing := make([]byte, ARENA_SIZE) + defer delete(arena_backing) + mem.arena_init(&arena, arena_backing) + context.allocator = mem.arena_allocator(&arena) + + log.debug("rsa/oaep/decryption: starting") + + files := []string { + "rsa_oaep_2048_sha1_mgf1sha1_test.json", + "rsa_oaep_2048_sha224_mgf1sha1_test.json", + "rsa_oaep_2048_sha224_mgf1sha224_test.json", + "rsa_oaep_2048_sha256_mgf1sha1_test.json", + "rsa_oaep_2048_sha256_mgf1sha256_test.json", + "rsa_oaep_2048_sha384_mgf1sha1_test.json", + "rsa_oaep_2048_sha384_mgf1sha384_test.json", + "rsa_oaep_2048_sha512_224_mgf1sha1_test.json", + "rsa_oaep_2048_sha512_mgf1sha1_test.json", + "rsa_oaep_2048_sha512_mgf1sha512_test.json", + "rsa_oaep_3072_sha256_mgf1sha1_test.json", + "rsa_oaep_3072_sha256_mgf1sha256_test.json", + "rsa_oaep_3072_sha512_256_mgf1sha1_test.json", + "rsa_oaep_3072_sha512_256_mgf1sha512_256_test.json", + "rsa_oaep_3072_sha512_mgf1sha1_test.json", + "rsa_oaep_3072_sha512_mgf1sha512_test.json", + "rsa_oaep_4096_sha256_mgf1sha1_test.json", + "rsa_oaep_4096_sha256_mgf1sha256_test.json", + "rsa_oaep_4096_sha512_mgf1sha1_test.json", + "rsa_oaep_4096_sha512_mgf1sha512_test.json", + "rsa_oaep_misc_test.json", + + // Unsupported hash algorithm: + // "rsa_oaep_2048_sha512_224_mgf1sha512_224_test.json", + } + for f in files { + mem.free_all() + + fn, _ := os.join_path([]string{BASE_PATH, f}, context.allocator) + + test_vectors: Test_Vectors(Rsa_Oaep_Dec_Test_Group) + load_ok := load(&test_vectors, fn) + if !testing.expectf(t, load_ok, "Unable to load {}", f) { + continue + } + + testing.expectf(t, test_rsa_oaep_dec(t, &test_vectors), "RSA OAEP decryption failed") + } +} + +test_rsa_oaep_dec :: proc(t: ^testing.T, test_vectors: ^Test_Vectors(Rsa_Oaep_Dec_Test_Group)) -> bool { + MGF1 :: "MGF1" + + params_str := fmt.aprintf("RSA-%d/OAEP/Decryption", test_vectors.test_groups[0].key_size) + log.debugf("%s: starting", params_str) + + num_ran, num_passed, num_failed, num_skipped: int + for &test_group, tg_id in test_vectors.test_groups { + if test_group.key_size > rsa.MODULUS_MAX_SIZE { + log.infof("%s: unsupported key size: %d", params_str, test_group.key_size) + num_ran += len(test_group.tests) + num_skipped += len(test_group.tests) + continue + } + if test_group.mgf != MGF1 { + log.infof("%s: unsupported MGF: %s", params_str, test_group.mgf) + num_ran += len(test_group.tests) + num_skipped += len(test_group.tests) + continue + } + + hash_str := test_group.sha + hash_alg, _ := hash_name_to_algorithm(hash_str) + if hash_alg == .Invalid { + log.infof("%s: unsupported hash: %s", params_str, hash_str) + num_ran += len(test_group.tests) + num_skipped += len(test_group.tests) + continue + } + + mgf_hash_str := test_group.mfg_sha + mgf_hash_alg, _ := hash_name_to_algorithm(mgf_hash_str) + if mgf_hash_alg == .Invalid { + log.infof("%s: unsupported MGF hash: %s", params_str, mgf_hash_str) + num_ran += len(test_group.tests) + num_skipped += len(test_group.tests) + continue + } + + hash_params_str := fmt.aprintf("%s/%s(%s)", hash_str, test_group.mgf, mgf_hash_str) + + priv_key: rsa.Private_Key + ok := json_to_private_key(&test_group.private_key, &priv_key) + if !testing.expectf(t, ok, "%s/%d: invalid RSA key: %v", params_str, tg_id, test_group.private_key) { + num_ran += len(test_group.tests) + num_failed += len(test_group.tests) + continue + } + + dst: [rsa.MODULUS_MAX_SIZE >> 3]byte = --- + for &test_vector in test_group.tests { + num_ran += 1 + + if comment := test_vector.comment; comment != "" { + log.debugf( + "%s/%s/%d/%d: %s: %+v", + params_str, + hash_params_str, + tg_id, + test_vector.tc_id, + comment, + test_vector.flags, + ) + } else { + log.debugf("%s/%s/%d/%d: %+v", params_str, hash_str, tg_id, test_vector.tc_id, test_vector.flags) + } + + label := common.hexbytes_decode(test_vector.label) + ct := common.hexbytes_decode(test_vector.ct) + + pt, decrypt_ok := rsa.decrypt_oaep( + &priv_key, + hash_alg, + ct, + dst[:], + label, + mgf_hash_alg, + ) + + if !testing.expectf( + t, + result_check(test_vector.result, decrypt_ok, false), + "%s/%s/%d/%d: decrypt failed: expected %s actual %v", + params_str, + hash_params_str, + tg_id, + test_vector.tc_id, + test_vector.result, + decrypt_ok, + ) { + num_failed += 1 + continue + } + if decrypt_ok { + if !testing.expectf( + t, + common.hexbytes_compare(test_vector.msg, pt), + "%s/%s/%d/%d: decrypt failed: expected %s actual %s", + params_str, + hash_str, + tg_id, + test_vector.tc_id, + test_vector.msg, + hex.encode(pt), + ) { + num_failed += 1 + continue + } + } + + num_passed += 1 + } + } + + assert(num_ran == test_vectors.number_of_tests) + assert(num_passed + num_failed + num_skipped == num_ran) + + log.infof( + "%s: ran %d, passed %d, failed %d, skipped %d", + params_str, + num_ran, + num_passed, + num_failed, + num_skipped, + ) + + return num_failed == 0 +} + +@(private="file") +jwk_to_private_key :: proc(jwk: ^Rsa_Jwk_Private_Key, priv_key: ^rsa.Private_Key) -> bool { + return rsa.private_key_set_bytes( + priv_key, + common.jwkbytes_decode(jwk.n), + common.jwkbytes_decode(jwk.e), + common.jwkbytes_decode(jwk.d), + common.jwkbytes_decode(jwk.p), + common.jwkbytes_decode(jwk.q), + common.jwkbytes_decode(jwk.dp), + common.jwkbytes_decode(jwk.dq), + common.jwkbytes_decode(jwk.qi), + ) +} + +@(private="file") +json_to_private_key :: proc(json: ^Rsa_Private_Key, priv_key: ^rsa.Private_Key) -> bool { + return rsa.private_key_set_bytes( + priv_key, + common.hexbytes_decode(json.modulus), + common.hexbytes_decode(json.public_exponent), + common.hexbytes_decode(json.private_exponent), + common.hexbytes_decode(json.prime1), + common.hexbytes_decode(json.prime2), + common.hexbytes_decode(json.exponent1), + common.hexbytes_decode(json.exponent2), + common.hexbytes_decode(json.coefficient), + ) +} \ No newline at end of file diff --git a/tests/core/crypto/wycheproof/schemas.odin b/tests/core/crypto/wycheproof/schemas.odin index 2e72a1098..39270a379 100644 --- a/tests/core/crypto/wycheproof/schemas.odin +++ b/tests/core/crypto/wycheproof/schemas.odin @@ -150,10 +150,10 @@ Eddsa_Key :: struct { } Eddsa_Jwk :: struct { - kid: string `json:"kid"`, - crv: string `json:"crv"`, - kty: string `json:"kty"`, - x: string `json:"x"`, + kid: string `json:"kid"`, + crv: string `json:"crv"`, + kty: string `json:"kty"`, + x: common.Jwk_Bytes `json:"x"`, } Ecdsa_Key :: struct { @@ -241,3 +241,96 @@ Mldsa_Test_Vector :: struct { result: Result `json:"result"`, flags: []string `json:"flags"`, } + +Rsa_Pkcs1_Sig_Test_Group :: struct { + private_key: Rsa_Private_Key `json:"privateKey"`, + key_asn: common.Hex_Bytes `json:"keyAsn"`, + key_der: common.Hex_Bytes `json:"keyDer"`, + key_pem: string `json:"keyPem"`, + key_size: int `json:"keySize"`, + private_key_jwk: Rsa_Jwk_Private_Key `json:"privateKeyJwk"`, + private_key_pem: string `json:"privateKeyPem"`, + private_key_pkcs8: common.Hex_Bytes `json:"privateKeyPkcs8"`, + sha: string `json:"sha"`, + type: string `json:"type"`, + source: Test_Group_Source `json:"source"`, + tests: []Rsa_Sig_Test_Vector `json:"tests"`, +} + +Rsa_Sig_Test_Vector :: struct { + tc_id: int `json:"tcId"`, + comment: string `json:"comment"`, + msg: common.Hex_Bytes `json:"msg"`, + sig: common.Hex_Bytes `json:"sig"`, + result: Result `json:"result"`, + flags: []string `json:"flags"`, +} + +Rsa_Pss_Sig_Test_Group :: struct { + public_key: Rsa_Public_Key `json:"publicKey"`, + public_key_asn: common.Hex_Bytes `json:"publicKeyAsn"`, + public_key_der: common.Hex_Bytes `json:"publicKeyDer"`, + public_key_pem: string `json:"publicKeyPem"`, + key_size: int `json:"keySize"`, + sha: string `json:"sha"`, + mgf: string `json:"mgf"`, + mfg_sha: string `json:"mgfSha"`, + s_len: int `json:"sLen"`, + type: string `json:"type"`, + source: Test_Group_Source `json:"source"`, + tests: []Rsa_Sig_Test_Vector `json:"tests"`, +} + +Rsa_Oaep_Dec_Test_Group :: struct { + key_size: int `json:"keySize"`, + private_key: Rsa_Private_Key `json:"privateKey"`, + private_key_jwk: Rsa_Jwk_Private_Key `json:"privateKeyJwk"`, + private_key_pem: string `json:"privateKeyPem"`, + private_key_pkcs8: common.Hex_Bytes `json:"privateKeyPkcs8"`, + sha: string `json:"sha"`, + mgf: string `json:"mgf"`, + mfg_sha: string `json:"mgfSha"`, + type: string `json:"type"`, + source: Test_Group_Source `json:"source"`, + tests: []Rsa_Oaep_Test_Vector `json:"tests"`, +} + +Rsa_Oaep_Test_Vector :: struct { + tc_id: int `json:"tcId"`, + comment: string `json:"comment"`, + msg: common.Hex_Bytes `json:"msg"`, + ct: common.Hex_Bytes `json:"ct"`, + label: common.Hex_Bytes `json:"label"`, + result: Result `json:"result"`, + flags: []string `json:"flags"`, +} + +Rsa_Public_Key :: struct { + modulus: common.Hex_Bytes `json:"modulus"`, + public_exponent: common.Hex_Bytes `json:"publicExponent"`, +} + +Rsa_Private_Key :: struct { + modulus: common.Hex_Bytes `json:"modulus"`, + private_exponent: common.Hex_Bytes `json:"privateExponent"`, + public_exponent: common.Hex_Bytes `json:"publicExponent"`, + prime1: common.Hex_Bytes `json:"prime1"`, + prime2: common.Hex_Bytes `json:"prime2"`, + exponent1: common.Hex_Bytes `json:"exponent1"`, + exponent2: common.Hex_Bytes `json:"exponent2"`, + coefficient: common.Hex_Bytes `json:"coefficient"`, +} + +Rsa_Jwk_Private_Key :: struct { + akg: string `json:"alg"`, + kid: string `json:"kid"`, + kty: string `json:"kty"`, + d: common.Jwk_Bytes `json:"d"`, + dp: common.Jwk_Bytes `json:"dp"`, + dq: common.Jwk_Bytes `json:"dq"`, + e: common.Jwk_Bytes `json:"e"`, + n: common.Jwk_Bytes `json:"n"`, + p: common.Jwk_Bytes `json:"p"`, + q: common.Jwk_Bytes `json:"q"`, + qi: common.Jwk_Bytes `json:"qi"`, +} diff --git a/tests/core/speed.odin b/tests/core/speed.odin index 5ea9bbc21..0da7c1b82 100644 --- a/tests/core/speed.odin +++ b/tests/core/speed.odin @@ -2,6 +2,7 @@ package tests_core @(require) import "crypto" +@(require) import "crypto/bigint" @(require) import "hash" @(require) import "image" @(require) import "math/big" \ No newline at end of file