Hakan54
Hakan54

Reputation: 3889

Extract public key from PrivateKeyInfo with Bouncy Castle

I am trying to obtain the public key from a private key, for some use cases it is simple if the type is of the instance PEMKeyPair and PEMEncryptedKeyPair. These two can be easily converted to a java.security.KeyPair with JcaPEMKeyConverter. However if I have an object of the type PrivateKeyInfo and PKCS8EncryptedPrivateKeyInfo I can only extract the private key with the JcaPEMKeyConverter and I am not sure if there is an utility available or a better way to also extract the public key from it. Currently I am using a workaround which is not that efficient as I create a pem file and then reread it with a pemparser to extract the public key, this happends in the extractPublicKey method. See below for the full code snippet:

import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class App {

    private static final JcaPEMKeyConverter keyConverter = new JcaPEMKeyConverter();
    private static final JceOpenSSLPKCS8DecryptorProviderBuilder pkcs8DecryptorProviderBuilder = new JceOpenSSLPKCS8DecryptorProviderBuilder();
    private static final JcePEMDecryptorProviderBuilder pemDecryptorProviderBuilder = new JcePEMDecryptorProviderBuilder();

    public static void main(String[] args) {
        Security.addProvider(new BouncyCastleProvider());

        List<Object> objects = parsePemContent(PEM_CONTENT);
        Object object = objects.get(0);

        KeyPair keyPair = extractKeyPair(object, null).orElseThrow();
        PrivateKey privateKey = keyPair.getPrivate();
        PublicKey publicKey = keyPair.getPublic();
    }

    private static List<Object> parsePemContent(String pemContent) {
        try (Reader stringReader = new StringReader(pemContent);
             PEMParser pemParser = new PEMParser(stringReader)) {

            List<Object> objects = new ArrayList<>();
            for (Object object = pemParser.readObject(); object != null; object = pemParser.readObject()) {
                objects.add(object);
            }

            return objects;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static Optional<KeyPair> extractKeyPair(Object object, char[] keyPassword) {
        try {
            PrivateKeyInfo privateKeyInfo = null;
            PEMKeyPair pemKeyPair = null;
            KeyPair keyPair = null;

            if (object instanceof PrivateKeyInfo) {
                privateKeyInfo = (PrivateKeyInfo) object;
            } else if (object instanceof PKCS8EncryptedPrivateKeyInfo) {
                InputDecryptorProvider decryptorProvider = pkcs8DecryptorProviderBuilder.build(keyPassword);
                PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = (PKCS8EncryptedPrivateKeyInfo) object;
                privateKeyInfo = encryptedPrivateKeyInfo.decryptPrivateKeyInfo(decryptorProvider);
            } else if (object instanceof PEMKeyPair) {
                pemKeyPair = (PEMKeyPair) object;
            } else if (object instanceof PEMEncryptedKeyPair) {
                PEMDecryptorProvider decryptorProvider = pemDecryptorProviderBuilder.build(keyPassword);
                PEMEncryptedKeyPair encryptedKeyPair = (PEMEncryptedKeyPair) object;
                pemKeyPair = encryptedKeyPair.decryptKeyPair(decryptorProvider);
            }

            if (privateKeyInfo != null) {
                PrivateKey privateKey = keyConverter.getPrivateKey(privateKeyInfo);
                PublicKey publicKey = extractPublicKey(privateKey);
                keyPair = new KeyPair(publicKey, privateKey);
            }

            if (pemKeyPair != null) {
                keyPair = keyConverter.getKeyPair(pemKeyPair);
            }

            return Optional.ofNullable(keyPair);
        } catch (IOException | OperatorCreationException | PKCSException e) {
            throw new RuntimeException(e);
        }
    }

    private static PublicKey extractPublicKey(PrivateKey privateKey) {
        try (StringWriter writer = new StringWriter()) {
            JcaMiscPEMGenerator pemGenerator = new JcaMiscPEMGenerator(privateKey);
            PemObject pemObject = pemGenerator.generate();

            PemWriter pemWriter = new PemWriter(writer);
            pemWriter.writeObject(pemObject);
            pemWriter.close();

            try (StringReader stringReader = new StringReader(writer.toString());
                 PEMParser pemParser = new PEMParser(stringReader)) {
                PEMKeyPair pemKeyPair = (PEMKeyPair) pemParser.readObject();
                KeyPair keyPair = keyConverter.getKeyPair(pemKeyPair);
                return keyPair.getPublic();
            }
        } catch (IOException exception) {
            throw new RuntimeException(exception);
        }
    }

    private static final String PEM_CONTENT = """
            Bag Attributes
                friendlyName: client
                localKeyID: 54 69 6D 65 20 31 35 39 39 34 30 31 38 32 38 37 32 35\s
            Key Attributes: <No Attributes>
            -----BEGIN PRIVATE KEY-----
            MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCTBHn+btuggNTb
            B5eOMTSpKnIL6+GTObUkn6PdTSV8Ids65tJCRWBOKARNCbJsRCfJeHdtvFojYZXm
            r6PKi84CsYPafn/D/8rIJCvbvUFM1K+zB7MpF3UzRhyU3QqsTfdutr72nLhUXlzl
            eH6bNJ547N8hz8NpU48jMNz7xB4MlekjrhlLHtgq8Qb43Q+lZm2VUW8iGC5xoDK1
            SnPlm5AoWVCaRbaN6nmfoeSIAdlzixtO41ZYGQm1Blhzsk08fQ4H5I6zHKYKyHY8
            k5oKpE7+wB7fGM0qyI19wAy1UuEa5hvSilrM66GoHkQ5bEa78QeD5PHw5ohofpKh
            Vys+vIFVAgMBAAECggEAR0OsPwFNxQeuJl4PwQVpGXdRwSWeOteGTzJzJBr5SKrA
            slShJy6p+Di9nPpOWtzOzIJwoejjaLMtDp2lL9GFExkpaQhYtpGPomSmPeYHeU6/
            vHDHD+wnC6u4vxBG1C8W+bvr5W8iiwMS1MkL1gAzsTphDuq/Npcik1RkSkZOqppj
            t6kQVxJ7yJ+iSPFzubABv4luxLg30ESSw/vD8MfaNWcud3XE5YAKACjqQqi2IEkE
            IJ/sCrqAq71bdlsFkAwm2T/8ecIBoUKuCYS9gteVAMXfT0lV5cAIag+zZ5cyOiBL
            Nx8UW3J5tc06u8mcYEIBQsWtrmNOkaV6ntFgG0r+IQKBgQDR182z6pQkVBPdHSI1
            V7gITifOmxZEUBWSnb3ekrHtCNcUzQ62Cd+0mCPcnhB40nm+7PfpOVsVu7gJEZb7
            e6xT8uYm4ZkFQqNzZQcx8TWXHEeaAArwzMetDrgz+bwZlWcUcG7S3RMybvWPqScz
            AXjYNGriZScHW8yh0CEUDsyVGwKBgQCzWvzj/W7D3d0C2dSJoSLsyseFd0kdUHA7
            1zj4PqOqfpZAdEe6elUKRADx1lIi/YVd2h5h4aN+dsZ71feCJLnZkCLIkeWn2M6Q
            5O9vtbmQ8YhxGktiqP7km3KgAdZk4C53Tacjq3R37JSOEX3vnJHUoXzkxXA71g7/
            SassCX1aTwKBgQCXoeZ9tOuZmLvF0rCOdTWBouA29nBPqsL78EpsU/qIOxQYbtjL
            iDUDrdB0Mi/a7tSUt22pNQ3xlXU18GT2knaDLwlKXUiSuYWc9AsP9qnv6LqAuLkv
            Kfq7veAzhql6nzAeX+RlMOUXU4DUb7norI6jRLVbpRZfxeEHqHrOoKcKswKBgBV4
            4SnSX35ng1wiBAXuGqZKqJRb8Y7m4GjpnVJq/WEeApL42NWEa8Xs2kgZpn+15k+U
            G2sQfmhXg++zcAxOpUlcri1g+iOcGy7RmbDACtVFdVZFFZ1cKhfoXFK3pZkyFZ4G
            1+m3TxxEYIyZn4AeOH9CThd9Y7BmMilyAmIlSLKVAoGADbX0WVnQieiA2NdgUqmX
            QIcg3Hsspsx5Ro7EReYuHVAC2J7WOrnINLPsCU+jLdnDm4iXgvjdb9Lsx3qCyAUA
            +K9GqpcRYXRGTz7+YdFVneU19WK6Jeo5vmy66KXVGBk/134aQ0QKQ1Tfnd+fmAtV
            iVDJeabSN63mDyr6CX6j5/0=
            -----END PRIVATE KEY-----
            Bag Attributes
                friendlyName: client
                localKeyID: 54 69 6D 65 20 31 35 39 39 34 30 31 38 32 38 37 32 35\s
            subject=/C=NL/O=Altindag/OU=Altindag/CN=black-hole
            issuer=/C=NL/O=Thunderberry/OU=Certificate Authority/CN=Root-CA
            -----BEGIN CERTIFICATE-----
            MIIDGjCCAgICCQDLR+kGVrMOoTANBgkqhkiG9w0BAQUFADBWMQswCQYDVQQGEwJO
            TDEVMBMGA1UEChMMVGh1bmRlcmJlcnJ5MR4wHAYDVQQLExVDZXJ0aWZpY2F0ZSBB
            dXRob3JpdHkxEDAOBgNVBAMTB1Jvb3QtQ0EwHhcNMjAwOTA2MDg0MTI0WhcNMjUw
            OTA1MDg0MTI0WjBIMQswCQYDVQQGEwJOTDERMA8GA1UEChMIQWx0aW5kYWcxETAP
            BgNVBAsTCEFsdGluZGFnMRMwEQYDVQQDEwpibGFjay1ob2xlMIIBIjANBgkqhkiG
            9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkwR5/m7boIDU2weXjjE0qSpyC+vhkzm1JJ+j
            3U0lfCHbOubSQkVgTigETQmybEQnyXh3bbxaI2GV5q+jyovOArGD2n5/w//KyCQr
            271BTNSvswezKRd1M0YclN0KrE33bra+9py4VF5c5Xh+mzSeeOzfIc/DaVOPIzDc
            +8QeDJXpI64ZSx7YKvEG+N0PpWZtlVFvIhgucaAytUpz5ZuQKFlQmkW2jep5n6Hk
            iAHZc4sbTuNWWBkJtQZYc7JNPH0OB+SOsxymCsh2PJOaCqRO/sAe3xjNKsiNfcAM
            tVLhGuYb0opazOuhqB5EOWxGu/EHg+Tx8OaIaH6SoVcrPryBVQIDAQABMA0GCSqG
            SIb3DQEBBQUAA4IBAQCWrAt3mlE+6iT8N55rFPqgOLBcdXizSQqW2ycMvwau9FqL
            V5i6nHDV6jWoGIuOmo4IvkXWBN/Q+SPqoAeSvJw5WgVpd6CF1+QlcdpP9k3utJqK
            OZJduZxgqTYHw+QUQbDCGHlVJCQZKKHqKQryXdP18SAXQt0GrDkmE53FUUF9Act3
            NxDUiMf3AgbS6NxY9oH3RR5LvcyvM8FYHaGX1mjjQzY8Fr0TmR+x8ItrXDphN0MW
            J3EuJE2qz7Dx7CQgJAiaQ43o7qMuVfqzKqSv8Sk46OBQCss0VNpqfsUCq0niIJJB
            NNPJMUaSaIx0aKdbvoEAVeekkGMBWakVW4z0xNMK
            -----END CERTIFICATE-----
                        
            """;

}

I used Java 17 and the following bouncy castle dependency:

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcpkix-jdk18on</artifactId>
    <version>1.76</version>
</dependency>

Any idea whether it is possible to extract the public key from a PrivateKeyInfo without the need of using the workaround in the method extractPublicKey which temporally creates a pem string of the private key with JcaMiscPEMGenerator to reprocess with the PEMParser to obtain the PEMKeyPair which in return holds the private and public key.

Upvotes: 0

Views: 1772

Answers (1)

dave_thompson_085
dave_thompson_085

Reputation: 38990

Why it doesn't work for Ed25519

[Jca]MiscPEMGenerator generates the OpenSSL 'traditional' formats for RSA, DSA, and EC(DSA), which PEMParser reads back to PEMKeyPair type as desired, but for other algorithms including Ed25519 it generates PKCS8 format -- the same format you started with (except not encrypted even if your input was), which reads back to PrivateKeyInfo, accomplishing nothing.

What you can do

For Ed25519 the JCA API actually allows you to generate the public key directly. The other algorithms don't, but for RSA the 'CRT' sub-API which in practice is always used contains the needed information and for EC the PKCS8 format in practice1 does. Only for DSA, which you didn't ask for but I included for near-completeness, do you need an unsafe hack. (I didn't include DH because in practice DH is not used with long-term keys that would be stored anywhere.)

1 The SEC1 ECPrivateKey format used in PKCS8 formally has the publicKey field OPTIONAL, but I have never encounted software that doesn't generate it.

    PublicKey publicKey = null;
    if( privateKey instanceof RSAPrivateCrtKey )
        publicKey = KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(
                ((RSAPrivateCrtKey)privateKey).getModulus(), ((RSAPrivateCrtKey)privateKey).getPublicExponent() ));
    else if( privateKey instanceof DSAPrivateKey ){
        DSAParams dsap = ((DSAPrivateKey)privateKey).getParams();
        publicKey = KeyFactory.getInstance("DSA").generatePublic(new DSAPublicKeySpec(
                dsap.getG().modPow( ((DSAPrivateKey)privateKey).getX(), dsap.getP()), dsap.getP(), dsap.getQ(), dsap.getG() ));
        // WARNING! VULNERABLE TO SIDE-CHANNEL ATTACKS!
    }else if( privateKey instanceof ECPrivateKey ){
        ASN1BitString wrappt = org.bouncycastle.asn1.sec.ECPrivateKey.getInstance(privateKeyInfo.getPrivateKey().getOctets()).getPublicKey();
        publicKey = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(
                new SubjectPublicKeyInfo(privateKeyInfo.getPrivateKeyAlgorithm(), wrappt).getEncoded() ));
    }else if( privateKey instanceof EdDSAPrivateKey ){
        publicKey = ((EdDSAPrivateKey)privateKey).getPublicKey();
    }else ; // handle error if/as appropriate

Upvotes: 1

Related Questions