ECDSA verification succeeds when it should fail
guidovranken opened this issue · 7 comments
I think the following should not pass ECDSA verification (invalid pubkey), but it does. Tested on Linux 64 bit, latest master branch checkout.
#include <eccrypto.h>
#include <ecp.h>
#include <oids.h>
int main(void)
{
::CryptoPP::ECDSA<::CryptoPP::ECP, ::CryptoPP::SHA256>::PublicKey publicKey;
publicKey.Initialize(
::CryptoPP::ASN1::secp256k1(),
CryptoPP::ECP::Point(
CryptoPP::Integer("83326269377737301187045338455478996967104803243941757917076354219390730898031"),
CryptoPP::Integer("108911706275326467973600132368983151825997206660859431906025905780521963107049")
));
::CryptoPP::ECDSA<::CryptoPP::ECP, ::CryptoPP::SHA256>::Verifier verifier(publicKey);
const CryptoPP::Integer R("58459610944154385406267492095069703630366579530687393858060946682600281547621");
const CryptoPP::Integer S("166425580247629610000042226331335729376739920034019158271667950393311552549306");
uint8_t ct[] = {0x31, 0x32, 0x33, 0x34, 0x30, 0x30};
const size_t siglen = verifier.SignatureLength();
uint8_t signature[siglen];
R.Encode(signature + 0, siglen / 2);
S.Encode(signature + (siglen / 2), siglen / 2);
printf("%d\n", verifier.VerifyMessage(ct, sizeof(ct), signature, siglen));
return 0;
}
Thanks @guidovranken,
I think the following should not pass ECDSA verification (invalid pubkey)
What is wrong with the key?
When I add a call to Validate
after the public key is loaded, it passes validation.
#include "eccrypto.h"
#include "ecp.h"
#include "oids.h"
#include "osrng.h"
#include <iostream>
int main(void)
{
using namespace CryptoPP;
ECDSA<ECP, SHA256>::PublicKey publicKey;
publicKey.Initialize(
ASN1::secp256k1(),
CryptoPP::ECP::Point(
CryptoPP::Integer("83326269377737301187045338455478996967104803243941757917076354219390730898031"),
CryptoPP::Integer("108911706275326467973600132368983151825997206660859431906025905780521963107049")
));
AutoSeededRandomPool prng;
std::cout << "Valid: " << !!publicKey.Validate(prng, 3) << std::endl;
return 0;
}
Crypto++ 5.6.2, 8.0 and Master each validate the public key.
Apologies, I was misreading my debug output. The pubkey is ok but the signature S is larger than the curve order and should be rejected as far as I can tell.
but the signature S is larger than the curve order and should be rejected as far as I can tell.
OK, so it looks like S
is being reduced prior to Verify
. Here's the Verify
function from gfpcrypt.h : 304
. Verify
performs the check.
bool Verify(const DL_GroupParameters<T> ¶ms, const DL_PublicKey<T> &publicKey, const Integer &e, const Integer &r, const Integer &s) const
{
const Integer &q = params.GetSubgroupOrder();
if (r>=q || r<1 || s>=q || s<1)
return false;
Integer w = s.InverseMod(q);
Integer u1 = (e * w) % q;
Integer u2 = (r * w) % q;
// verify r == (g^u1 * y^u2 mod p) mod q
return r == params.ConvertElementToInteger(publicKey.CascadeExponentiateBaseAndPublicElement(u1, u2)) % q;
}
Tracing back in the call stack leads to pubkey.h : 1729
and VerifyAndRestart
. S
is reduced when it is initialized ma.m_s
, which is a PK_MessageAccumulator
-based class:
bool VerifyAndRestart(PK_MessageAccumulator &messageAccumulator) const
{
this->GetMaterial().DoQuickSanityCheck();
PK_MessageAccumulatorBase &ma = static_cast<PK_MessageAccumulatorBase &>(messageAccumulator);
const DL_ElgamalLikeSignatureAlgorithm<T> &alg = this->GetSignatureAlgorithm();
const DL_GroupParameters<T> ¶ms = this->GetAbstractGroupParameters();
const DL_PublicKey<T> &key = this->GetKeyInterface();
SecByteBlock representative(this->MessageRepresentativeLength());
this->GetMessageEncodingInterface().ComputeMessageRepresentative(NullRNG(), ma.m_recoverableMessage, ma.m_recoverableMessage.size(),
ma.AccessHash(), this->GetHashIdentifier(), ma.m_empty,
representative, this->MessageRepresentativeBitLength());
ma.m_empty = true;
Integer e(representative, representative.size());
Integer r(ma.m_semisignature, ma.m_semisignature.size());
return alg.Verify(params, key, e, r, ma.m_s);
}
I don't see an easy way around this.
@mouse07410, any thoughts?
Here's the external checks that can be added to check r
and s
:
#include "eccrypto.h"
#include "ecp.h"
#include "oids.h"
#include "osrng.h"
#include <iostream>
int main(void)
{
using namespace CryptoPP;
ECDSA<ECP, SHA256>::PublicKey publicKey;
publicKey.Initialize(
ASN1::secp256k1(),
CryptoPP::ECP::Point(
CryptoPP::Integer("83326269377737301187045338455478996967104803243941757917076354219390730898031"),
CryptoPP::Integer("108911706275326467973600132368983151825997206660859431906025905780521963107049")
));
AutoSeededRandomPool prng;
std::cout << "Group: " << !!publicKey.GetAbstractGroupParameters().Validate(prng, 3) << std::endl;
std::cout << "Key: " << !!publicKey.Validate(prng, 3) << std::endl;
ECDSA<ECP, SHA256>::Verifier verifier(publicKey);
const CryptoPP::Integer R("58459610944154385406267492095069703630366579530687393858060946682600281547621");
const CryptoPP::Integer S("166425580247629610000042226331335729376739920034019158271667950393311552549306");
if (R >= publicKey.GetGroupParameters().GetSubgroupOrder())
std::cout << "R is too large" << std::endl;
else
std::cout << "R is OK" << std::endl;
if (S >= publicKey.GetGroupParameters().GetSubgroupOrder())
std::cout << "S is too large" << std::endl;
else
std::cout << "S is OK" << std::endl;
uint8_t ct[] = {0x31, 0x32, 0x33, 0x34, 0x30, 0x30};
const size_t siglen = verifier.SignatureLength();
uint8_t signature[siglen];
R.Encode(signature + 0, siglen / 2);
S.Encode(signature + (siglen / 2), siglen / 2);
std::cout << "Verify: " << !!verifier.VerifyMessage(ct, sizeof(ct), signature, siglen) << std::endl;
return 0;
}
In ECDSA in general, if the hash size is greater than the P, it's truncated.
Usually it's done when the signature is created - but I see no obvious reason why Verify shouldn't do that. Though I'd be less comfortable with truncation on Verify.
Haven't checked the code - it should be truncation rather than modular reduction.
Yeah, it looks like DL_VerifierBase
lops-off s
:
void InputSignature(PK_MessageAccumulator &messageAccumulator, const byte *signature, size_t signatureLength) const
{
CRYPTOPP_UNUSED(signature); CRYPTOPP_UNUSED(signatureLength);
PK_MessageAccumulatorBase &ma = static_cast<PK_MessageAccumulatorBase &>(messageAccumulator);
const DL_ElgamalLikeSignatureAlgorithm<T> &alg = this->GetSignatureAlgorithm();
const DL_GroupParameters<T> ¶ms = this->GetAbstractGroupParameters();
const size_t rLen = alg.RLen(params);
ma.m_semisignature.Assign(signature, rLen);
ma.m_s.Decode(signature+rLen, alg.SLen(params));
this->GetMessageEncodingInterface().ProcessSemisignature(ma.AccessHash(), ma.m_semisignature, ma.m_semisignature.size());
}
I wonder if we can safely perform a check that verifies signatureLength == rLen + sLen
.
I wonder if we can safely perform a check that verifies signatureLength == rLen + sLen.
The answer to this question appears to be no. Running cryptest.exe v
shows the NIST test vectors violate the condition.
This is a kind of unexpected result. When I change:
const size_t rLen = alg.RLen(params);
ma.m_semisignature.Assign(signature, rLen);
ma.m_s.Decode(signature+rLen, alg.SLen(params));
To:
ma.m_semisignature.Assign(signature, rLen);
ma.m_s.Decode(signature+rLen, signatureLength - rLen);
std::cout << " O: " << std::hex << params.GetSubgroupOrder() << std::endl;
std::cout << " S: " << std::hex << ma.m_s << std::endl;
Then the result is:
O: fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141h
S: 6ff18a52dcc0336f7af62400a6dd9b810732baf1ff758000d6f613a556eb31bah
So S
is getting lopped-off before InputSignature
. I suspect it is happening when the PK_MessageAccumulator
is being filled. I'll dig a little further this afternoon when I get some time.
So
S
is getting lopped-off beforeInputSignature
.
Doh... S
is being lopped-off when it is encoded:
const size_t siglen = verifier.SignatureLength();
uint8_t signature[siglen];
R.Encode(signature + 0, siglen / 2);
S.Encode(signature + (siglen / 2), siglen / 2);
S.MinEncodedSize()
will return a value larger than siglen / 2
. When we add the following to the test program:
std::cout << "Min size: " << S.MinEncodedSize() << std::endl;
std::cout << "s length: " << siglen / 2 << std::endl;
It results in:
Min size: 21
s length: 20