import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.attribute.PosixFilePermissions;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.spec.ECGenParameterSpec;import java.util.Base64; public class GenerateKey { public static void main(String[] args) throws Exception { KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); generator.initialize(new ECGenParameterSpec("secp256r1")); // P-256, aka prime256v1 KeyPair pair = generator.generateKeyPair(); // getEncoded() is PKCS#8 for the private key and SPKI for the public key Path keyFile = Files.createFile(Path.of("key.pem"), PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"))); Files.writeString(keyFile, pem("PRIVATE KEY", pair.getPrivate().getEncoded())); Files.writeString(Path.of("pub.pem"), pem("PUBLIC KEY", pair.getPublic().getEncoded())); System.out.print(pem("PUBLIC KEY", pair.getPublic().getEncoded())); } static String pem(String label, byte[] der) { Base64.Encoder base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.US_ASCII)); return "-----BEGIN " + label + "-----\n" + base64.encodeToString(der) + "\n-----END " + label + "-----\n"; }} // "BEGIN ENCRYPTED PRIVATE KEY": PKCS#8 under PBES2 (PBKDF2-HMAC-SHA256 + AES-256-CBC)char[] password = System.getenv("KEY_PASSWORD").toCharArray();byte[] salt = new byte[16];new SecureRandom().nextBytes(salt); Cipher cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");cipher.init(Cipher.ENCRYPT_MODE, SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256").generateSecret(new PBEKeySpec(password)), new PBEParameterSpec(salt, 600_000));byte[] encrypted = cipher.doFinal(pair.getPrivate().getEncoded()); // EncryptedPrivateKeyInfo only accepts the parameters under their standard name, PBES2AlgorithmParameters pbes2 = AlgorithmParameters.getInstance("PBES2");pbes2.init(cipher.getParameters().getEncoded());Files.writeString(Path.of("key-encrypted.pem"), pem("ENCRYPTED PRIVATE KEY", new EncryptedPrivateKeyInfo(pbes2, encrypted).getEncoded())); # a P-256 key pair plus a self-signed certificate in a password-protected PKCS#12 keystorekeytool -genkeypair -alias signing -keyalg EC -groupname secp256r1 \ -sigalg SHA256withECDSA -dname "CN=signing key" -validity 365 \ -keystore keys.p12 -storetype PKCS12 -storepass:env STORE_PASS # the same private key as PKCS#8 PEM, for tools outside the JVMopenssl pkcs12 -in keys.p12 -passin env:STORE_PASS -nocerts -nodes | openssl pkey -out key-from-p12.pem How it works
KeyPairGenerator.getInstance("EC")plusECGenParameterSpec("secp256r1")selects P-256; the JDK's own provider (SunEC) also supportssecp384r1andsecp521r1.getPrivate().getEncoded()is PKCS#8 DER andgetPublic().getEncoded()is SPKI DER. PEM is that DER in base64, wrapped at 64 columns betweenBEGIN/ENDlines — which is all thepemhelper does.Files.createFilewithrw-------creates the private key file with owner-only permissions in one step, and refuses to overwrite an existing key.- For an encrypted file,
PBEWithHmacSHA256AndAES_256encrypts the PKCS#8 bytes with a PBKDF2-derived key, andEncryptedPrivateKeyInfowraps them in the standard structure that OpenSSL opens with-passin. keytool -genkeypairstores the key next to a self-signed certificate, because a keystore entry for a private key always carries a certificate chain;openssl pkcs12 -nocerts -nodesextracts the key as PEM.
Gotchas
- The JDK knows P-256 only as
secp256r1,NIST P-256,X9.62 prime256v1or its OID:new ECGenParameterSpec("P-256")and"prime256v1"fail withUnknown curve name. - secp256k1 is gone from SunEC since JDK 16 (
Curve not supported: secp256k1). Bitcoin and Ethereum keys need Bouncy Castle as the provider. - Passing
cipher.getParameters()straight toEncryptedPrivateKeyInfothrowsNoSuchAlgorithmException: unrecognized algorithm name: PBEWithHmacSHA256AndAES_256. Re-reading the parameters asPBES2, as in the second snippet, is the fix. - JDK 25 ships
java.security.PEMEncoderandPEMDecoderonly as a preview API (JEP 470,--enable-preview). Until it is final, a Base64 helper like the one above is the portable way to write PEM. PosixFilePermissionsworks on Linux and macOS only; on WindowsFiles.createFilewith that attribute throwsUnsupportedOperationException, so protect the key file with ACLs there.