web-push-libs / webpush-java

Web Push library for Java
MIT License
323 stars 112 forks source link

InvalidKeyException: Not an EC key: ECDH #104

Closed iyashsoni closed 5 years ago

iyashsoni commented 5 years ago

Constantly getting Exception in thread "main" java.security.InvalidKeyException: Not an EC key: ECDH

Environment Info: MacOS, Java 1.8

I've tried the solution mentioned in this issue but still no luck.

I have done the following:

  1. Generated Vapid keys on my local machine.
  2. Always inserting BouncyCastleProvider at 1st position.
  3. Using the same version of Bouncy Castle libs 1.54 (bcpg-jdk15on, bcprov-jdk15on, bcmail-jdk15on, bcpkix-jdk15on)
  4. Put one jar file bcprov-jdk15on-154 at /Library/Java/JavaVirtualMachines/jdk1.8.0_211.jdk/Contents/Home/jre/lib/ext/

This is the stack trace:

Screenshot 2019-10-14 at 12 26 31 PM

This is the Subscription Class:


class WebSubscription {
    private String auth, key, endpoint;

    public WebSubscription() {
        Security.insertProviderAt(new BouncyCastleProvider(), 1);
    }

    public String getAuth() {
        return auth;
    }

    public void setAuth(String auth) {
        this.auth = auth;
    }

    /**
     * Returns the base64 encoded auth string as a byte[]
     */
    public byte[] getAuthAsBytes() {
        return org.bouncycastle.util.encoders.Base64.decode(getAuth());
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    /**
     * Returns the base64 encoded public key string as a byte[]
     */
    public byte[] getKeyAsBytes() {
        return org.bouncycastle.util.encoders.Base64.decode(getKey());
    }

    /**
     * Returns the base64 encoded public key as a PublicKey object
     */
    public PublicKey getUserPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.insertProviderAt(new BouncyCastleProvider(), 1);
        }
        KeyFactory kf;
        try {
            kf = KeyFactory.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME);
        } catch (NoSuchAlgorithmException nsae) {
            kf = KeyFactory.getInstance("ECDH");
        }
        ECNamedCurveParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256r1");
        ECPoint point = ecSpec.getCurve().decodePoint(getKeyAsBytes());
        ECPublicKeySpec pubSpec = new ECPublicKeySpec(point, ecSpec);
        return kf.generatePublic(pubSpec);
    }

    public String getEndpoint() {
        return endpoint;
    }

    public void setEndpoint(String endpoint) {
        this.endpoint = endpoint;
    }
}

The sendNotification Method:

protected void sendNotification(final Notification webNotification) {
        final String sourceMethod = "sendWebNotification";
        WebPushNotification wpNotification = (WebPushNotification) webNotification;
        String applicationId = webNotification.getApplicationId();
        String messageId = webNotification.getNotificationId();
        List<Long> deviceIds = webNotification.getDeviceIds();

        List<String> params = new ArrayList<>();
        nl.martijndwars.webpush.Notification notification;

        WebSubscription sub = new WebSubscription();
        sub.setEndpoint(FCM_ENDPOINT);
        sub.setAuth(AUTH_TOKEN_FROM_SUBSCRIPTION);
        sub.setKey(KEY_FROM_SUBSCRIPTION);
        try {
            PushService pushService;
            notification = new nl.martijndwars.webpush.Notification(
                    sub.getEndpoint(),
                    sub.getUserPublicKey(),
                    sub.getAuthAsBytes(),
                    wpNotification.getAlert().getBytes(),
                    255);

            // Instantiate the push service with a GCM API key
            pushService = new PushService(GCM_API);
            pushService.setPrivateKey(PRIVATE_KEY);
            pushService.setPublicKey(PUBLIC_KEY);
            HttpResponse httpResponse = pushService.send(notification);

        } catch (Exception e) {
            LOG.error(sourceMethod, "Send Message failure...", e);
        }
    }

P.S.: Don't know if it helps but I am building a final war file of the PushService and deploying in my local environment.

Let me know if more information is required. Thanks, Yash Soni

martijndwars commented 5 years ago

Thanks for the bug report! It looks like you're doing everything right. How did you generate the VAPID keys?

iyashsoni commented 5 years ago

@MartijnDwars I’m using the npm module web push to generate keys. The weird part is, the same piece of code runs perfectly fine when run from a plain java program - a separate java project with only one file.

I’ve debugged my main java project and the final exception is thrown from the key agreement class.

Any help? Thanks.

iyashsoni commented 5 years ago

@MartijnDwars Update: When I debugged through the code - I see method checkKey of sun.security.ec.ECKeyFactory class gets invoked. This is the origin of the error.

Screenshot 2019-10-17 at 2 31 18 PM

I have stepped through the code in both - my Push Server and a plain Push Java sample side by side. Everything is same, until the following function is triggered: It is in javax.crypto.KeyAgreement Class:

private void chooseProvider(int var1, Key var2, AlgorithmParameterSpec var3, SecureRandom var4) throws InvalidKeyException, InvalidAlgorithmParameterException {
        synchronized(this.lock) {
            if (this.spi != null) {
                this.implInit(this.spi, var1, var2, var3, var4);
            } else {
                Exception var6 = null;

                while(true) {
                    Service var7;
                    do {
                        do {
                            if (this.firstService == null && !this.serviceIterator.hasNext()) {
                                if (var6 instanceof InvalidKeyException) {
                                    throw (InvalidKeyException)var6;
                                }

                                if (var6 instanceof InvalidAlgorithmParameterException) {
                                    throw (InvalidAlgorithmParameterException)var6;
                                }

                                if (var6 instanceof RuntimeException) {
                                    throw (RuntimeException)var6;
                                }

                                String var12 = var2 != null ? var2.getClass().getName() : "(null)";
                                throw new InvalidKeyException("No installed provider supports this key: " + var12, var6);
                            }

                            if (this.firstService != null) {
                                var7 = this.firstService;
                                this.firstService = null;
                            } else {
                                var7 = (Service)this.serviceIterator.next();
                            }
                        } while(!var7.supportsParameter(var2));
                    } while(!JceSecurity.canUseProvider(var7.getProvider()));

                    try {
                        KeyAgreementSpi var8 = (KeyAgreementSpi)var7.newInstance((Object)null);
                        this.implInit(var8, var1, var2, var3, var4);
                        this.provider = var7.getProvider();
                        this.spi = var8;
                        this.firstService = null;
                        this.serviceIterator = null;
                        return;
                    } catch (Exception var10) {
                        if (var6 == null) {
                            var6 = var10;
                        }
                    }
                }
            }
        }
    }

Here this.serviceIterator contains two elements in both the programs - these are: BC: KeyAgreement.ECDH -> org.bouncycastle.jcajce.provider.asymmetric.ec.KeyAgreementSpi$DH SunEC: KeyAgreement.ECDH -> sun.security.ec.ECDHKeyAgreement

The only difference is: In my Push Server, JceSecurity.canUseProvider(var7.getProvider()) gives false for Bouncy Castle while it gives true in the case of Plain Java Push Sample.

Hence when var7.getProvider() is called in the final try block, we get BouncyCastle Prov correctly in Java Sample and we get SunEC Prov in my Push Server.

As a result, in implInit method of KeyAgreement class:

    private void implInit(KeyAgreementSpi var1, int var2, Key var3, AlgorithmParameterSpec var4, SecureRandom var5) throws InvalidKeyException, InvalidAlgorithmParameterException {
        if (var2 == 1) {
            var1.engineInit(var3, var5);
        } else {
            var1.engineInit(var3, var4, var5);
        }

    }

In Push Sample, var1 is instance of class org.bouncycastle.jcajce.provider.asymmetric.ec.KeyAgreementSpi$DH with following values:

Screenshot 2019-10-17 at 4 14 48 PM

In my Push Server, var1 is instance of class sun.security.ec.ECDHKeyAgreement with privateKey, publicKey = null and secretLen = 0

As a final result, in my Push Server, I get: Not an EC key: ECDH Error from checkKey method in sun.security.ec.ECKeyFactory:

Screenshot 2019-10-17 at 4 16 42 PM

I am completely unaware of what can be the reason for this. I thought this info might be helpful to you.

Thanks.

martijndwars commented 5 years ago

Thanks for the detailed analysis! The problem seems to be that JceSecurity.canUseProvider returns false for the BouncyCastle provider. This method from the standard library verifies if the JAR is a signed provider JAR file. The BouncyCastle JARs are signed, but it's possible that your deployment unpacks/repacks the JAR and thereby removes the signature. Can you check if the original BouncyCastle JAR is on your classpath?

iyashsoni commented 5 years ago

Hey @MartijnDwars, I got past this error with Reflection and dynamic class loading. Now I've hit across this SSL handshake error:

A signer with SubjectDN C=US,ST=California,L=Mountain View,O=Google
LLC,CN=edgecert.googleapis.com was sent from the target host.  The signer might need to be added to
local trust store /Users/yashsoni/MobileFirst-8.0.0.0/mfp-
server/usr/servers/mfp/resources/security/svkpush-PKCS-12.p12, located in SSL configuration alias 
defaultSSLConfig.  The extended error message from the SSL handshake exception is: PKIX path building 
failed: java.security.cert.CertPathBuilderException: No issuer certificate for certificate in certification path 
found.

Any idea?

martijndwars commented 5 years ago

The signer might need to be added to local trust store /Users/yashsoni/MobileFirst-8.0.0.0/mfp-server/usr/servers/mfp/resources/security/svkpush-PKCS-12.p12

It looks like you're running your JVM with a custom trust store that does not contain Google's root certificate. The error hints at a solution: add the signer to the local trust store. You'll have to Google on how to do this exactly, but maybe you can start at the following references:

iyashsoni commented 5 years ago

Thanks, @MartijnDwars for the help. I'm closing this issue as it is solved.