diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..81dcf70 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/coverity-scan.yml b/.github/workflows/coverity-scan.yml new file mode 100644 index 0000000..aa644e3 --- /dev/null +++ b/.github/workflows/coverity-scan.yml @@ -0,0 +1,16 @@ +name: Run Coverity scan and upload results + +on: + workflow_dispatch: + schedule: + - cron: '0 10 1 * *' # monthly + + +jobs: + coverity-scan: + uses: wultra/wultra-infrastructure/.github/workflows/coverity-scan.yml@develop + secrets: inherit + with: + project-name: ${{ github.event.repository.name }} + version: ${{ github.sha }} + description: ${{ github.ref }} diff --git a/.github/workflows/maven-package.yml b/.github/workflows/maven-package.yml new file mode 100644 index 0000000..e14ecd0 --- /dev/null +++ b/.github/workflows/maven-package.yml @@ -0,0 +1,24 @@ +name: Maven Package + +on: + workflow_dispatch: + +jobs: + maven-package: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Run Maven Package Step + run: mvn -B -U package + - name: Archive JAR Artifacts + uses: actions/upload-artifact@v3 + with: + name: Jar Artifacts + path: '**/target/*.jar' diff --git a/.github/workflows/maven-test.yml b/.github/workflows/maven-test.yml new file mode 100644 index 0000000..6111162 --- /dev/null +++ b/.github/workflows/maven-test.yml @@ -0,0 +1,18 @@ +name: Test with Maven + +on: + workflow_dispatch: + push: + branches: + - 'master' + - 'releases/**' + pull_request: + branches: + - 'develop' + - 'master' + - 'releases/**' + +jobs: + maven-tests: + uses: wultra/wultra-infrastructure/.github/workflows/maven-test.yml@develop + secrets: inherit diff --git a/.gitignore b/.gitignore index a1c2a23..81f6ec5 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,29 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +**/.DS_Store +**/target/ +**/src/main/java/generated/ +**/.springBeans + +### NetBeans template +**/nbproject/private/ +**/nbproject/ + +### Eclipse template +**/.settings +**/.project +**/.classpath + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio +*.iml +## Directory-based project format: +.idea/ +.mvn +dist/ + +### JRebel +rebel.xml + diff --git a/README.md b/README.md index 341f0e0..938f417 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,7 @@ Both approaches are described in chapters below. ### Supported Java Version -Only Java version 8 is supported at the moment. - -### Install Bouncy Castle provider - -The Bouncy Castle library is required to be installed in the JRE in order to run the Java utility. - -See: -https://github.com/wultra/powerauth-server/wiki/Installing-Bouncy-Castle +Only Java version 17+ is supported at the moment. ### Generate a Signing Key Pair @@ -110,6 +103,16 @@ You can convert EC private key to public key and print it: java -jar ssl-pinning-tool.jar export -k keypair.pem -p [password] ``` +### Export Private Key + +Server app developers might need the private key from generated key pair in order to be able to dynamically compute signatures. For example, our Mobile Utility Server uses the private key from this utility output. + +You can convert EC private key to the appropriate format and print it: + +```sh +java -jar ssl-pinning-tool.jar export-private -k keypair.pem -p [password] +``` + ### Troubleshooting Error: diff --git a/pom.xml b/pom.xml index 7f876f2..32fe64f 100644 --- a/pom.xml +++ b/pom.xml @@ -9,12 +9,13 @@ com.wultra ssl-pinning-tool - 1.0.2 + 1.5.0 jar org.springframework.boot spring-boot-starter-parent - 2.0.4.RELEASE + 3.1.3 + 2018 @@ -55,41 +56,46 @@ - UTF-8 - 1.8 - 1.8 + 1.76 + 1.5.1 org.bouncycastle - bcprov-jdk15on - 1.60 + bcprov-jdk18on + ${bcprov-jdk18on.version} org.bouncycastle - bcpkix-jdk15on - 1.60 + bcpkix-jdk18on + ${bcprov-jdk18on.version} io.getlime.security powerauth-java-crypto - 0.19.0 + ${powerauth-crypto.version} commons-cli commons-cli - 1.4 + 1.5.0 + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple com.fasterxml.jackson.core jackson-databind - 2.9.6 org.junit.jupiter junit-jupiter-engine - 5.2.0 test @@ -100,14 +106,13 @@ org.springframework.boot spring-boot-maven-plugin - 2.0.4.RELEASE com.wultra.security.ssl.pinning.Application + org.apache.maven.plugins maven-surefire-plugin - 2.22.0 diff --git a/src/main/java/com/wultra/security/ssl/pinning/Application.java b/src/main/java/com/wultra/security/ssl/pinning/Application.java index ac8682f..16e13ed 100755 --- a/src/main/java/com/wultra/security/ssl/pinning/Application.java +++ b/src/main/java/com/wultra/security/ssl/pinning/Application.java @@ -17,16 +17,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.google.common.base.Charsets; -import com.google.common.io.BaseEncoding; import com.wultra.security.ssl.pinning.errorhandling.SSLPinningException; import com.wultra.security.ssl.pinning.model.CertificateInfo; -import io.getlime.security.powerauth.crypto.lib.config.PowerAuthConfiguration; -import io.getlime.security.powerauth.crypto.lib.util.SignatureUtils; - import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator; -import io.getlime.security.powerauth.provider.CryptoProviderUtil; -import io.getlime.security.powerauth.provider.CryptoProviderUtilFactory; +import io.getlime.security.powerauth.crypto.lib.model.exception.CryptoProviderException; +import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException; +import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor; +import io.getlime.security.powerauth.crypto.lib.util.SignatureUtils; import org.apache.commons.cli.*; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; @@ -56,19 +53,26 @@ import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemObjectGenerator; import org.bouncycastle.util.io.pem.PemWriter; - -import java.io.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.*; import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.util.Base64; /** * SSL pinning tool command line application for generating signatures of SSL certificates. */ public class Application { + private static final Logger logger = LoggerFactory.getLogger(Application.class); + private static final ASN1ObjectIdentifier PASSWORD_ENCRYPTION_ALGORITHM = PKCS8Generator.AES_128_CBC; private static final AlgorithmIdentifier PASSWORD_ENCRYPTION_PRF = PKCS8Generator.PRF_HMACSHA256; @@ -78,7 +82,6 @@ public class Application { // Add Bouncy Castle Security Provider Security.addProvider(new BouncyCastleProvider()); - PowerAuthConfiguration.INSTANCE.setKeyConvertor(CryptoProviderUtilFactory.getCryptoProviderUtils()); } /** @@ -87,9 +90,9 @@ public class Application { * @param args Command line arguments. */ public static void main(String[] args) { - Application app = new Application(); + final Application app = new Application(); - CommandLine cmd = app.prepareCommandLine(args); + final CommandLine cmd = app.prepareCommandLine(args); if (cmd == null) { return; } @@ -118,7 +121,7 @@ private void executeCommand(CommandLine cmd) { try { expirationTime = Long.parseLong(expires); } catch (NumberFormatException ex) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, ex.getMessage(), ex); + logger.error(ex.getMessage(), ex); return; } } @@ -130,39 +133,39 @@ private void executeCommand(CommandLine cmd) { // Sign fingerprint data try { if (privateKeyPath == null) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, "Private key path is not specified, cannot compute signature."); + logger.error("Private key path is not specified, cannot compute signature."); return; } if (outputPath == null) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, "Output path is not specified, cannot generate JSON file."); + logger.error("Output path is not specified, cannot generate JSON file."); return; } if (certificatePath == null && commonName == null) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, "Common name is not specified, cannot compute signature."); + logger.error("Common name is not specified, cannot compute signature."); return; } if (certificatePath == null && fingerprint == null) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, "Fingerprint is not specified, cannot compute signature."); + logger.error("Fingerprint is not specified, cannot compute signature."); return; } if (certificatePath == null && expirationTime == null) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, "Expiration time is not specified, cannot compute signature."); + logger.error("Expiration time is not specified, cannot compute signature."); return; } if (certificatePath != null && (commonName != null || fingerprint != null && expirationTime != null)) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, "Ambiguous certificate data found, cannot compute signature."); + logger.error("Ambiguous certificate data found, cannot compute signature."); return; } - CertificateInfo signedFingerprint; + final CertificateInfo signedFingerprint; if (certificatePath != null) { - CertificateInfo certInfo = readCertificateInfo(certificatePath); + final CertificateInfo certInfo = readCertificateInfo(certificatePath); signedFingerprint = sign(privateKeyPath, privateKeyPassword, certInfo); } else { signedFingerprint = sign(privateKeyPath, privateKeyPassword, commonName, fingerprint, expirationTime); } generateJsonFile(outputPath, signedFingerprint); } catch (Exception ex) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, ex.getMessage(), ex); + logger.error(ex.getMessage(), ex); } break; @@ -170,12 +173,12 @@ private void executeCommand(CommandLine cmd) { // Generate keypair try { if (outputPath == null) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, "Output path is not specified, cannot generate key pair."); + logger.error("Output path is not specified, cannot generate key pair."); return; } generateKeyPair(outputPath, privateKeyPassword); } catch (Exception ex) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, ex.getMessage(), ex); + logger.error(ex.getMessage(), ex); } break; @@ -183,19 +186,32 @@ private void executeCommand(CommandLine cmd) { // Export public key try { if (privateKeyPath == null) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, "Private key path is not specified, cannot export public key."); + logger.error("Private key path is not specified, cannot export public key."); return; } - PublicKey publicKey = exportPublicKey(privateKeyPath, privateKeyPassword); + final PublicKey publicKey = exportPublicKey(privateKeyPath, privateKeyPassword); printPublicKey(publicKey); } catch (Exception ex) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, ex.getMessage(), ex); + logger.error(ex.getMessage(), ex); } break; + case "export-private": + // Export public key + try { + if (privateKeyPath == null) { + logger.error("Private key path is not specified, cannot export public key."); + return; + } + final PrivateKey privateKey = exportPrivateKey(privateKeyPath, privateKeyPassword); + printPrivateKey(privateKey); + } catch (Exception ex) { + logger.error(ex.getMessage(), ex); + } + break; default: // Unknown action - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, "Unknown command: " + command); + logger.error("Unknown command: {}", command); } } @@ -211,17 +227,17 @@ private CommandLine prepareCommandLine(String[] args) { // Prepare command line final CommandLine cmd; try { - CommandLineParser parser = new DefaultParser(); + final CommandLineParser parser = new DefaultParser(); cmd = parser.parse(options, args); // Print options when user specified no options or help was invoked if (args.length == 0 || cmd.hasOption("h")) { - HelpFormatter formatter = new HelpFormatter(); + final HelpFormatter formatter = new HelpFormatter(); formatter.setWidth(100); formatter.printHelp("java -jar ssl-pinning-tool.jar", options); return null; } } catch (ParseException ex) { - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, ex.getMessage(), ex); + logger.error(ex.getMessage(), ex); return null; } return cmd; @@ -255,30 +271,30 @@ private Options buildOptions() { * @param expirationTime Expiration time as Unix timestamp. * @return Fingerprint object. * @throws SSLPinningException Thrown when encryption fails. - * @throws java.security.SignatureException Thrown when signature computation fails. * @throws InvalidKeyException Thrown when signature key is invalid. + * @throws SSLPinningException Thrown when issue happens with SSL pinning data. */ CertificateInfo sign(String privateKeyPath, String privateKeyPassword, String commonName, String fingerprint, long expirationTime) - throws SSLPinningException, SignatureException, InvalidKeyException { + throws SSLPinningException, InvalidKeyException, GenericCryptoException, CryptoProviderException { // Load private key final PrivateKey privKey = loadPrivateKey(privateKeyPath, privateKeyPassword); // Remove all whitespaces from fingerprint - String fingerprintFormatted = fingerprint.replaceAll("\\s+", ""); + final String fingerprintFormatted = fingerprint.replaceAll("\\s+", ""); // Convert fingerprint to byte[] - byte[] fingerPrintBytes = Hex.decode(fingerprintFormatted); + final byte[] fingerPrintBytes = Hex.decode(fingerprintFormatted); // Convert fingerprint bytes to Base64 - final String fingerprintBase64 = BaseEncoding.base64().encode(fingerPrintBytes); + final String fingerprintBase64 = Base64.getEncoder().encodeToString(fingerPrintBytes); // Signature payload final String data = commonName + "&" + fingerprintBase64 + "&" + expirationTime; // Compute signature of payload using ECDSA with given EC private key final SignatureUtils utils = new SignatureUtils(); - byte[] signature = utils.computeECDSASignature(data.getBytes(Charsets.UTF_8), privKey); - final String signatureBase64 = BaseEncoding.base64().encode(signature); + final byte[] signature = utils.computeECDSASignature(data.getBytes(StandardCharsets.UTF_8), privKey); + final String signatureBase64 = Base64.getEncoder().encodeToString(signature); // Return Fingerprint object return new CertificateInfo(commonName, fingerprintBase64, expirationTime, signatureBase64); @@ -291,10 +307,9 @@ CertificateInfo sign(String privateKeyPath, String privateKeyPassword, String co * @param certInfo Information about certificate. * @return Signed certificate fingerprint. * @throws SSLPinningException Thrown when certificate fingerprint signature could not be computed. - * @throws java.security.SignatureException Thrown when data signature could not be computed. * @throws InvalidKeyException Thrown when private key is invalid. */ - CertificateInfo sign(String privateKeyPath, String privateKeyPassword, CertificateInfo certInfo) throws SSLPinningException, java.security.SignatureException, InvalidKeyException { + CertificateInfo sign(String privateKeyPath, String privateKeyPassword, CertificateInfo certInfo) throws SSLPinningException, InvalidKeyException, GenericCryptoException, CryptoProviderException { return sign(privateKeyPath, privateKeyPassword, certInfo.getName(), certInfo.getFingerprint(), certInfo.getExpires()); } @@ -311,27 +326,27 @@ PrivateKey loadPrivateKey(String privateKeyPath, String password) throws SSLPinn final KeyFactory kf = KeyFactory.getInstance("ECDSA", "BC"); final Object pemInfo = pemParser.readObject(); pemParser.close(); - if (pemInfo instanceof PrivateKeyInfo) { + if (pemInfo == null) { + throw new SSLPinningException("PemParser read null"); + } else if (pemInfo instanceof final PrivateKeyInfo privateKeyInfo) { // Private key is not encrypted if (password != null) { throw new SSLPinningException("Private key is not encrypted, however private key password is specified."); } - byte[] privateKeyBytes = ((PrivateKeyInfo) pemInfo).getEncoded(); - KeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + final byte[] privateKeyBytes = privateKeyInfo.getEncoded(); + final KeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); return kf.generatePrivate(keySpec); - } else if (pemInfo instanceof PKCS8EncryptedPrivateKeyInfo) { + } else if (pemInfo instanceof final PKCS8EncryptedPrivateKeyInfo pemPrivateKeyInfo) { // Private key is encrypted by password, decrypt it - PKCS8EncryptedPrivateKeyInfo pemPrivateKeyInfo = (PKCS8EncryptedPrivateKeyInfo) pemInfo; if (password == null) { throw new SSLPinningException("Private key is encrypted, however private key password is missing."); } - InputDecryptorProvider provider = new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray()); - JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + final InputDecryptorProvider provider = new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray()); + final JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); return converter.getPrivateKey(pemPrivateKeyInfo.decryptPrivateKeyInfo(provider)); } } catch (Exception ex) { - throw new SSLPinningException("Failed to load private key, error: "+ex.getMessage(), ex); - + throw new SSLPinningException("Failed to load private key, error: " + ex.getMessage(), ex); } throw new SSLPinningException("Private key could not be loaded because of unknown format."); } @@ -347,22 +362,22 @@ CertificateInfo readCertificateInfo(String certificatePath) throws SSLPinningExc final PEMParser pemParser = new PEMParser(new BufferedReader(fileReader)); final Object pemInfo = pemParser.readObject(); pemParser.close(); - if (pemInfo instanceof X509CertificateHolder) { - X509CertificateHolder x509Cert = (X509CertificateHolder) pemInfo; - CertificateInfo certInfo = new CertificateInfo(); - byte[] signature = computeSHA256Signature(x509Cert.getEncoded()); + if (pemInfo == null) { + throw new SSLPinningException("PemParser read null"); + } else if (pemInfo instanceof final X509CertificateHolder x509Cert) { + final CertificateInfo certInfo = new CertificateInfo(); + final byte[] signature = computeSHA256Signature(x509Cert.getEncoded()); certInfo.setFingerprint(new String(Hex.encode(signature))); // Expiration timestamps is stored as unix timestamp with seconds certInfo.setExpires(x509Cert.getNotAfter().getTime()/1000); - X500Name x500Name = x509Cert.getSubject(); - RDN commonNameRDN = x500Name.getRDNs(BCStyle.CN)[0]; - String commonName = IETFUtils.valueToString(commonNameRDN.getFirst().getValue()); + final X500Name x500Name = x509Cert.getSubject(); + final RDN commonNameRDN = x500Name.getRDNs(BCStyle.CN)[0]; + final String commonName = IETFUtils.valueToString(commonNameRDN.getFirst().getValue()); certInfo.setName(commonName); return certInfo; } } catch (Exception ex) { - throw new SSLPinningException("Failed to load certificate, error: "+ex.getMessage(), ex); - + throw new SSLPinningException("Failed to load certificate, error: " + ex.getMessage(), ex); } throw new SSLPinningException("Certificate could not be loaded because of unknown format."); } @@ -374,7 +389,7 @@ CertificateInfo readCertificateInfo(String certificatePath) throws SSLPinningExc * @throws NoSuchAlgorithmException Thrown when SHA-256 algorithm is not supported. */ private byte[] computeSHA256Signature(byte[] certificateData) throws NoSuchAlgorithmException { - MessageDigest md = MessageDigest.getInstance("SHA-256"); + final MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(certificateData); return md.digest(); } @@ -385,23 +400,23 @@ private byte[] computeSHA256Signature(byte[] certificateData) throws NoSuchAlgor * @param keyPairPassword Private key password (optional). * @throws IOException Thrown when key pair could not be generates. */ - void generateKeyPair(String outputPath, String keyPairPassword) throws IOException { + void generateKeyPair(String outputPath, String keyPairPassword) throws CryptoProviderException, IOException { final KeyGenerator keyGen = new KeyGenerator(); final KeyPair keyPair = keyGen.generateKeyPair(); OutputEncryptor encryptor = null; // The getEncoded() method returns key in PKCS8 format, it can be either unencrypted or encrypted if (keyPairPassword != null) { // Password was specified, generate encrypted PEM file - JceOpenSSLPKCS8EncryptorBuilder encryptorBuilder = new JceOpenSSLPKCS8EncryptorBuilder(PASSWORD_ENCRYPTION_ALGORITHM); + final JceOpenSSLPKCS8EncryptorBuilder encryptorBuilder = new JceOpenSSLPKCS8EncryptorBuilder(PASSWORD_ENCRYPTION_ALGORITHM); encryptorBuilder.setProvider("BC"); encryptorBuilder.setRandom(new SecureRandom()); - encryptorBuilder.setPasssword(keyPairPassword.toCharArray()); + encryptorBuilder.setPassword(keyPairPassword.toCharArray()); encryptorBuilder.setPRF(PASSWORD_ENCRYPTION_PRF); try { encryptor = encryptorBuilder.build(); } catch (OperatorCreationException ex) { // Failed to create encryptor, PEM file will not be encrypted - Logger.getLogger(Application.class.getName()).log(Level.SEVERE, "Failed to create encryptor, PEM file will not be encrypted. Error: "+ex.getMessage(), ex); + logger.error("Failed to create encryptor, PEM file will not be encrypted. Error: {}", ex.getMessage(), ex); } } @@ -414,7 +429,7 @@ void generateKeyPair(String outputPath, String keyPairPassword) throws IOExcepti final FileWriter fw = new FileWriter(outputPath); try (PemWriter pemWriterPriv = new PemWriter(fw)) { pemWriterPriv.writeObject(pemObject); - Logger.getLogger(Application.class.getName()).log(Level.INFO, "EC private key generated in file: " + outputPath); + logger.info("EC private key generated in file: {}", outputPath); } } @@ -431,7 +446,7 @@ void generateJsonFile(String outputPath, CertificateInfo fingerPrint) throws IOE objectMapper.enable(SerializationFeature.INDENT_OUTPUT); objectMapper.writeValue(fw, fingerPrint); fw.close(); - Logger.getLogger(Application.class.getName()).log(Level.INFO, "JSON output generated in file: " + outputPath); + logger.info("JSON output generated in file: {}", outputPath); } /** @@ -442,14 +457,31 @@ void generateJsonFile(String outputPath, CertificateInfo fingerPrint) throws IOE */ PublicKey exportPublicKey(String privateKeyPath, String privateKeyPassword) throws SSLPinningException { try { - PrivateKey privateKey = loadPrivateKey(privateKeyPath, privateKeyPassword); - KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", "BC"); - ECNamedCurveParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256r1"); - ECPoint Q = ecSpec.getG().multiply(((ECPrivateKey) privateKey).getD()); - ECPublicKeySpec pubSpec = new ECPublicKeySpec(Q, ecSpec); + final PrivateKey privateKey = loadPrivateKey(privateKeyPath, privateKeyPassword); + final KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", "BC"); + final ECNamedCurveParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256r1"); + if (ecSpec == null) { + throw new SSLPinningException("Curve secp256r1 is not present"); + } + final ECPoint Q = ecSpec.getG().multiply(((ECPrivateKey) privateKey).getD()); + final ECPublicKeySpec pubSpec = new ECPublicKeySpec(Q, ecSpec); return keyFactory.generatePublic(pubSpec); } catch (Exception ex) { - throw new SSLPinningException("Failed to convert private key, error: "+ex.getMessage(), ex); + throw new SSLPinningException("Failed to convert private key, error: " + ex.getMessage(), ex); + } + } + + /** + * Load private key + * @param privateKeyPath Path to private key. + * @param privateKeyPassword Private key password. + * @throws SSLPinningException Thrown when export fails. + */ + PrivateKey exportPrivateKey(String privateKeyPath, String privateKeyPassword) throws SSLPinningException { + try { + return loadPrivateKey(privateKeyPath, privateKeyPassword); + } catch (Exception ex) { + throw new SSLPinningException("Failed to convert private key, error: " + ex.getMessage(), ex); } } @@ -457,11 +489,22 @@ PublicKey exportPublicKey(String privateKeyPath, String privateKeyPassword) thro * Prints public key in PEM format. * @param publicKey Public key. */ - private void printPublicKey(PublicKey publicKey) { - final CryptoProviderUtil keyConversionUtilities = PowerAuthConfiguration.INSTANCE.getKeyConvertor(); - byte[] publicKeyBytes = keyConversionUtilities.convertPublicKeyToBytes(publicKey); - String publicKeyEncoded = BaseEncoding.base64().encode(publicKeyBytes); - System.out.println("Exported public key: " + publicKeyEncoded); + private void printPublicKey(PublicKey publicKey) throws CryptoProviderException { + final KeyConvertor keyConversionUtilities = new KeyConvertor(); + final byte[] publicKeyBytes = keyConversionUtilities.convertPublicKeyToBytes(publicKey); + final String publicKeyEncoded = Base64.getEncoder().encodeToString(publicKeyBytes); + logger.info(publicKeyEncoded); + } + + /** + * Prints private key in PEM format. + * @param privateKey Private key. + */ + private void printPrivateKey(PrivateKey privateKey) { + final KeyConvertor keyConversionUtilities = new KeyConvertor(); + final byte[] privateKeyBytes = keyConversionUtilities.convertPrivateKeyToBytes(privateKey); + final String privateKeyEncoded = Base64.getEncoder().encodeToString(privateKeyBytes); + logger.info(privateKeyEncoded); } } diff --git a/src/test/java/com/wultra/security/ssl/pinning/SSLPinningTest.java b/src/test/java/com/wultra/security/ssl/pinning/SSLPinningTest.java index 8882f10..67ca60a 100644 --- a/src/test/java/com/wultra/security/ssl/pinning/SSLPinningTest.java +++ b/src/test/java/com/wultra/security/ssl/pinning/SSLPinningTest.java @@ -10,8 +10,8 @@ import java.io.File; import java.io.FileWriter; -import java.io.IOException; -import java.security.*; +import java.security.PublicKey; +import java.security.Security; import java.util.Scanner; import static org.junit.jupiter.api.Assertions.*; @@ -25,7 +25,7 @@ class SSLPinningTest { private static final String TEST_CERTIFICATE_BASE64 = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR4ekNDQXErZ0F3SUJBZ0lJZkd4NURBK3VZS1F3RFFZSktvWklodmNOQVFFTEJRQXdWREVMTUFrR0ExVUUKQmhNQ1ZWTXhIakFjQmdOVkJBb1RGVWR2YjJkc1pTQlVjblZ6ZENCVFpYSjJhV05sY3pFbE1DTUdBMVVFQXhNYwpSMjl2WjJ4bElFbHVkR1Z5Ym1WMElFRjFkR2h2Y21sMGVTQkhNekFlRncweE9EQTRNVFF3TnpRME16VmFGdzB4Ck9ERXdNak13TnpNNE1EQmFNR2d4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGgKTVJZd0ZBWURWUVFIREExTmIzVnVkR0ZwYmlCV2FXVjNNUk13RVFZRFZRUUtEQXBIYjI5bmJHVWdURXhETVJjdwpGUVlEVlFRRERBNTNkM2N1WjI5dloyeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBCkJORVVBazlkQm5kTHJkci9FV01aWlI2NHZZOXVUdWNkUWM3Ymp4U0lESXFYZCtyVlhRdFg5VzRxZmh5eDhFVHgKZUZ0ZDltLy9QV3M2TDFYS3JlcWdaa0dqZ2dGU01JSUJUakFUQmdOVkhTVUVEREFLQmdnckJnRUZCUWNEQVRBTwpCZ05WSFE4QkFmOEVCQU1DQjRBd0dRWURWUjBSQkJJd0VJSU9kM2QzTG1kdmIyZHNaUzVqYjIwd2FBWUlLd1lCCkJRVUhBUUVFWERCYU1DMEdDQ3NHQVFVRkJ6QUNoaUZvZEhSd09pOHZjR3RwTG1kdmIyY3ZaM055TWk5SFZGTkgKU1VGSE15NWpjblF3S1FZSUt3WUJCUVVITUFHR0hXaDBkSEE2THk5dlkzTndMbkJyYVM1bmIyOW5MMGRVVTBkSgpRVWN6TUIwR0ExVWREZ1FXQkJRZlQxUjFJUVVyc05NVjhFdEd5M2wvcmNPMDF6QU1CZ05WSFJNQkFmOEVBakFBCk1COEdBMVVkSXdRWU1CYUFGSGZDdUZDYVozWjJzUzNDaHRDRG9INm1mcnBMTUNFR0ExVWRJQVFhTUJnd0RBWUsKS3dZQkJBSFdlUUlGQXpBSUJnWm5nUXdCQWdJd01RWURWUjBmQkNvd0tEQW1vQ1NnSW9ZZ2FIUjBjRG92TDJOeQpiQzV3YTJrdVoyOXZaeTlIVkZOSFNVRkhNeTVqY213d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFJWmlXM2pXCm1zOUJ5a0NZUDN1Z2haMDJ2ZE4xZ3dnWmtYa281eVh6TUhyMWtzem5nRFlZLzYrRlB0RHBNb0YwbW4yZ0ZkR1IKK1Z2RUFMaTlLaW95M2s0T0dOaEFtd0NHR2JEelNqYlRIK3dPUHZpdnFPR3lMRUpTbzREeGlqNHZPUU9ENlRUYQpKREhrT0Q3OVFFY3VqRlRjM3lEZzMvZ0M0Tm14dm14SEZ0UlNmenJxSUQ3VG9tTmVyL2NFSE1tTytFRWl6YlR1CjU2L2xiVVpqQ3dkNzB3aFFNZ0wwNWpneXdOWUpVay8waUhZd0JGbjhDRWU1QVlBR3FMeThGYWJDZ2ZSbmFzeW4KTGRSZTRoMU1NaFdvT0toSTdueVNvL3NlS3k5OFRhd0RFczdjcTZwR3ovR1h6RWdYQmVVU1ZEU0lURkM1MFRPVQpEZnFYS3Bpa2Z0MzN5TTg9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"; @BeforeEach - void setUp() throws IOException { + void setUp() throws Exception { app = new Application(); keyPairFile = File.createTempFile("ssl_pinning", ".pem"); app.generateKeyPair(keyPairFile.getAbsolutePath(), PRIVATE_KEY_PASSWORD); @@ -42,7 +42,7 @@ void testBCProvider() { } @Test - void testGenerateKeyPairCorrectPassword() throws SSLPinningException { + void testGenerateKeyPairCorrectPassword() throws Exception { assertNotNull(app.loadPrivateKey(keyPairFile.getAbsolutePath(), PRIVATE_KEY_PASSWORD)); } @@ -52,7 +52,7 @@ void testGenerateKeyPairInvalidPassword() { } @Test - void testSignatureWithDetails() throws SSLPinningException, SignatureException, InvalidKeyException { + void testSignatureWithDetails() throws Exception { CertificateInfo certInfo = app.sign(keyPairFile.getAbsolutePath(), PRIVATE_KEY_PASSWORD, "www.google.com", "9eed43381cf7d58e4563a951364255fc776707a043542a7b997d27c646ee6fb6", 1540280280L); byte[] signature = BaseEncoding.base64().decode(certInfo.getSignature()); @@ -63,7 +63,7 @@ void testSignatureWithDetails() throws SSLPinningException, SignatureException, } @Test - void testSignatureWithCertInfo() throws SSLPinningException, SignatureException, InvalidKeyException { + void testSignatureWithCertInfo() throws Exception { CertificateInfo certInfoIn = new CertificateInfo(); certInfoIn.setName("www.google.com"); certInfoIn.setExpires(1540280280L); @@ -77,7 +77,7 @@ void testSignatureWithCertInfo() throws SSLPinningException, SignatureException, } @Test - void testReadCertificate() throws IOException, SSLPinningException { + void testReadCertificate() throws Exception { File cerFile = File.createTempFile("ssl_pinning", ".cer"); FileWriter fw = new FileWriter(cerFile.getAbsolutePath()); fw.write(new String(BaseEncoding.base64().decode(TEST_CERTIFICATE_BASE64))); @@ -90,7 +90,7 @@ void testReadCertificate() throws IOException, SSLPinningException { } @Test - void testGenerateJsonFile() throws IOException, SSLPinningException, SignatureException, InvalidKeyException { + void testGenerateJsonFile() throws Exception { File jsonFile = File.createTempFile("ssl_pinning", ".json"); File cerFile = File.createTempFile("ssl_pinning", ".cer"); FileWriter fw = new FileWriter(cerFile.getAbsolutePath()); @@ -105,12 +105,13 @@ void testGenerateJsonFile() throws IOException, SSLPinningException, SignatureEx scanner.close(); // replace current signature with static string, it is always different generatedJson = generatedJson.replace(certInfo.getSignature(), "SIGNATURE"); - assertEquals("{\n" + - " \"name\" : \"www.google.com\",\n" + - " \"fingerprint\" : \"nu1DOBz31Y5FY6lRNkJV/HdnB6BDVCp7mX0nxkbub7Y=\",\n" + - " \"expires\" : 1540280280,\n" + - " \"signature\" : \"SIGNATURE\"\n" + - "}", generatedJson); + assertEquals(""" + { + "name" : "www.google.com", + "fingerprint" : "nu1DOBz31Y5FY6lRNkJV/HdnB6BDVCp7mX0nxkbub7Y=", + "expires" : 1540280280, + "signature" : "SIGNATURE" + }""", generatedJson); assertTrue(cerFile.delete()); assertTrue(jsonFile.delete()); }