web画面でSSL用CSRをつくる(Ruby, sinatra)

# はじめに

2年ほど前にSSL証明書をシェルコマンドで手作りしていてそれを変えたいと思っていました。残念ながらその時は力及ばず挫折しました。なんとなく昨日ふと思い立って続きをやってみたところなんかそれっぽくなったので、記録を残しておきたいと思います。

# 概要

本記事は以下の内容について書きます

1. rubyで秘密鍵の作成
1. rubyでCSRの作成
1. sinatra + hamlで入力フォームの作成
1. sinatra + hamlで表示(views)の作成

完成形としては、ブラウザでアクセスできる簡単なページを作成し、
そこでCSRに必要な情報を送信すると秘密鍵の文字列とCSRの文字列が表示される。
というものです。

## 注意点

本記事で作成された秘密鍵とCSRについては、データの確認中です。**SSLの申請用途として使用し得るものなのか検証ができていません**(ので誰か詳しい人教えて下さい)。
テストやエラー処理は記述していません。

## 環境

rubyがちょっと古い点に関しては後述します

* ruby 2.1.5
* sinatra 1.4.6
* MAC OS 10.11.1
* haml 4.0.7

# Rubyで秘密鍵の作成

参考URL: http://docs.ruby-lang.org/ja/1.8.7/class/OpenSSL=3a=3aPKey=3a=3aRSA.html

のちほど全体のssl.rbを記載しますが、秘密鍵の部分のクラスは以下のとおりです。
かなりの部分を参考URLを参考にしています

シェルコマンドで言うと

```
$ openssl genrsa -rand （擬似乱数ファイル名） -des3 （キー長） > （秘密鍵ファイル名）
```

に当たる部分です。

```ssl.rb
class PrivateKey
    SEED_MACHINE_PATH = '/dev/random'   # ランダム生成器の指定
    ENTROPY = 100
    BITSIZE = 2048                      # 暗号化強度

    # fqdnだけ指定します。
    def initialize(fqdn)
      @fqdn = fqdn
      @rsa  = ""
    end

    # 秘密鍵のデータ作成部分
    def generate
      # シードの登録
      OpenSSL::Random.seed(File.read(SSL::PrivateKey::SEED_MACHINE_PATH, SSL::PrivateKey::ENTROPY))
      @rsa = OpenSSL::PKey::RSA.generate(SSL::PrivateKey::BITSIZE)
    end
  end
```

参考URLのところに書いてあったとおり、ランダムなシードを設定して指定したbitsizeの秘密鍵を作成します。

# rubyでCSRの作成

参考URL: http://docs.ruby-lang.org/ja/2.0.0/class/OpenSSL=3a=3aX509=3a=3aRequest.html

こちらも秘密鍵同様、該当部分を一部抜粋します
いわゆる

```
$ openssl req -new -key （秘密鍵ファイル名） -out （CSRファイル名）
```

に当たる部分です。

```ssl.rb
class CSR
    # いわゆるsubjectデータと秘密鍵を指定
    def initialize(fqdn, country, state, locality, organization, organization_unit, key)
      @fqdn              = fqdn
      @country           = country
      @state             = state
      @locality          = locality
      @organization      = organization
      @organization_unit = organization_unit
      @key               = key
    end

    # 作成のところ
    def generate
      @rsa = OpenSSL::PKey::RSA.new(@key)
      @csr = OpenSSL::X509::Request.new
      @csr.subject = add_subject()
      @csr.version = 0
      @csr.public_key = @rsa.public_key
      @csr.sign(@rsa, "sha256")
      @csr
    end

    private
    # subjectの追加
    def add_subject
      name = OpenSSL::X509::Name.new
      name.add_entry('C',  @country)
      name.add_entry('ST', @state)
      name.add_entry('L',  @locality)
      name.add_entry('O',  @organization)
      name.add_entry('OU', @organization_unit)
      name.add_entry('CN', @fqdn)
      name
    end
  end
```

気をつけたポイントは、参考サイトだとattributeを追加して代替アドレスを指定している部分があるんですが、CSRを普通にshのコマンドで作成したものとそこだけ差異がでるので思い切って削ってみました。

あとは、基本去年からsha2がデフォルトとなっているのでsha256の設定も入れました。

## ssl.rbの全体

全体のクラスのコードは以下の様な感じです。

```ssl.rb
module SSL
  require 'openssl'

  class PrivateKey
    SEED_MACHINE_PATH = '/dev/random'
    ENTROPY = 100
    BITSIZE = 2048

    def initialize(fqdn)
      @fqdn = fqdn
      @rsa  = ""
    end

    def generate
      OpenSSL::Random.seed(File.read(SSL::PrivateKey::SEED_MACHINE_PATH, SSL::PrivateKey::ENTROPY))
      @rsa = OpenSSL::PKey::RSA.generate(SSL::PrivateKey::BITSIZE)
    end

  class CSR
    def initialize(fqdn, country, state, locality, organization, organization_unit, key)
      @fqdn              = fqdn
      @country           = country
      @state             = state
      @locality          = locality
      @organization      = organization
      @organization_unit = organization_unit
      @key               = key
    end

    def generate
      @rsa = OpenSSL::PKey::RSA.new(@key)
      @csr = OpenSSL::X509::Request.new
      @csr.subject = add_subject()
      @csr.version = 0
      @csr.public_key = @rsa.public_key
      @csr.sign(@rsa, "sha256")
      @csr
    end

    private
    def add_subject
      name = OpenSSL::X509::Name.new
      name.add_entry('C',  @country)
      name.add_entry('ST', @state)
      name.add_entry('L',  @locality)
      name.add_entry('O',  @organization)
      name.add_entry('OU', @organization_unit)
      name.add_entry('CN', @fqdn)
      name
    end
  end
end
```

# sinatra + hamlで入力フォームの作成

ssl.rbでクラスを作成したので、フォームから入力した値を利用してsslクラスのライブラリを呼びだそうとしてみます。

## はまった点

私の環境だと ruby2.2.3からsinatraを起動すると、起動のコメントはでますが、localhost:4567につながらないという事象が起こりました。Ctrl+Cで停止させてもコンソールが止まってしまって戻ってこず、結局ruby2.1.5で起動させました。
回避に過ぎないので、解決法があったら誰かおしえて下さい。

## 利用しているgemのインストール

Gemをインストールします。bundle installで一気に入れてしまおうと思いますので

```
$ bundle init
```

して、Gemfileを作成します。

```Gemfile
source "https://rubygems.org"

gem 'sinatra', '1.4.6'
gem 'sinatra-contrib', '1.4.6'
gem 'haml', '4.0.7'
```

こちらを作成したら $ bundle installしてください。

## sinatraの準備&app.rbの作成

sinatraを書きます。基本は下記の公式サイトを参考に。
参考URL: http://www.sinatrarb.com/intro-ja.html

```app.rb
require 'sinatra'
require 'sinatra/reloader'

# ruby2.2.3で動かなかった時の試行錯誤(効果なしのところ)
set :environment, :development
# set :port, 5678
set :bind, '127.0.0.1'

# http://localhost:4567/にアクセスした時に表示する内容
get '/' do
  haml :index
end

# http://localhost:4567/からフォームを入力し
# 遷移した先のcompleteが表示する内容
get '/complete' do
  @fqdn              = params[:fqdn]
  @country           = params[:country]
  @state             = params[:state]
  @locality          = params[:locality]
  @organization      = params[:organization]
  @organization_unit = params[:organization_unit]

  require './lib/ssl.rb'
  rsa = SSL::PrivateKey.new(@fqdn)
  @key = rsa.generate()

  csr = SSL::CSR.new(
    @fqdn,
    @country,
    @state,
    @locality,
    @organization,
    @organization_unit,
    @key)

  @csr = csr.generate()

  haml :complete
end
```

# sinatra + hamlで表示(views)の作成

トップにアクセスした際のhamlは以下のとおりです。
念のためですが、hamlファイルはviewsディレクトリに置くといい感じです

```views/index.haml
<!DOCTYPE html>
%html{:lang => "ja"}
%head
  %meta{:charset => "UTF-8"}/
%body{:style => 'text-align:center;'}
  %h1
    CSR TEST
  %div
    %form{ :action => 'complete', :methods => 'get'}
      %p
        %strong
          FQDN
        %input{:type => 'text', :name => 'fqdn'}
      %p
        %strong
          COUNTRY
        %input{:type => 'text', :name => 'country'}
      %p
        %strong
          STATE
        %input{:type => 'text', :name => 'state'}
      %p
        %strong
          LOCALITY
        %input{:type => 'text', :name => 'locality'}
      %p
        %strong
          ORGANIZATION
        %input{:type => 'text', :name => 'organization'}
      %p
        %strong
          ORGANIZATION-UNIT
        %input{:type => 'text', :name => 'organization_unit'}
      %p
        %input{:type => "submit", :value => '作成する'}
```

つづいてコンプリートページです。

```views/complete.haml
<!DOCTYPE html>
%html{:lang => "ja"}
  %head
    %meta{:charset => "UTF-8"}/
  %body{:style => 'text-align:center;'}
    %h1
      CSR TEST
    %p
      COMPLETE
    %h2
      INPUT
    %div
      %p
        %strong
          FQDN:
        = @fqdn
      %p
        %strong
          COUNTRY:
        = @country
      %p
        %strong
          STATE:
        = @state
      %p
        %strong
          LOCALITY:
        = @locality
      %p
        %strong
          ORGANIZATION:
        = @organization
      %p
        %strong
          ORGANIZATION-UNIT:
        = @organization_unit
    %hr
    %h2
      DATA
    %h3
      PrivateKey
    %div
      %p
        = find_and_preserve("<textarea cols='100' rows='30'>#{@key}</textarea>")
    %h3
      CertificateSigningRequest
    %div
      %p
        = find_and_preserve("<textarea cols='100' rows='30'>#{@csr}</textarea>")
```

textareaタグを本来であれば使いたいところなのですが、これを使ってしまうとhamlが展開された先のHTMLでhamlのインデントが秘密鍵とCSRの内容にしっかりと付いてしまいます。
それをふせぐために find_and_preserveメソッドがhamlに用意されています。この中で内容を展開することで、左側にインデント分の余白が無い文字列データを出すことができます。

# 実際の画面と操作イメージ

超簡素ですみません。

$ ruby app.rb するとサーバが起動します。

■トップページ
<img width="1280" alt="toppage.png" src="https://qiita-image-store.s3.amazonaws.com/0/20865/245f6bba-dd1d-9f29-6dea-64007f72f370.png">

画像の様にいわゆるCSRの情報を書いていきます。
作成するボタンを押すと下記の画像のようなページが出ます。

■作成後ページ
![complete.png](https://qiita-image-store.s3.amazonaws.com/0/20865/dbacedd8-a855-cb8e-4d9e-f82876fe80e8.png)

* input => 入力内容の確認
* DATA>PrivateKey => 秘密鍵のテキスト
* DATA>CertificateSigningRequest => CSRのテキスト

一応ここで作成されたものについてopensslのコマンドでmodulusとtextを確認したところそんなに悪いデータにはなっていなさそうでした。しかし要検証だと思います。

# 最後に

SSLのデータは秘密鍵の取扱いなども有りますので、慎重さが求められます。
しかしながら今までshで手作業でやってた部分もGUIでできるようになると、手間の削減や自分がやらなくて良い作業になったりしますからやらんとあかんのですが、なかなか。

ともあれOpensslの仕様の部分が全然わかっていないのでココらへんの勉強もしっかりしないといけないですね。