0

I have a public key, and its corresponded signature, R, S values generated by nodejs (v20.14.0) with the function as below

const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { 
  namedCurve:  'P-256', //'secp256k1', // invalid 'secp256r1', 
  publicKeyEncoding: { type: 'spki', format: 'der' }
});
const sign = crypto.createSign('SHA256');
const message = 'my message';
sign.update(message);
sign.end();
const signature = sign.sign(privateKey);
const RLength = parseInt(signature.toString('hex', 3, 4), 16);
const R = signature.subarray(4, 4+RLength);
const S = signature.subarray(4+RLength+2, signature.length);

A sample result:

  • public key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEL+RfybPlDY/+KMwY3ROpD4aSgtJFxnPOraWQpJkMJ0Ovj4rkrOSMPj+5rhE3jCJNYOV35TIGomJSxI65shfKug==.
  • signature: MEUCIQD+SjM+JD2u91p2Fy8UKtkMqXCkSPaCCIDFBaqzVbDA6QIgU5pGg8qnt7iWHpL9Anw5uxLXTH64gj9V9o0HjEmsBWo=
  • R: AP5KMz4kPa73WnYXLxQq2QypcKRI9oIIgMUFqrNVsMDp
  • S: U5pGg8qnt7iWHpL9Anw5uxLXTH64gj9V9o0HjEmsBWo=

These values will be passed to my java program (java 11) for verifying. The java program is


String pkey = "MFkwEw...Kug=="; // the entire base64 string above
byte[] publicKey = Base64.getDecoder().decode(pkey);
X9ECParameters curve = NISTNamedCurves.getByName("P-256"); //"secp256k1"
ECDomainParameters domain = new ECDomainParameters(curve.getCurve(), curve.getG(), curve.getN(), curve.getH());
ECDSASigner signer = new ECDSASigner(); // I use bouncy castle 1.78
signer.init (
    false, 
    new ECPublicKeyParameters(
        curve.getCurve().decodePoint(publicKey),  // the place where exception is thrown
        domain
    )
);
signer.verifySignature(
    message, 
    new BigInteger(Base64.getDecoder().decode(R)), 
    new BigInteger(Base64.getDecoder().decode(S))
);

However, the java code throws the error java.lang.IllegalArgumentException: Invalid point encoding 0x30.

Why the NISTNamedCurves can't decode the base64 string? I am completely new to cryptography, so I search with the exception Invalid point encoding 0x30. Some threads like [1] comes out, but I can't figure out why.

I appreciate any advice. Many thanks.


Edit: Valid test data:

public key (SPKI):     MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/UREdRdAbtwg/HeUhb8B2rICVjIT7ZtxloBVNrBV0jiMCOLmVs8gm0LE7oWlPfBToaWsbfp2sCCR5EhhalGeHQ== 
signature (ASN.1/DER): MEYCIQDLeC2T8ZIlruOw3Km0THK8yxcMPS5ufOHqus3Y/vyYMgIhAKgoAIV50yXYGIARdNE4bEt+eaujKFwX2cX6pf/y681p 
R:                     AMt4LZPxkiWu47DcqbRMcrzLFww9Lm584eq6zdj+/Jgy 
S:                     AKgoAIV50yXYGIARdNE4bEt+eaujKFwX2cX6pf/y681p
message:               my message

[1]. Read EC Public Key, works in python, error in java

6
  • The data (message, publicKey, signature) are inconsistent, even with NodeJS they cannot be verified: jdoodle.com/ia/14H1. Please post consistent data. Commented Jul 6, 2024 at 18:04
  • In the current Java/BouncyCastle code, the public key is imported incorrectly (decodePoint() requires the uncompressed/compressed public key), which is the cause of the error message. In addition, the hashing is missing (ECDSASigner#verifySignature() does not hash implicitly). Commented Jul 6, 2024 at 18:28
  • 1
    Note that no BouncyCastle is required for ECDSA on Java 11 (see here, sec. The SunEC Provider). Incidentally, both sides support the ASN.1/DER and the P1363 format. So there is no need for a manual transformation in the NodeJS code to R and S, i.e. signature can be verified directly on the Java side. Commented Jul 6, 2024 at 19:06
  • @Topaco Sorry my bad. I missed editing the value of message var after pasting my code, so the pubkey, R, S do not match. I use the same code (the code is also pasted at paste.debian.net/1322560 with the sample, R, S, Public Key values) as here The pubkye MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/UREdRdAbtwg/HeUhb8B2rICVjIT7ZtxloBVNrBV0jiMCOLmVs8gm0LE7oWlPfBToaWsbfp2sCCR5EhhalGeHQ==, sign: MEYCIQDLeC2T8ZIlruOw3Km0THK8yxcMPS5ufOHqus3Y/vyYMgIhAKgoAIV50yXYGIARdNE4bEt+eaujKFwX2cX6pf/y681p, R:AMt4LZPxkiWu47DcqbRMcrzLFww9Lm584eq6zdj+/Jgy, S: AKgoAIV50yXYGIARdNE4bEt+eaujKFwX2cX6pf/y681p Commented Jul 7, 2024 at 3:06
  • @Topaco With Java 11, which class/ methods should I use for verify the signature directly so I do not need R, S values? ECDSASigner I use (at javadoc.io/doc/org.bouncycastle/bcprov-jdk18on/latest/org/…) still requires me to pass in R, S values. That's why I manually extract R, S values. I am completely new to cryptography, so I appreciate your advice. Commented Jul 7, 2024 at 3:15

2 Answers 2

0

Verification with the Java code fails for the following reasons:

  • The public key is imported incorrectly. ECCurve#decodePoint() returns the public key as org.bouncycastle.math.ec.ECPoint.ECPoint and requires the uncompressed/compressed key. A possible fix is to make the following changes to import the DER encoded public SPKI key and get the required ECPoint:

    import java.security.KeyFactory;
    import java.security.spec.X509EncodedKeySpec;
    import org.bouncycastle.jce.interfaces.ECPublicKey;
    import org.bouncycastle.crypto.signers.ECDSASigner;
    import org.bouncycastle.jce.provider.BouncyCastleProvider;
    ...
    Security.addProvider(new BouncyCastleProvider());
    ...
    byte[] publicKey = Base64.getDecoder().decode(pkey);      
    KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
    X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKey);
    ECPublicKey ecPublicKey = (ECPublicKey)keyFactory.generatePublic(x509EncodedKeySpec); // note: org.bouncycastle.jce.interfaces.ECPublicKey
    ...
    signer.init (false, new ECPublicKeyParameters(ecPublicKey.getQ(), domain)); // note: public key is imported as ECPoint
    ...
    
  • When verifying, the hashing is missing. This must be done explicitly, as ECDSASigner#verifySignature() does not hash automatically:

    import java.security.MessageDigest;
    ...
    MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
    messageDigest.update(message.getBytes(StandardCharsets.UTF_8));
    byte[] hash = messageDigest.digest();
    ...
    String R = "AMt4LZPxkiWu47DcqbRMcrzLFww9Lm584eq6zdj+/Jgy";
    String S = "AKgoAIV50yXYGIARdNE4bEt+eaujKFwX2cX6pf/y681p";
    boolean verified = signer.verifySignature(hash, new BigInteger(Base64.getDecoder().decode(R)), new BigInteger(Base64.getDecoder().decode(S)));
    

With these changes, the verification of the test data (posted at the end of the question) is successful.


As already noted in the comment, ECDSA is supported in Java 11 in both formats (ASN.1/DER and IEEE P1363), so that no BouncyCastle is required. Since sign.sign() in the NodeJS code generates the signature in ASN.1/DER format by default (IEEE P1363 is also supported as of v13.2.0, see here), the signature can be verified directly in this format on the Java side:

import java.security.KeyFactory;
import java.security.spec.X509EncodedKeySpec;
import java.security.interfaces.ECPublicKey;
import java.security.Signature;
...
KeyFactory keyFactory = KeyFactory.getInstance("EC");
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(pkey));
ECPublicKey ecPublicKey = (ECPublicKey) keyFactory.generatePublic(x509EncodedKeySpec); // note: java.security.interfaces.ECPublicKey

Signature verifier = Signature.getInstance("SHA256withEcdsa");
verifier.initVerify(ecPublicKey);
verifier.update(message.getBytes(StandardCharsets.UTF_8));
String signature = "MEYCIQDLeC2T8ZIlruOw3Km0THK8yxcMPS5ufOHqus3Y/vyYMgIhAKgoAIV50yXYGIARdNE4bEt+eaujKFwX2cX6pf/y681p";
boolean verified = verifier.verify(Base64.getDecoder().decode(signature));
...

which also successfully verifies the signature.


For the sake of completeness: The specifier for IEEE P1363 is SHA256withECDSAinP1363Format, s. The SunEC Provider. In this case, the signature is to be passed as a concatenation of the byte arrays r and s, where r and s are fixed sized, unsigned big endian. r and s each have the length of the order of the generator point (if necessary with leading 0x00 values), e.g. for your example (hex encoded):

cb782d93f19225aee3b0dca9b44c72bccb170c3d2e6e7ce1eabacdd8fefc9832 a828008579d325d818801174d1386c4b7e79aba3285c17d9c5faa5fff2ebcd69

Spaces only for display purposes! Note the difference to the ASN.1/DER format: There r and s are minimally sized, signed big endian (which is why in the case of a leading byte > 0x7f a 0x00 must be prefixed so that the values are positive):

30460221 00cb782d93f19225aee3b0dca9b44c72bccb170c3d2e6e7ce1eabacdd8fefc9832 0221 00a828008579d325d818801174d1386c4b7e79aba3285c17d9c5faa5fff2ebcd69

Based on these definitions, IEEE P1363 provides a fixed size signature (64 bytes for P-256), while ASN.1/DER has no fixed size, but only a maximum size (72 bytes for P-256). See also this post for more details on both formats.

When creating BigInteger values from unsigned r and s, use the constructor BigInteger(int, byte[]) instead of BigInteger(byte[]). In your test data, r and s are signed, so the BigInteger conversion is correct.

Sign up to request clarification or add additional context in comments.

Comments

0

Just as an addition to Topaco's explanation.... I was still getting issues aroud the point, either because it wasn't found on the line or with the Invalid point encoding 0x30.

For me the solution was not to load the curve via NISTNamedCurves.getByName("P-256"), but instead, get it from the public key itself.

public boolean signatureMatchesMessages(String message, BigInteger r, BigInteger s) {
        try {

            Security.addProvider(new BouncyCastleProvider());
            String pkey = "MFkwEw...Kug=="; // the entire base64 string above

            byte[] publicKeyDecoded = Base64.decode(pkey);
            KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
            X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyDecoded);

            ECPublicKey publicKey = (ECPublicKey)keyFactory.generatePublic(x509EncodedKeySpec);
            ECDSASigner verifier = new ECDSASigner();
            ECCurve curve = publicKey.getQ().getCurve();
            ECDomainParameters domain = new ECDomainParameters(curve, publicKey.getParameters().getG(), publicKey.getParameters().getN(), publicKey.getParameters().getH());

            MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
            messageDigest.update(message.getBytes(StandardCharsets.UTF_8));
            byte[] hash = messageDigest.digest();

            verifier.init (
                    false,
                    new ECPublicKeyParameters(
                            publicKey.getQ(),
                            domain
                    )
            );

            return verifier.verifySignature(hash, r, s);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.