到現在,我們已經完成了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/, 彈出視窗一堆廢話後還是成功連線上了。