Akka-CQRS(13)- SSL/TLS for gRPC and HTTPS:自簽名證書產生和使用

  到現在,我們已經完成了POS平台和前端的網絡集成。不過,還是那句話:平台系統的網絡安全是至關重要的。前一篇博客里我們嘗試實現了gRPC ssl/tls網絡連接,但測試時用的證書如何產生始終沒有搞清楚。現在akka-http開發的ws同樣面臨HTTPS的設置和使用問題。所以,特別抽出這篇博文討論一下数字證書的問題。

在正式的生產環境里数字證書應該是由第三方公證機構CA簽發的,我們需要向CA提出申請。数字證書的申請、簽發和驗證流程如下:

1) 服務⽅ S 向第三⽅方機構CA提交公鑰、組織信息、個⼈信息(域名)等資料提出認證申請 (不需要提供私鑰) 2) CA 通過各種手段驗證申請者所提供信息的真實性,如組織是否存在、 企業是否合法,是否擁有域名的所有權等 3) 如信息審核通過,CA 會向申請者簽發認證文件-證書。 證書包含以下信息:申請者公鑰、申請者的組織信息和個⼈信息、簽發機構 CA 信息、有效時間、證書序列號等信息的明⽂,同時包含一個簽名的產⽣生算法:首先,使用散列函數計算出證書中公開明文信息的信息摘要,然後, 採用 CA 的私鑰對信息摘要進⾏加密,這個密⽂就是簽名了 4) 客戶端 C 向服務器 S 發出請求時,S 返回證書文件 5) 客戶端 C 讀取證書中的相關的明⽂信息,采⽤相同的散列函數計算得到信息摘要, 然後,利用對應 CA 的公鑰解密簽名數據,對比證書的信息摘要,如果一致,則可以確認證書的合法性,即公鑰合法 6) 客戶端 C 然後檢驗證書相關的域名信息、有效時間等信息 7) 客戶端 C 應內置信任 CA 的證書信息(包含公鑰),如果 CA 不被信任,則找不到對應 CA 的證書,證書也會被判定非法 8) 內置 CA 對應的證書稱為根證書,頒發者和使⽤者相同,用 CA ⾃⼰的私鑰簽名,即⾃簽名證書(此證書中的公鑰即為 CA 的公鑰,可以使用這個公鑰對證書的簽名進行校驗,⽆需另外⼀份證書)

服務器端在通信中建立SSL加密渠道過程如下:

1)客戶端 C 發送請求到服務器端 S 2) 服務器端 S 返回證書和公開密鑰到 C,公開密鑰作為證書的一部分傳送 3)客戶端 C 檢驗證書和公開密鑰的有效性,如果有效,則⽣成共享密鑰並使⽤公開密鑰加密發送到服務器端 S 4) 服務器端 S 使⽤私有密鑰解密數據,並用收到的共享密鑰加密數據,發送到客戶端 C 5) 客戶端 C 使⽤用共享密鑰解密數據 6) SSL 加密通信渠道建立 ...

應該說,需要在客戶端進行認證的應用場景不多。這種情況需要在客戶端存放数字證書。像支付寶和一些銀行客戶端一般都需要安裝證書。

好了,還是回到如何產生自簽名證書示範吧。下面是一個標準的用openssl命令產生自簽名證書流程:

在產生證書和密鑰的過程中所有系統提問回答要一致。我們先假設密碼統一為:123456

1、生成根證書私鑰: rootCA.key:  openssl genrsa -des3 -out rootCA.key 2048 

2、根證書申請 rootCA.csr:openssl req -new -key rootCA.key -out rootCA.csr

3、用申請rootCA.csr生成根證書 rootCA.crt:openssl x509 -req -days 365 -sha256 -extensions v3_ca -signkey rootCA.key -in rootCA.csr -out rootCA.crt

4、pem根證書 rootCA.pem:openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.pem

5、創建⼀個v3.ext⽂件,目的是產生X509 v3證書,主要目的是指定subjectAltName選項:

  authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment subjectAltName = @alt_names [alt_names] DNS.1 = localhost IP.1 = "192.168.11.189" IP.5 = "192.168.0.189" IP.2 = "132.232.229.60" IP.3 = "118.24.165.225" IP.4 = "129.28.108.238"

注意subjectAltName,這些都是可以信任的域名或地址。

6、構建證書密鑰 server.key:openssl req -new -sha256 -nodes -out server.csr -newkey rsa:2048 -keyout server.key

7、用根證書rootCA產生自簽證書 server.crt:openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.crt -days 500 -sha256 -extfile v3.ext

上面這個過程需要不斷重複回答同樣的問題,很煩。可以用配置文件來一次性產生:

先構建一個ssl.cnf文件:

  [req]
  prompt = no
  default_bits = 4096
  default_md = sha256
  distinguished_name = dn
  x509_extensions = v3_req
  [dn]
  C=CN
  ST=GuangDong
  L=ShenZhen
  O=Bayakala
  OU=POS
  CN=www.bayakala.com
  emailAddress=admin@localhost
  [v3_req]
  keyUsage=keyEncipherment, dataEncipherment
  extendedKeyUsage=serverAuth
  subjectAltName=@alt_names
  [alt_names]
  DNS.1 = localhost
  IP.1 = "192.168.11.189"  
  IP.5 = "192.168.0.189"
  IP.2 = "132.232.229.60"
  IP.3 = "118.24.165.225"
  IP.4 = "129.28.108.238"

然後:openssl req -new -newkey rsa:2048 -sha1 -days 3650 -nodes -x509 -keyout server.key -out server.crt -config ssl.cnf

一個指令同時產生需要的server.crt,server.key。

除aubjectAltName外還要關注CN這個字段,它就是我們經常會遇到系統提問:你確定信任“域名”嗎?中這個域名,也就是對外界開放的一個使用了数字證書的域名。

把crt,key抄寫到main/resources目錄下,然後在gRPC服務器配置證書:

trait gRPCServer { val serverCrtFile = new File(getClass.getClassLoader.getResource("server.crt").getPath) val serverKeyFile = new File(getClass.getClassLoader.getResource("server.key").getPath) def runServer(service: ServerServiceDefinition): Unit = { val server = NettyServerBuilder .forPort(50051) .addService(service) .useTransportSecurity(serverCrtFile,serverKeyFile) .build .start // make sure our server is stopped when jvm is shut down
    Runtime.getRuntime.addShutdownHook(new Thread() { override def run(): Unit = { server.shutdown() server.awaitTermination() } }) } }

啟動gRPC服務,運作正常。在看看客戶端代碼:

    val clientCrtFile = new File(getClass.getClassLoader.getResource("server.crt").getPath)
 //或者   val clientCrtFile = new File(getClass.getClassLoader.getResource("rootCA.pem").getPath)

//這樣也行 val clientCrtFile: InputStream = getClass.getClassLoader.getResourceAsStream("rootCA.pem")

    val sslContextBuilder = GrpcSslContexts.forClient().trustManager(clientCrtFile)

    //build connection channel
    val channel = NettyChannelBuilder
      .forAddress("192.168.11.189",50051)
      .negotiationType(NegotiationType.TLS)
      .sslContext(sslContextBuilder.build())
//      .overrideAuthority("192.168.1.3")
      .build()

測試連接,gRPC SSL/TLS成功!

現在開始了解一下https證書的配置使用方法吧。看了一下akka-http關於server端HTTPS設置的例子,證書是嵌在HttpsConnectionContext類型裏面的。還有就是akka-http使用的https證書格式只支持pkcs12,所以需要把上面用openssl產生的自簽名證書server.crt轉成server.p12。這個轉換又需要先產生證書鏈certificate-chain chain.pem:

1)產生certificate-chain:  cat server.crt rootCA.crt > chain.pem

2) server.crt轉換成server.p12: openssl pkcs12 -export -name servercrt -in chain.pem -inkey server.key -out server.p12

https server 測試代碼:

//#imports
import java.io.InputStream import java.security.{ SecureRandom, KeyStore } import javax.net.ssl.{ SSLContext, TrustManagerFactory, KeyManagerFactory } import akka.actor.ActorSystem import akka.http.scaladsl.server.{ Route, Directives } import akka.http.scaladsl.{ ConnectionContext, HttpsConnectionContext, Http } import akka.stream.ActorMaterializer import akka.http.scaladsl.Http import akka.http.scaladsl.server.Directives._ //#imports


object HttpsDemo extends App { implicit val httpSys = ActorSystem("httpSystem") implicit val httpMat = ActorMaterializer() implicit val httpEC = httpSys.dispatcher val password: Array[Char] = "123456".toCharArray // do not store passwords in code, read them from somewhere safe!
 val ks: KeyStore = KeyStore.getInstance("PKCS12") val keystore: InputStream = getClass.getClassLoader.getResourceAsStream("server.p12") ks.load(keystore, password) val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance("SunX509") keyManagerFactory.init(ks, password) val tmf: TrustManagerFactory = TrustManagerFactory.getInstance("SunX509") tmf.init(ks) val sslContext: SSLContext = SSLContext.getInstance("TLS") sslContext.init(keyManagerFactory.getKeyManagers, tmf.getTrustManagers, new SecureRandom) val https: HttpsConnectionContext = ConnectionContext.https(sslContext) val route = get { complete("Hello world!") } val (port, host) = (50081,"192.168.11.189") val bindingFuture = Http().bindAndHandle(route,host,port,connectionContext = https) println(s"Https Server running at $host $port. Press any key to exit ...") scala.io.StdIn.readLine() bindingFuture.flatMap(_.unbind()) .onComplete(_ => httpSys.terminate()) }

用safari連接https://192.168.11.189:50081/, 彈出窗口一堆廢話后還是成功連接上了。

 

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

您可能也會喜歡…