admin管理员组

文章数量:1316399

I have a class EncryptionManager that generates public and private keys along with a x509 certificate. These are stored inside a Java keystore.

How would I go about programatically importing this certificate into the computers keychain?

I know the commands to run in a terminal, I just wondered if there is a way to do it from Java without needing to spawn a process that runs bash?

EDIT:

Someone pointed out this might be a duplicate. Let me expand a little.

My KeyStore is also storing other keys and certificates for secure communication with other clients, not just SSL. The other answer tells me how to point the system at my keystore, but not how I would specify which key/cert to use.

Also, does the other answer work for all systems? On a Mac, the cert must be added to the System.keychain using sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/.localhost-ssl/localhost.crt, which I would like to do without spawning a bash process if possible.

I have a class EncryptionManager that generates public and private keys along with a x509 certificate. These are stored inside a Java keystore.

How would I go about programatically importing this certificate into the computers keychain?

I know the commands to run in a terminal, I just wondered if there is a way to do it from Java without needing to spawn a process that runs bash?

EDIT:

Someone pointed out this might be a duplicate. Let me expand a little.

My KeyStore is also storing other keys and certificates for secure communication with other clients, not just SSL. The other answer tells me how to point the system at my keystore, but not how I would specify which key/cert to use.

Also, does the other answer work for all systems? On a Mac, the cert must be added to the System.keychain using sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/.localhost-ssl/localhost.crt, which I would like to do without spawning a bash process if possible.

Share Improve this question edited Jan 29 at 20:30 Robert 42.8k18 gold badges109 silver badges172 bronze badges asked Jan 29 at 10:31 Kris RiceKris Rice 8691 gold badge11 silver badges29 bronze badges 7
  • This question is similar to: SSL and cert keystore. If you believe it’s different, please edit the question, make it clear how it’s different and/or how the answers on that question are not helpful for your problem. – John Williams Commented Jan 29 at 10:37
  • @JohnWilliams see edits please – Kris Rice Commented Jan 29 at 10:45
  • I think you need to use the ProcessBuilder to run indeed a bash command. There is no other way to access the keychain in Mac in Java. I am also doing something similar, not appending like you but reading the content, see here: github/Hakky54/sslcontext-kickstart/blob/master/… for linux you can use /usr/local/share/ca-certificates however the format yould be PEM – Hakan54 Commented Jan 29 at 10:56
  • @Hakan54 Thanks, this looks like the kind of thing I want. I'll have a deeper look through your code, much appreciated – Kris Rice Commented Jan 29 at 10:59
  • @Hakan54 can I ask, does your code work? I tried using a process but got issues with adding to keychain as non-root user. Obviously providing root password or any kind of sudo password plain-text is not going to be secure, and running the application as root has similar security concerns – Kris Rice Commented Jan 29 at 11:01
 |  Show 2 more comments

2 Answers 2

Reset to default 2

I tried couple of different keychains but somehow it is restricted for the following keychain files:

  • /Library/Keychains/System.keychain
  • /System/Library/Keychains/SystemRootCertificates.keychain

The only one which I was able to append additional certificates was to the following keychain file:

  • /Users/[MY_USER]/Library/Keychains/login.keychain-db

The following code should do the trick and additional it will print the console output if you have any errors:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;

public class App {

    public static void main(String[] args) throws IOException {
        ProcessBuilder processBuilder = new ProcessBuilder(
                "security", "add-trusted-cert", 
                "-d", "-r", "trustAsRoot", 
                "-k", "/Users/[MY_USER]/Library/Keychains/login.keychain-db", 
                "/absolute-path/to/my-custom-root-ca.crt");

        Process process = processBuilder.start();

        String consoleOutput = getConsoleOutput(process);
        System.out.println(consoleOutput);
    }

    private static String getConsoleOutput(Process process) throws IOException {
        try (InputStream inputStream = process.getInputStream();
             InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
             BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {

            return bufferedReader.lines()
                    .collect(Collectors.joining(System.lineSeparator()));
        }
    }

}

You don't need to include the sudo command, but you will be prompted to scan either your finger or enter the password. I am not sure how to avoid that part yet, but my assumption is that it is required.

Another sidenote to this solution is that a path to a binary certificate is required. So if you use a keystore file either in memory or on the filesystem you first need to extract the certificates from it and save it on the filesystem. Get the absolute file paths for all of those files and loop through it for every file with the mac command in the code example.

Whilst the above answer is still the accepted answer (Thanks @Hakan54), I would like to provide my full solution for anyone else who comes across this.

I know the question was originally Java, but my application is Scala and the answer is easy enough to translate to Java. I have been trying to auto-configure ssl with generated certificates entirely within my application for use with SSLContext and nginx as part of my question here: https://serverfault/questions/1171697/how-is-best-to-implement-ssl-for-my-setup

The full solution generates self-signed certificates and keys into a Java keystore, loads them into macos keychain and generates nginx config using the certificate and key for ssl.

The solution:

KeyStoreManagement class:

package utils

import .bouncycastle.asn1.x500.X500Name
import .bouncycastle.cert.jcajce.{JcaX509CertificateConverter, JcaX509v3CertificateBuilder}
import .bouncycastle.jce.provider.BouncyCastleProvider
import .bouncycastle.operator.jcajce.JcaContentSignerBuilder

import java.io.{File, FileInputStream, FileOutputStream}
import java.math.BigInteger
import java.security.*
import java.security.cert.Certificate
import java.security.spec.{ECGenParameterSpec, X509EncodedKeySpec}
import java.util.{Base64, Calendar, Date}

trait KeyStoreManagement {

  def createCert(keyPair: KeyPair, keyAlgorithm: String, domain: String): Certificate = {
    val dnName = new X500Name("CN=" + domain)
    val serialNumber = BigInteger.ONE
    val startDate = new Date()
    val calendar = Calendar.getInstance()
    calendar.setTime(startDate)
    calendar.add(Calendar.YEAR, 100)
    val endDate = calendar.getTime

    val contentSigner = new JcaContentSignerBuilder("SHA256with" + keyAlgorithm).build(keyPair.getPrivate)
    val certificateBuilder = new JcaX509v3CertificateBuilder(dnName, serialNumber, startDate, endDate, dnName, keyPair.getPublic)

    JcaX509CertificateConverter().setProvider(new BouncyCastleProvider).getCertificate(certificateBuilder.build(contentSigner))
  }

  def loadKeyStore(keyStorePath: String, keyStorePass: Array[Char]): KeyStore = {
    val keyStore = KeyStore.getInstance("PKCS12")
    val keyFile = new File(keyStorePath)

    if (keyFile.exists) {
      keyStore.load(new FileInputStream(keyFile), keyStorePass)
    } else {
      keyStore.load(null, null)
      keyStore.store(new FileOutputStream(keyFile), keyStorePass)
    }

    keyStore
  }

  def getCertificateString(keyStorePath: String, keyStorePass: Array[Char], alias: String): String = {
    val keyStore = loadKeyStore(keyStorePath, keyStorePass)
    val certificate = keyStore.getCertificate(alias)
    Base64.getEncoder.encodeToString(certificate.getEncoded)
  }

  def createNewCert(keyStore: KeyStore, keyAlgorithm: String, domain: String, alias: String, keyPass: Array[Char], keyStorePath: String, keyStorePass: Array[Char]): Unit = {
    val keyPair = generateKey(keyAlgorithm)
    val cert = createCert(keyPair, keyAlgorithm, domain)
    val chain = Array[Certificate](cert)

    keyStore.setKeyEntry(alias, keyPair.getPrivate, keyPass, chain)

    val keyFile = new File(keyStorePath)
    keyStore.store(new FileOutputStream(keyFile), keyStorePass)
  }

  def toPemFormat(certificate: Certificate): String = {
    "-----BEGIN CERTIFICATE-----\n" + Base64.getEncoder.encodeToString(certificate.getEncoded) + "\n-----END CERTIFICATE-----"
  }

  def toPemFormat(privateKey: PrivateKey): String = {
    "-----BEGIN PRIVATE KEY-----\n" + Base64.getEncoder.encodeToString(privateKey.getEncoded) + "\n-----END PRIVATE KEY-----"
  }

  def toPemCombined(certificate: Certificate, privateKey: PrivateKey): String = {
    toPemFormat(privateKey) + "\n" + toPemFormat(certificate)
  }

  def generateKey(keyAlgorithm: String): KeyPair = {
    keyAlgorithm match {
      case "ECDSA" =>
        val keyPairGenerator = KeyPairGenerator.getInstance("EC", "SunEC")
        val ecParameterSpec = new ECGenParameterSpec("secp256r1")
        keyPairGenerator.initialize(ecParameterSpec)
        keyPairGenerator.genKeyPair()
      case "RSA" =>
        val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
        keyPairGenerator.initialize(2048)
        keyPairGenerator.genKeyPair()
    }
  }

}

SSLManager class:

package server

import database.DatabaseUtil
import server.SSLManager._
import server.WebServer.SERVER_ID
import utils.OS.OSVersion.*
import utils.{KeyStoreManagement, OS}

import java.io.File
import java.security.*
import javax.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}
import scala.language.postfixOps
import scala.sys.process.stringSeqToProcess

object SSLManager {

  private val SSL_ROOT: String = System.getProperty("user.home") + File.separator + ".localhost-ssl" + File.separator
  val KEY_STORE_PATH: String = SSL_ROOT + "keystore_name.pkcs12"
  val SSL_CERTIFICATE_PATH: String = SSL_ROOT + "localhost.crt"
  val SSL_KEY_PATH: String = SSL_ROOT + "localhost.key"
}

/**
 * Class for generating SSL Certificates into a Java KeyStore and adding them to the system keychain
 */
class SSLManager extends KeyStoreManagement with FileHandler {

  private val KEY_PASS = DatabaseUtil.hashString("key_pass").toCharArray
  private val KEY_STORE_PASS = DatabaseUtil.hashString("key_store_pass").toCharArray
  private val ALIAS = "sslCertificateAlias"
  private var sslContext: SSLContext = _

  def init(): Unit = {
    if (!fileExists(KEY_STORE_PATH)) {
      mkDirs(SSL_ROOT)
      // generate RSA key pair
      val keyPair = generateKey("RSA")
      // generate certificate
      val certificate = createCert(keyPair, "RSA", "localhost")
      val keyPem = toPemFormat(keyPair.getPrivate)
      val certPem = toPemFormat(certificate)
      writeFile(SSL_CERTIFICATE_PATH, Seq(certPem))
      writeFile(SSL_KEY_PATH, Seq(keyPem))

      // load the key into keystore and create
      val command = s"openssl pkcs12 -export -out $KEY_STORE_PATH -in ${SSL_ROOT + "localhost.crt"} -inkey ${SSL_ROOT + "localhost.key"} -passout pass:"
        + DatabaseUtil.hashString(SERVER_ID)
      command.split(" ").toSeq !!

      // register certificate with OS
      registerWithOS()
    }
    val keyStore = loadKeyStore(KEY_STORE_PATH, KEY_STORE_PASS)
    val entry = keyStore.getEntry(ALIAS, new KeyStore.PasswordProtection(KEY_PASS))

    val cert = keyStore.getCertificate(ALIAS)
    val privateKeyEntry = classOf[KeyStore.PrivateKeyEntry].cast(entry)

    val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509")
    keyManagerFactory.init(keyStore, KEY_STORE_PASS)

    val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509")
    tmf.init(keyStore)

    sslContext = SSLContext.getInstance("TLS")
    sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom)
  }

  private def registerWithOS(): Unit = {
    if (OS.getOS == MAC) {
      val command = s"security add-trusted-cert -d -r trustRoot -k " +
        s"${System.getProperty("user.home")}/Library/Keychains/login.keychain-db ${SSL_ROOT + "localhost.crt"}"
      command.split(" ").toSeq!!
    } else if (OS.getOS == WINDOWS) {
        //TODO
    } else {
        /TODO
    }
  }

  def getSSLContext: SSLContext = sslContext

}

Nginx class:

package utils

import server.FileHandler
import server.SSLManager.*
import utils.OS.OSVersion.*

import java.io.File
import scala.language.postfixOps
import scala.sys.process.stringSeqToProcess

object Nginx extends FileHandler {

  private val UI_ROOT: String = System.getProperty("user.home") + File.separator + "ui"

  def init(): Unit = {
    OS.getOS match {
      case MAC =>
        if (!fileExists("/usr/local/etc/nginx")) {
          throw new RuntimeException("nginx is not installed. Please install nginx before launching.")
        } else if (!fileExists("/usr/local/etc/nginx/servers/default.conf")) {
          // write config
          val config = readResource("nginx/default")
            .replace("$SSL_CERTIFICATE", SSL_CERTIFICATE_PATH)
            .replace("$SSL_KEY", SSL_KEY_PATH)
            .replace("$ROOT", UI_ROOT)
          writeFile("/usr/local/etc/nginx/servers/hydra.conf", Seq(config))
          // restart nginx
          Seq("brew", "services", "restart", "nginx")!!
        }
      case LINUX =>
        //TODO
      case WINDOWS =>
        //TODO
    }
  }

}

Then to use:

    val sslManager = new SSLManager()
    sslManager.init()

    val sslContext = sslManager.getSSLContext
    val https: HttpsConnectionContext = ConnectionContext.httpsServer(sslContext)
    
    // setup nginx
    Nginx.init()

    // create https Engine
    ConnectionContext.httpsServer(() => {
      val engine = sslContext.createSSLEngine()
      engine.setUseClientMode(false)
      engine.setNeedClientAuth(true)
      engine
    })

    Http().newServerAt("localhost", 8080).enableHttps(https).bind(routes.routes)
      .onComplete {
        case Success(binding) =>
          val address = binding.localAddress
          SYSTEM.log.info(s"Server is listening on ${address.getHostString}:${address.getPort}")
        case Failure(ex) =>
          SYSTEM.log.error("Server could not be started", ex)
          stop()
      }
  }

default nginx config (obviously the routes are specific to my application):

server {
    server_name www.localhost;

    ssl_certificate $SSL_CERTIFICATE;
    ssl_certificate_key $SSL_KEY;

    index index.html index.htm;
    root $ROOT;

    location / {
        try_files $uri.html $uri/index.html
        @public
        @nextjs;
        add_header Cache-Control "public, max-age=3600";
    }

    location @public {
        add_header Cache-Control "public, max-age=3600";
    }

    location /ping {
        proxy_pass https://localhost:8080;
    }

    location /auth {
        proxy_pass https://localhost:8080;
    }

    location @nextjs {
        proxy_pass https://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
    
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
}

本文标签: