sgykfjsm.github.com

GoでSSL証明書の情報を取得したい

タイトルの通り。GoでSSL証明書の検証を行う簡単なコードを書く必要が出てきたので、それを実現するための検証の記録を残す。

ここでいう「検証」というのは、有効期限が現在より未来かどうかを調べるやり方を意味する。

検証材料のため、自己発行証明書を作る

自己発行証明書は以下のコマンドで簡単に発行できる。

1
$ openssl req -x509 -nodes -newkey rsa:2048 -keyout server.rsa.key -out server.rsa.crt -days 3650 -batch

コマンドの詳細は割愛するけど、とりあえずコレで10年間利用できる(もちろん正当な証明書としては利用できない)証明書を発行することができる。

これを実行しても良いけど、せっかくなのでgopherっぽくやるなら、以下のような感じになる。

1
2
3
$ go run $GOROOT/src/crypto/tls/generate_cert.go -host $(hostname)
2017/09/21 23:25:18 written cert.pem
2017/09/21 23:25:18 written key.pem

ログの通りにファイルが生成されている。

1
2
3
4
5
6
$ ls
total 24
drwxr-xr-x   5 sgyk  staff   170  9 21 23:48 ./
drwxr-xr-x  13 sgyk  staff   442  9 21 23:05 ../
-rw-r--r--   1 sgyk  staff  1099  9 21 23:25 cert.pem
-rw-------   1 sgyk  staff  1675  9 21 23:25 key.pem

念のために検証しておく。まずは鍵の確認。

1
2
$ openssl rsa -in key.pem -check -noout
RSA key ok

証明書の確認

1
2
3
4
5
6
7
8
9
10
11
12
$ openssl x509 -in ./cert.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            9c:4e:90:ec:b8:f2:ac:91:91:04:19:e3:2c:d0:cf:b6
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: O=Acme Co
        Validity
            Not Before: Sep 13 15:05:11 2017 GMT
            Not After : Sep 13 15:05:11 2018 GMT
(snip)

とりあえず大丈夫そう。

実践

こんな記事を書いといてアレなんだけど、色々試行錯誤した結果、大変泥臭いやり方になった。というのも、有効期限(notAfterの日付)をスマートに取り出すことができなかった。GoのOpenSSLバインディングだと https://github.com/spacemonkeygo/openssl が良さそうだけど、これだと取り出される情報に日付が含まれていなかった。

ということで、結局、以下のようにシェルコマンドでnotAfterの日付を取り出して整形するというやり方になった。

シェルで表現するとこうなる。

1
2
3
4
# $ openssl x509 -in ./cert.pem -noout -enddate
# notAfter=Sep 21 14:25:18 2018 GMT
$ openssl x509 -in ./cert.pem -noout -enddate | cut -d'=' -f2
Sep 21 14:25:18 2018 GMT

これと同じことをGoでやると、だいたいこうなる

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
  "bytes"
  "fmt"
  "log"
  "os/exec"
  "strings"
  "time"
)

func main() {
  line := strings.Split("openssl x509 -in ./cert.pem -noout -enddate", " ")
  cmd := exec.Command(line[0], line[1:]...)
  out, err := cmd.CombinedOutput()
  if err != nil {
      log.Fatal(err)
  }
  out = bytes.Trim(out, "notAfter=")
  out = bytes.TrimSpace(out)

  const layout = "Jan 2 15:04:05 2006 MST"
  notAfter, err := time.Parse(layout, string(out))
  if err != nil {
      log.Fatal(err)
  }

  fmt.Println("notAfter is", notAfter)
  if notAfter.After(time.Now()) {
      fmt.Println("Certification is valid")
  } else {
      fmt.Println("Certification is invalid")
  }
}

うーん長い。果たしてコレはどうなのか…でもまぁとりあえずはコレで目的は達成できるので、まぁ良しとしよう。

参考