メモ:Rで証明書の情報を扱うときはopensslパッケージ

この影響を調べようと思ってopensslコマンドでちまちまやってたわけですが、

Rでも扱えないかな?と思ったらopensslパッケージがあったのでその使い方のメモ。

github.com

download_ssl_cert()

あるウェブサーバから証明書チェーンを取得するにはdownload_ssl_cert()が使えます。

library(openssl)

chain <- download_ssl_cert("www.r-project.org", 443)
chain
#> [[1]]
#> [x509 certificate] *.r-project.org
#> md5: 9aa98308c79a93da348a6a228b2e48ab
#> sha1: 35c5d904e830005b852c2c1b6e5db00773524aad
#> 
#> [[2]]
#> [x509 certificate] Starfield Secure Certificate Authority - G2
#> md5: 1cd0976187316dde07259fd6267e2f0e
#> sha1: 7edc376dcfd45e6ddf082c160df6ac21835b95d4
#> 
#> [[3]]
#> [x509 certificate] Starfield Root Certificate Authority - G2
#> md5: 497bf0a8bc53a0846d7fd29499f558e9
#> sha1: 9565b778c8a50eb4fefd45c8a658dde2411ead0a
#> 
#> [[4]]
#> [x509 certificate] 
#> md5: 324a4bbbc863699bbe749ac6dd1d4624
#> sha1: ad7e1c28b064ef8f6003402014c3d0e3370eb58a

この4つ並んでいる[x509 certificate]というやつは何者なんでしょう。

is(chain[[1]])
#> [1] "cert"
str(chain[[1]])
#> List of 8
#>  $ subject    : chr "CN=*.r-project.org,OU=Domain Control Validated"
#>  $ issuer     : chr "CN=Starfield Secure Certificate Authority - G2,OU=http://certs.starfieldtech.com/repository/,O=Starfield Technologies\\, Inc.,L"| __truncated__
#>  $ algorithm  : chr "sha256WithRSAEncryption"
#>  $ signature  : raw [1:256] 6c a9 bd d1 ...
#>  $ validity   : chr [1:2] "Aug 18 09:39:38 2015 GMT" "Aug 18 09:39:38 2018 GMT"
#>  $ self_signed: logi FALSE
#>  $ alt_names  : chr [1:2] "*.r-project.org" "r-project.org"
#>  $ pubkey     :List of 5
#>   ..$ type       : chr "rsa"
#>   ..$ size       : int 2048
#>   ..$ ssh        : chr "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQ/cx8HyMW9SCLJsC/UKaTh/RskQ33Leicy0prlBc2D ..."
#>   ..$ fingerprint:Classes 'hash', 'md5'  raw [1:16] 6b 37 2d 9b ...
#>   ..$ data       :List of 2
#>   .. ..$ e:Class 'bignum'  raw [1:3] 01 00 01
#>   .. ..$ n:Class 'bignum'  raw [1:257] 00 d0 fd cc ...

よくわかりませんが、certというクラスのオブジェクトのようです。専用のメソッドもいくつか用意されています。

methods(class = "cert")
#>  [1] $              $<-            [              [[            
#>  [5] [[<-           [<-            as.environment as.list       
#>  [9] names          print          str           
#> see '?methods' for accessing help and source code

certpubkey

しかし、strしたときの見た目はリストでありながら、実はrawなのでうまくリストっぽく扱うことができません。

purrr::flatten(chain[[1]])
#> Error: `.x` must be a list (raw)
typeof(chain[[1]])
#> [1] "raw"

どうすれば扱えるのかというと、as.list.certの中身を覗いてみると、cert_info()cert_pubkey()という関数で情報を取り出せるようです。(エクスポートされていない関数)

openssl:::as.list.cert
#> function (x, ...) 
#> {
#>     cert <- x
#>     info <- cert_info(cert)
#>     info$pubkey <- cert_pubkey(cert)
#>     info
#> }
#> <environment: namespace:openssl>

cert_info()のほうは分かりやすい感じです。

openssl:::cert_info(chain[[1]])
#> $subject
#> [1] "CN=*.r-project.org,OU=Domain Control Validated"
#> 
#> $issuer
#> [1] "CN=Starfield Secure Certificate Authority - G2,OU=http://certs.starfieldtech.com/repository/,O=Starfield Technologies\\, Inc.,L=Scottsdale,ST=Arizona,C=US"
#> 
#> $algorithm
#> [1] "sha256WithRSAEncryption"
#> 
#> $signature
#>   [1] 6c a9 bd d1 96 3b 70 99 5a 81 cb 20 28 f1 c9 65 bf ae 31 84 8e d3 a1
#>  [24] c7 13 cc bc 6b 4b 76 73 16 a9 2b 5a b3 54 a6 54 bb da 3d de 4c b6 39
#>  [47] b7 6e a0 09 8c e0 2b 14 5b 81 64 2b e1 99 54 7a 2c e2 b1 82 d4 7e aa
#>  [70] 5b 1f 0c 49 47 91 f3 ea 56 4a b7 6e 2f 09 68 a7 5c 39 ec 22 92 91 84
#>  [93] 49 62 9d 5f ee bd cc 13 76 02 71 ea 10 1d a3 ac 1a 8a 5e fb 56 38 aa
#> [116] b5 58 59 d0 25 95 98 25 cb 4e 63 64 10 de 5e bc 8f e1 b3 5a e3 c7 87
#> [139] b5 f9 eb d1 aa 79 69 35 42 5c 60 c2 39 2a 45 94 2d fd 5a 58 0c b2 ae
#> [162] 45 0d 1c 10 98 16 1f 53 ff 0a d2 bb 3f b0 00 c7 95 df 40 07 ab 3f 3d
#> [185] 06 1b da 6f bb 49 79 04 18 92 a7 72 c7 22 8a 4f bd 35 3f c1 2d b0 94
#> [208] 3d 86 96 66 c0 40 cc fc 42 dc df 84 55 99 87 cc e1 0c e3 e7 16 c9 23
#> [231] 41 95 27 1f 95 40 12 4a 59 3d c9 9c fe f0 78 da 3e a1 3c c9 ff 73 53
#> [254] aa fb 62
#> 
#> $validity
#> [1] "Aug 18 09:39:38 2015 GMT" "Aug 18 09:39:38 2018 GMT"
#> 
#> $self_signed
#> [1] FALSE
#> 
#> $alt_names
#> [1] "*.r-project.org" "r-project.org"

cert_pubkey()のほうはpubkeyというクラスで公開鍵の情報を取り出します。

openssl:::cert_pubkey(chain[[1]])
#> [2048-bit rsa public key]
#> md5: 6b372d9b5c5759fbb28baf03c609730a
is(openssl:::cert_pubkey(chain[[1]]))
#> [1] "pubkey"

これをリストにするのはちょっとややこしそうです。

openssl:::as.list.pubkey
#> function (x, ...) 
#> {
#>     pubkey <- x
#>     data <- decompose(pubkey)
#>     type <- ifelse(inherits(pubkey, "ed25519"), "ed25519", pubkey_type(pubkey))
#>     size <- pubkey_bitsize(pubkey)
#>     header <- switch(type, rsa = "ssh-rsa", dsa = "ssh-dss", 
#>         ed25519 = "ssh-ed25519", ecdsa = paste0("ecdsa-sha2-nistp", 
#>             substring(data$curve, 3)), stop("Unsupported keytype: ", 
#>             type))
#>     fp <- unlist(unname(fpdata(pubkey)))
#>     list(type = type, size = size, ssh = paste(header, base64_encode(fp)), 
#>         fingerprint = md5(fp), data = data)
#> }
#> <environment: namespace:openssl>

このうち、$data$eがexponent、$data$nがmodulusであるようです。

as.list(openssl:::cert_pubkey(chain[[1]]))
#> $type
#> [1] "rsa"
#> 
#> $size
#> [1] 2048
#> 
#> $ssh
#> [1] "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQ/cx8HyMW9SCLJsC/UKaTh/RskQ33Leicy0prlBc2Dm3Tk/m1Dn7cpwGZQrsS4ak1IaV5WroeJjWUzEiaJH5Ky4vJOGWJYXoXw5THDFxraaCk4CPd/cQuFkezY6iuEYORuNYoukV9xgXhfaGJN4K8Gan6Hi8syY6HqnOiSt887GUZckO7TQF3RishPlbLzx0G17inlhigDFtoFenG/x043EwkIFJ6pz1jA8h/Us29Rw1+853AwNZ/nOptaUFku6W3uXgf4TLYUn6NzPSKY6lJ34gpSoBVQIIU8hjz2Y3cwlIzgQnrw2qlzDmFmiZtjPTaMrvBzs5QHfsgOBHvc5j/"
#> 
#> $fingerprint
#> md5 6b:37:2d:9b:5c:57:59:fb:b2:8b:af:03:c6:09:73:0a 
#> 
#> $data
#> $data$e
#> [b] 65537
#> $data$n
#> [b] 2638272027041747783761783549979133009904993477539266489148528104756141183116161853163984969825794002213359659579336056608215733466383159766139002018736633037788760944415871039657395687796561342102059330892378474802087844506583623983593958964128848966396664577492070253302465477713056839221376905333814306381725584171170790982755178470223655368274409275562361074599147461145420231751245480426593043389988210105506540000926552341283069418023609512462144893525398509145051250972555474545313066085477071580898111192447716762311418704485605783286008932951815748561342469340269836474975416443381026444469976424561917215359

tidyな感じにする

こんな感じ?

flatten_cert <- function(x) {
  result <- as.list(x)
  
  # signatureはraw。characterに変換
  result$signature <- paste(as.character(result$signature), collapse = ":")
  
  # pubkeyはmodulusだけ取り出す
  pubkey <- as.list(result$pubkey)
  result$pubkey  <- NULL
  
  # modulusはbignumなので、rawに変換→characterに変換、というルートにする
  modulus_raw    <- as.raw(as.list(pubkey)$data$n)
  result$modulus <- paste(as.character(modulus_raw), collapse = ":")
  
  # SubjectAltNamesは複数あるのでlistでくるむ
  result$alt_names <- list(result$alt_names)
  
  # validityは有効期限の開始と終了を示す。パースしてそれぞれ別のフィールドに
  validity <- lubridate::parse_date_time2(result$validity, "mdHMSY", tz = "GMT")
  result$validity  <- NULL
  result$notBefore <- validity[1]
  result$notAfter  <- validity[2] 
  
  result
}

purrr::map_df(chain, flatten_cert)
#> # A tibble: 4 × 9
#>                                                                                                            subject
#>                                                                                                              <chr>
#> 1                                                                   CN=*.r-project.org,OU=Domain Control Validated
#> 2 CN=Starfield Secure Certificate Authority - G2,OU=http://certs.starfieldtech.com/repository/,O=Starfield Technol
#> 3       CN=Starfield Root Certificate Authority - G2,O=Starfield Technologies\\, Inc.,L=Scottsdale,ST=Arizona,C=US
#> 4                               OU=Starfield Class 2 Certification Authority,O=Starfield Technologies\\, Inc.,C=US
#> # ... with 8 more variables: issuer <chr>, algorithm <chr>, signature <chr>, self_signed <lgl>,
#> #   alt_names <list>, modulus <chr>, notBefore <dttm>, notAfter <dttm>

見づらい…。たぶんsubjectはもっと分解できる気がするけど、issuerとどう分ければいいのか思いつきませんでした。

感想

むずかしい。Pythonとかだともっとやりやすかったりするんでしょうか。