[release/5.x] Cherry pick: Replace RSASSA-PKCS1-v1_5 with RSA-PSS in crypto API (#6415) (#6425)

Co-authored-by: Max <maxtropets@microsoft.com>
This commit is contained in:
CCF [bot] 2024-08-05 14:53:11 +01:00 коммит произвёл GitHub
Родитель 6006f8b9a0
Коммит 9217756d92
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
14 изменённых файлов: 162 добавлений и 41 удалений

Просмотреть файл

@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
[5.0.2]: https://github.com/microsoft/CCF/releases/tag/ccf-5.0.2 [5.0.2]: https://github.com/microsoft/CCF/releases/tag/ccf-5.0.2
### Developer API
#### C++
- `RSAKeyPair::sign` and `RSAKeyPair::verify` now use `RSA-PSS` instead of `RSASSA-PKCS1-v1_5`.
- Users can specify `salt_length` (defaulted to `0`).
#### TypeScript/JavaScript
- `ccfapp.crypto.sign()` and `ccfapp.crypto.verifySignature()` no longer support `RSASSA-PKCS1-v1_5`, instead `RSA-PSS` has been added.
- `SigningAlgorithm` has been extended with optional `saltLength`, defaulted to `0` if not passed.
### Bug Fixes ### Bug Fixes
- The `/tx` endpoint returns more accurate error messages for incorrectly formed transactions ids (#6359). - The `/tx` endpoint returns more accurate error messages for incorrectly formed transactions ids (#6359).

Просмотреть файл

@ -55,26 +55,31 @@ namespace ccf::crypto
virtual std::vector<uint8_t> public_key_der() const = 0; virtual std::vector<uint8_t> public_key_der() const = 0;
virtual std::vector<uint8_t> sign( virtual std::vector<uint8_t> sign(
std::span<const uint8_t> d, MDType md_type = MDType::NONE) const = 0; std::span<const uint8_t> d,
MDType md_type = MDType::NONE,
size_t salt_length = 0) const = 0;
virtual bool verify( virtual bool verify(
const uint8_t* contents, const uint8_t* contents,
size_t contents_size, size_t contents_size,
const uint8_t* signature, const uint8_t* signature,
size_t signature_size, size_t signature_size,
MDType md_type = MDType::NONE) = 0; MDType md_type = MDType::NONE,
size_t salt_length = 0) = 0;
virtual bool verify( virtual bool verify(
const std::vector<uint8_t>& contents, const std::vector<uint8_t>& contents,
const std::vector<uint8_t>& signature, const std::vector<uint8_t>& signature,
MDType md_type = MDType::NONE) MDType md_type = MDType::NONE,
size_t salt_length = 0)
{ {
return verify( return verify(
contents.data(), contents.data(),
contents.size(), contents.size(),
signature.data(), signature.data(),
signature.size(), signature.size(),
md_type); md_type,
salt_length);
} }
virtual JsonWebKeyRSAPrivate private_key_jwk_rsa( virtual JsonWebKeyRSAPrivate private_key_jwk_rsa(

Просмотреть файл

@ -81,7 +81,8 @@ namespace ccf::crypto
size_t contents_size, size_t contents_size,
const uint8_t* signature, const uint8_t* signature,
size_t signature_size, size_t signature_size,
MDType md_type = MDType::NONE) = 0; MDType md_type = MDType::NONE,
size_t salt_legth = 0) = 0;
struct Components struct Components
{ {

Просмотреть файл

@ -231,7 +231,7 @@ export interface CryptoKeyPair {
publicKey: string; publicKey: string;
} }
export type AlgorithmName = "RSASSA-PKCS1-v1_5" | "ECDSA" | "EdDSA" | "HMAC"; export type AlgorithmName = "RSA-PSS" | "ECDSA" | "EdDSA" | "HMAC";
export type DigestAlgorithm = "SHA-256" | "SHA-384" | "SHA-512"; export type DigestAlgorithm = "SHA-256" | "SHA-384" | "SHA-512";
@ -239,9 +239,14 @@ export interface SigningAlgorithm {
name: AlgorithmName; name: AlgorithmName;
/** /**
* Digest algorithm. It's necessary for "RSASSA-PKCS1-v1_5", "ECDSA", and "HMAC" * Digest algorithm. It's necessary for "RSA-PSS", "ECDSA", and "HMAC"
*/ */
hash?: DigestAlgorithm; hash?: DigestAlgorithm;
/**
* Salt length, necessary for "RSA-PSS".
*/
saltLength?: number;
} }
/** /**

Просмотреть файл

@ -142,8 +142,8 @@ class CCFPolyfill implements CCF {
let padding = undefined; let padding = undefined;
const privKey = jscrypto.createPrivateKey(key); const privKey = jscrypto.createPrivateKey(key);
if (privKey.asymmetricKeyType == "rsa") { if (privKey.asymmetricKeyType == "rsa") {
if (algorithm.name === "RSASSA-PKCS1-v1_5") { if (algorithm.name === "RSA-PSS") {
padding = jscrypto.constants.RSA_PKCS1_PADDING; padding = jscrypto.constants.RSA_PKCS1_PSS_PADDING;
} else { } else {
throw new Error("incompatible signing algorithm for given key type"); throw new Error("incompatible signing algorithm for given key type");
} }
@ -168,6 +168,7 @@ class CCFPolyfill implements CCF {
key: privKey, key: privKey,
dsaEncoding: "ieee-p1363", dsaEncoding: "ieee-p1363",
padding: padding, padding: padding,
saltLength: algorithm.saltLength ?? 0,
}); });
}, },
verifySignature( verifySignature(
@ -179,8 +180,8 @@ class CCFPolyfill implements CCF {
let padding = undefined; let padding = undefined;
const pubKey = jscrypto.createPublicKey(key); const pubKey = jscrypto.createPublicKey(key);
if (pubKey.asymmetricKeyType == "rsa") { if (pubKey.asymmetricKeyType == "rsa") {
if (algorithm.name === "RSASSA-PKCS1-v1_5") { if (algorithm.name === "RSA-PSS") {
padding = jscrypto.constants.RSA_PKCS1_PADDING; padding = jscrypto.constants.RSA_PKCS1_PSS_PADDING;
} else { } else {
throw new Error("incompatible signing algorithm for given key type"); throw new Error("incompatible signing algorithm for given key type");
} }
@ -211,6 +212,7 @@ class CCFPolyfill implements CCF {
key: pubKey, key: pubKey,
dsaEncoding: "ieee-p1363", dsaEncoding: "ieee-p1363",
padding: padding, padding: padding,
saltLength: algorithm.saltLength ?? 0,
}, },
new Uint8Array(signature), new Uint8Array(signature),
); );

Просмотреть файл

@ -167,7 +167,7 @@ describe("polyfill", function () {
}); });
}); });
describe("sign", function () { describe("sign", function () {
it("performs RSASSA-PKCS1-v1_5 sign correctly", function () { it("performs RSA-PSS sign correctly", function () {
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", { const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048, modulusLength: 2048,
publicKeyEncoding: { publicKeyEncoding: {
@ -182,7 +182,7 @@ describe("polyfill", function () {
const data = ccf.strToBuf("foo"); const data = ccf.strToBuf("foo");
const signature = ccf.crypto.sign( const signature = ccf.crypto.sign(
{ {
name: "RSASSA-PKCS1-v1_5", name: "RSA-PSS",
hash: "SHA-256", hash: "SHA-256",
}, },
privateKey, privateKey,
@ -198,6 +198,7 @@ describe("polyfill", function () {
{ {
key: publicKey, key: publicKey,
dsaEncoding: "ieee-p1363", dsaEncoding: "ieee-p1363",
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
}, },
new Uint8Array(signature), new Uint8Array(signature),
), ),
@ -208,7 +209,7 @@ describe("polyfill", function () {
assert.isTrue( assert.isTrue(
ccf.crypto.verifySignature( ccf.crypto.verifySignature(
{ {
name: "RSASSA-PKCS1-v1_5", name: "RSA-PSS",
hash: "SHA-256", hash: "SHA-256",
}, },
publicKey, publicKey,
@ -392,7 +393,7 @@ describe("polyfill", function () {
}); });
}); });
describe("verifySignature", function () { describe("verifySignature", function () {
it("performs RSASSA-PKCS1-v1_5 validation correctly", function () { it("performs RSA-PSS validation correctly", function () {
const { cert, publicKey, privateKey } = generateSelfSignedCert(); const { cert, publicKey, privateKey } = generateSelfSignedCert();
const signer = crypto.createSign("sha256"); const signer = crypto.createSign("sha256");
const data = ccf.strToBuf("foo"); const data = ccf.strToBuf("foo");
@ -400,12 +401,13 @@ describe("polyfill", function () {
signer.end(); signer.end();
const signature = signer.sign({ const signature = signer.sign({
key: crypto.createPrivateKey(privateKey), key: crypto.createPrivateKey(privateKey),
padding: crypto.constants.RSA_PKCS1_PADDING, padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: 0,
}); });
assert.isTrue( assert.isTrue(
ccf.crypto.verifySignature( ccf.crypto.verifySignature(
{ {
name: "RSASSA-PKCS1-v1_5", name: "RSA-PSS",
hash: "SHA-256", hash: "SHA-256",
}, },
cert, cert,
@ -416,7 +418,7 @@ describe("polyfill", function () {
assert.isTrue( assert.isTrue(
ccf.crypto.verifySignature( ccf.crypto.verifySignature(
{ {
name: "RSASSA-PKCS1-v1_5", name: "RSA-PSS",
hash: "SHA-256", hash: "SHA-256",
}, },
publicKey, publicKey,
@ -427,7 +429,7 @@ describe("polyfill", function () {
assert.isNotTrue( assert.isNotTrue(
ccf.crypto.verifySignature( ccf.crypto.verifySignature(
{ {
name: "RSASSA-PKCS1-v1_5", name: "RSA-PSS",
hash: "SHA-256", hash: "SHA-256",
}, },
cert, cert,
@ -494,7 +496,7 @@ describe("polyfill", function () {
assert.throws(() => assert.throws(() =>
ccf.crypto.verifySignature( ccf.crypto.verifySignature(
{ {
name: "RSASSA-PKCS1-v1_5", name: "RSA-PSS",
hash: "SHA-256", hash: "SHA-256",
}, },
publicKey, publicKey,
@ -543,7 +545,7 @@ describe("polyfill", function () {
assert.throws(() => assert.throws(() =>
ccf.crypto.verifySignature( ccf.crypto.verifySignature(
{ {
name: "RSASSA-PKCS1-v1_5", name: "RSA-PSS",
hash: "SHA-256", hash: "SHA-256",
}, },
publicKey, publicKey,

Просмотреть файл

@ -205,12 +205,14 @@ namespace ccf::crypto
} }
std::vector<uint8_t> RSAKeyPair_OpenSSL::sign( std::vector<uint8_t> RSAKeyPair_OpenSSL::sign(
std::span<const uint8_t> d, MDType md_type) const std::span<const uint8_t> d, MDType md_type, size_t salt_length) const
{ {
std::vector<uint8_t> r(2048); std::vector<uint8_t> r(2048);
auto hash = OpenSSLHashProvider().Hash(d.data(), d.size(), md_type); auto hash = OpenSSLHashProvider().Hash(d.data(), d.size(), md_type);
Unique_EVP_PKEY_CTX pctx(key); Unique_EVP_PKEY_CTX pctx(key);
CHECK1(EVP_PKEY_sign_init(pctx)); CHECK1(EVP_PKEY_sign_init(pctx));
CHECK1(EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PSS_PADDING));
CHECK1(EVP_PKEY_CTX_set_rsa_pss_saltlen(pctx, salt_length));
CHECK1(EVP_PKEY_CTX_set_signature_md(pctx, get_md_type(md_type))); CHECK1(EVP_PKEY_CTX_set_signature_md(pctx, get_md_type(md_type)));
size_t olen = r.size(); size_t olen = r.size();
CHECK1(EVP_PKEY_sign(pctx, r.data(), &olen, hash.data(), hash.size())); CHECK1(EVP_PKEY_sign(pctx, r.data(), &olen, hash.data(), hash.size()));
@ -223,10 +225,11 @@ namespace ccf::crypto
size_t contents_size, size_t contents_size,
const uint8_t* signature, const uint8_t* signature,
size_t signature_size, size_t signature_size,
MDType md_type) MDType md_type,
size_t salt_length)
{ {
return RSAPublicKey_OpenSSL::verify( return RSAPublicKey_OpenSSL::verify(
contents, contents_size, signature, signature_size, md_type); contents, contents_size, signature, signature_size, md_type, salt_length);
} }
JsonWebKeyRSAPrivate RSAKeyPair_OpenSSL::private_key_jwk_rsa( JsonWebKeyRSAPrivate RSAKeyPair_OpenSSL::private_key_jwk_rsa(

Просмотреть файл

@ -36,14 +36,17 @@ namespace ccf::crypto
virtual std::vector<uint8_t> public_key_der() const override; virtual std::vector<uint8_t> public_key_der() const override;
virtual std::vector<uint8_t> sign( virtual std::vector<uint8_t> sign(
std::span<const uint8_t> d, MDType md_type = MDType::NONE) const override; std::span<const uint8_t> d,
MDType md_type = MDType::NONE,
size_t salt_length = 0) const override;
virtual bool verify( virtual bool verify(
const uint8_t* contents, const uint8_t* contents,
size_t contents_size, size_t contents_size,
const uint8_t* signature, const uint8_t* signature,
size_t signature_size, size_t signature_size,
MDType md_type = MDType::NONE) override; MDType md_type = MDType::NONE,
size_t salt_length = 0) override;
virtual JsonWebKeyRSAPrivate private_key_jwk_rsa( virtual JsonWebKeyRSAPrivate private_key_jwk_rsa(
const std::optional<std::string>& kid = std::nullopt) const override; const std::optional<std::string>& kid = std::nullopt) const override;

Просмотреть файл

@ -195,11 +195,14 @@ namespace ccf::crypto
size_t contents_size, size_t contents_size,
const uint8_t* signature, const uint8_t* signature,
size_t signature_size, size_t signature_size,
MDType md_type) MDType md_type,
size_t salt_length)
{ {
auto hash = OpenSSLHashProvider().Hash(contents, contents_size, md_type); auto hash = OpenSSLHashProvider().Hash(contents, contents_size, md_type);
Unique_EVP_PKEY_CTX pctx(key); Unique_EVP_PKEY_CTX pctx(key);
CHECK1(EVP_PKEY_verify_init(pctx)); CHECK1(EVP_PKEY_verify_init(pctx));
CHECK1(EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PSS_PADDING));
CHECK1(EVP_PKEY_CTX_set_rsa_pss_saltlen(pctx, salt_length));
CHECK1(EVP_PKEY_CTX_set_signature_md(pctx, get_md_type(md_type))); CHECK1(EVP_PKEY_CTX_set_signature_md(pctx, get_md_type(md_type)));
return EVP_PKEY_verify( return EVP_PKEY_verify(
pctx, signature, signature_size, hash.data(), hash.size()) == 1; pctx, signature, signature_size, hash.data(), hash.size()) == 1;

Просмотреть файл

@ -52,7 +52,8 @@ namespace ccf::crypto
size_t contents_size, size_t contents_size,
const uint8_t* signature, const uint8_t* signature,
size_t signature_size, size_t signature_size,
MDType md_type = MDType::NONE) override; MDType md_type = MDType::NONE,
size_t salt_length = 0) override;
virtual Components components() const override; virtual Components components() const override;

Просмотреть файл

@ -932,3 +932,47 @@ TEST_CASE("Incremental hash")
} }
ccf::crypto::openssl_sha256_shutdown(); ccf::crypto::openssl_sha256_shutdown();
} }
TEST_CASE("Sign and verify with RSA key")
{
const auto kp = ccf::crypto::make_rsa_key_pair();
const auto pub = ccf::crypto::make_rsa_public_key(kp->public_key_pem());
const auto mdtype = ccf::crypto::MDType::SHA256;
vector<uint8_t> contents(contents_.begin(), contents_.end());
{
constexpr size_t salt_length = 0;
const auto sig = kp->sign(contents, mdtype, salt_length);
REQUIRE(pub->verify(
contents.data(),
contents.size(),
sig.data(),
sig.size(),
mdtype,
salt_length));
}
{
constexpr size_t sign_salt_length = 0, verify_salt_legth = 32;
const auto sig = kp->sign(contents, mdtype, sign_salt_length);
REQUIRE(!pub->verify(
contents.data(),
contents.size(),
sig.data(),
sig.size(),
mdtype,
verify_salt_legth));
}
{
constexpr size_t sign_salt_length = 32, verify_salt_legth = 32;
const auto sig = kp->sign(contents, mdtype, sign_salt_length);
REQUIRE(pub->verify(
contents.data(),
contents.size(),
sig.data(),
sig.size(),
mdtype,
verify_salt_legth));
}
}

Просмотреть файл

@ -884,10 +884,19 @@ namespace ccf::js::extensions
sig_der, key_pair->get_curve_id()); sig_der, key_pair->get_curve_id());
return JS_NewArrayBufferCopy(ctx, sig.data(), sig.size()); return JS_NewArrayBufferCopy(ctx, sig.data(), sig.size());
} }
else if (algo_name == "RSASSA-PKCS1-v1_5") else if (algo_name == "RSA-PSS")
{ {
auto key_pair = ccf::crypto::make_rsa_key_pair(key); auto key_pair = ccf::crypto::make_rsa_key_pair(key);
auto sig = key_pair->sign(contents, mdtype);
int64_t salt_length{};
std::ignore = JS_ToInt64(
jsctx,
&salt_length,
jsctx.get_property(algorithm, "saltLength").val);
auto sig =
key_pair->sign(contents, mdtype, static_cast<size_t>(salt_length));
return JS_NewArrayBufferCopy(ctx, sig.data(), sig.size()); return JS_NewArrayBufferCopy(ctx, sig.data(), sig.size());
} }
else if (algo_name == "HMAC") else if (algo_name == "HMAC")
@ -900,8 +909,8 @@ namespace ccf::js::extensions
{ {
return JS_ThrowRangeError( return JS_ThrowRangeError(
ctx, ctx,
"Unsupported signing algorithm, supported: RSASSA-PKCS1-v1_5, " "Unsupported signing algorithm, supported: RSA-PSS, ECDSA, EdDSA, "
"ECDSA, EdDSA, HMAC"); "HMAC");
} }
} }
catch (const std::exception& ex) catch (const std::exception& ex)
@ -1010,12 +1019,12 @@ namespace ccf::js::extensions
ctx, "Unsupported hash algorithm, supported: SHA-256"); ctx, "Unsupported hash algorithm, supported: SHA-256");
} }
if (algo_name != "RSASSA-PKCS1-v1_5" && algo_name != "ECDSA") if (algo_name != "RSA-PSS" && algo_name != "ECDSA")
{ {
return JS_ThrowRangeError( return JS_ThrowRangeError(
ctx, ctx,
"Unsupported signing algorithm, supported: RSASSA-PKCS1-v1_5, " "Unsupported signing algorithm, supported: RSA-PSS, ECDSA, "
"ECDSA, EdDSA"); "EdDSA");
} }
std::vector<uint8_t> sig(signature, signature + signature_size); std::vector<uint8_t> sig(signature, signature + signature_size);
@ -1035,13 +1044,29 @@ namespace ccf::js::extensions
valid = valid =
verifier->verify(data, data_size, sig.data(), sig.size(), mdtype); verifier->verify(data, data_size, sig.data(), sig.size(), mdtype);
} }
else else if (algo_name == "ECDSA")
{ {
auto public_key = ccf::crypto::make_public_key(key); auto public_key = ccf::crypto::make_public_key(key);
valid = valid =
public_key->verify(data, data_size, sig.data(), sig.size(), mdtype); public_key->verify(data, data_size, sig.data(), sig.size(), mdtype);
} }
else
{
int64_t salt_length{};
std::ignore = JS_ToInt64(
jsctx,
&salt_length,
jsctx.get_property(algorithm, "saltLength").val);
auto public_key = ccf::crypto::make_rsa_public_key(key);
valid = public_key->verify(
data,
data_size,
sig.data(),
sig.size(),
mdtype,
static_cast<size_t>(salt_length));
}
return JS_NewBool(ctx, valid); return JS_NewBool(ctx, valid);
} }
catch (const std::exception& ex) catch (const std::exception& ex)

Просмотреть файл

@ -213,8 +213,15 @@ def sign(algorithm: dict, key_pem: str, data: bytes) -> bytes:
else: else:
raise ValueError("Unsupported hash algorithm") raise ValueError("Unsupported hash algorithm")
if isinstance(key, rsa.RSAPrivateKey): if isinstance(key, rsa.RSAPrivateKey):
if algorithm["name"] == "RSASSA-PKCS1-v1_5": if algorithm["name"] == "RSA-PSS":
return key.sign(data, padding.PKCS1v15(), hash_alg) return key.sign(
data,
padding=padding.PSS(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
salt_length=algorithm.get("saltLength", 0),
),
algorithm=hash_alg,
)
else: else:
raise ValueError("Unsupported signing algorithm") raise ValueError("Unsupported signing algorithm")
elif isinstance(key, ec.EllipticCurvePrivateKey): elif isinstance(key, ec.EllipticCurvePrivateKey):
@ -254,7 +261,15 @@ def verify_signature(algorithm: dict, signature: bytes, data: bytes, key_pub_pem
if isinstance(key_pub, rsa.RSAPublicKey): if isinstance(key_pub, rsa.RSAPublicKey):
if algorithm["hash"] != "SHA-256": if algorithm["hash"] != "SHA-256":
raise ValueError("Unsupported hash algorithm") raise ValueError("Unsupported hash algorithm")
key_pub.verify(signature, data, padding.PKCS1v15(), hashes.SHA256()) key_pub.verify(
signature,
data,
padding.PSS(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
salt_length=algorithm.get("saltLength", 0),
),
hashes.SHA256(),
)
elif isinstance(key_pub, ec.EllipticCurvePublicKey): elif isinstance(key_pub, ec.EllipticCurvePublicKey):
if algorithm["hash"] != "SHA-256": if algorithm["hash"] != "SHA-256":
raise ValueError("Unsupported hash algorithm") raise ValueError("Unsupported hash algorithm")

Просмотреть файл

@ -438,7 +438,7 @@ def test_npm_app(network, args):
# Test RSA signing + verification # Test RSA signing + verification
key_priv_pem, key_pub_pem = infra.crypto.generate_rsa_keypair(2048) key_priv_pem, key_pub_pem = infra.crypto.generate_rsa_keypair(2048)
algorithm = {"name": "RSASSA-PKCS1-v1_5", "hash": "SHA-256"} algorithm = {"name": "RSA-PSS", "hash": "SHA-256", "saltLength": 32}
data = rand_bytes(random.randint(2, 50)) data = rand_bytes(random.randint(2, 50))
r = c.post( r = c.post(
"/app/sign", "/app/sign",
@ -551,7 +551,7 @@ def test_npm_app(network, args):
pass pass
key_priv_pem, key_pub_pem = infra.crypto.generate_rsa_keypair(2048) key_priv_pem, key_pub_pem = infra.crypto.generate_rsa_keypair(2048)
algorithm = {"name": "RSASSA-PKCS1-v1_5", "hash": "SHA-256"} algorithm = {"name": "RSA-PSS", "hash": "SHA-256", "saltLength": 32}
signature = infra.crypto.sign(algorithm, key_priv_pem, data) signature = infra.crypto.sign(algorithm, key_priv_pem, data)
r = c.post( r = c.post(
"/app/verifySignature", "/app/verifySignature",