twitter rss
Goで書くはじめてのデジタル署名
Jun 26, 2017
5 minutes read

最近仕事でデジタル署名を書いていたので、仕組みやロジックなどの基礎的なところから、簡単なサンプルプログラムの実装までをまとめる。 プログラムはGoで書く。

デジタル署名について

デジタル署名は公開鍵暗号方式の一種で、一般的には3つのアルゴリズムから成る。

  • 鍵生成アルゴリズムG

署名者の”鍵ペア”(PK, SK)を生成する。PKは公開する検証鍵(公開鍵)、そしてSKは秘密にする署名鍵(秘密鍵)である。

  • 署名生成アルゴリズムS

メッセージmと署名鍵SKを入力とし、署名σを生成する。

  • 署名検証アルゴリズムV

メッセージm、検証鍵PK、署名σを入力とし、承認または拒否を出力する。

今回、鍵生成 -> 署名生成 -> 署名検証という一連の流れを、サンプルコードで実装しました。解説はコード内のコメントに書いてます。これらの仕組みで何ができるかというと、「送信者が正しい」「伝送の過程においてデータが改ざんされていない」という信頼性を担保することができる。

参考にさせていただきました。

Wikipedia デジタル署名

Wikipedia 公開鍵暗号

1. 鍵生成(RSA暗号化基準の秘密鍵と公開鍵の発行)

opensslコマンドを使用して鍵を発行できる。デフォルトでPEM形式のファイルが作られる。

RSA暗号化基準の秘密鍵の発行

$ openssl genrsa 2048 > private.key

デフォルトだと1024bit、上記の例は2048bitで作成

公開鍵の発行

$ openssl rsa -pubout < private.key > public.key

公開鍵は秘密鍵を元に作られる。 秘密鍵と公開鍵は送信者が作成し、送信者は受信者に公開鍵を渡す。

2. 署名生成

送信者は、メッセージ(送信データ)と秘密鍵を入力とし、署名を生成する。

サンプルコード

実行方法

$ go run main.go
package main

import (
	"bufio"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"fmt"
	"log"
	"os"
)

func main() {
	// 秘密鍵の読込み、ここには上記で発行した秘密鍵のファイルパスを指定する
	privateKeyStr, err := readPrivateKey("private.key")
	if err != nil {
		log.Fatal(err)
	}

	// 例えば送信データ(署名対象のメッセージ)を「Hello World」としてみる
	message := "Hello World"

	// 送信データ(メッセージ)と秘密鍵を入力とし、署名を生成する
	signature, err := createSignature(message, privateKeyStr)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("signature: ", signature)
}

func readPrivateKey(filepath string) (string, error) {
	s := ""
	fp, err := os.Open(filepath)
	if err != nil {
		return "", err
	}
	defer fp.Close()
	scanner := bufio.NewScanner(fp)
	for scanner.Scan() {
		text := scanner.Text()
		if text == "-----BEGIN RSA PRIVATE KEY-----" || text == "-----END RSA PRIVATE KEY-----" {
			continue
		}
		s = s + scanner.Text()
	}
	if err := scanner.Err(); err != nil {
		return "", err
	}

	return s, nil
}

func createSignature(message, keystr string) (string, error) {
	// PEMの中身はDERと同じASN.1のバイナリデータをBase64によってエンコーディングされたテキストなのでBase64でデコードする
	// ゆえにDERエンコード形式に変換
	keyBytes, err := base64.StdEncoding.DecodeString(keystr)
	if err != nil {
		return "", err
	}

	// ASN.1 PKCS#1 DERエンコード形式からRSA秘密鍵を返す
	private, err := x509.ParsePKCS1PrivateKey(keyBytes)
	if err != nil {
		return "", err
	}

	// SHA-256のハッシュ関数を使って送信データのハッシュ値を算出する
	h := crypto.Hash.New(crypto.SHA256)
	h.Write(([]byte)(message))
	hashed := h.Sum(nil)

	// ハッシュ値をRSA秘密鍵を使って暗号化する
	signedData, err := rsa.SignPKCS1v15(rand.Reader, private, crypto.SHA256, hashed)
	if err != nil {
		return "", err
	}

	// 暗号化したバイト列のデータをBase64でエンコーディングし、署名文字列を生成
	signature := base64.StdEncoding.EncodeToString(signedData)
	return signature, nil
}

3. 署名検証

受信者は、メッセージ(受信データ)、送信者からもらった公開鍵、署名を入力とし、検証結果の承認または拒否を出力する。 承認の場合、「送信者が正しい」「送信データが改ざんされていない」ということになる。

サンプルコード

実行方法

$ go run main.go
package main

import (
	"bufio"
	"crypto"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"fmt"
	"log"
	"os"
)

func main() {
	// 公開鍵の読込み、ここには上記で発行した公開鍵のファイルパスを指定する
	publicKeyStr, err := readPublicKey("public.key")
	if err != nil {
		log.Fatal(err)
	}

	// 署名文字列
	signature := "作成された署名文字列をここに入力する"

	// 受信データ
	message := "Hello World"

	// 受信データ(メッセージ)、公開鍵、署名を入力とし、検証結果の承認または拒否を出力する
	if err := verifySignature(message, publicKeyStr, signature); err != nil {
		fmt.Println("err: ", err)
		fmt.Println("拒否")
	} else {
		fmt.Println("承認")
	}
}

func readPublicKey(filepath string) (string, error) {
	s := ""
	fp, err := os.Open(filepath)
	if err != nil {
		return "", err
	}
	defer fp.Close()
	scanner := bufio.NewScanner(fp)
	for scanner.Scan() {
		text := scanner.Text()
		if text == "-----BEGIN PUBLIC KEY-----" || text == "-----END PUBLIC KEY-----" {
			continue
		}
		s = s + text
	}
	if err := scanner.Err(); err != nil {
		return "", err
	}
	return s, nil
}

func verifySignature(message string, keystr string, signature string) error {
	// PEMの中身はDERと同じASN.1のバイナリデータをBase64によってエンコーディングされたテキストなのでBase64でデコードする
	// ゆえにDERエンコード形式に変換
	keyBytes, err := base64.StdEncoding.DecodeString(keystr)
	if err != nil {
		return err
	}

	// DERでエンコードされた公開鍵を解析する
	// 成功すると、pubは* rsa.PublicKey、* dsa.PublicKey、または* ecdsa.PublicKey型になる
	pub, err := x509.ParsePKIXPublicKey(keyBytes)
	if err != nil {
		return err
	}

	// 署名文字列はBase64でエンコーディングされたテキストなのでBase64でデコードする
	signDataByte, err := base64.StdEncoding.DecodeString(signature)
	if err != nil {
		return err
	}

	// SHA-256のハッシュ関数を使って受信データのハッシュ値を算出する
	h := crypto.Hash.New(crypto.SHA256)
	h.Write([]byte(message))
	hashed := h.Sum(nil)

	// 署名の検証、有効な署名はnilを返すことによって示される
	// ここで何をしているかというと、、
	// ①送信者のデータ(署名データ)を公開鍵で復号しハッシュ値を算出
	// ②受信側で算出したハッシュ値と、①のハッシュ値を比較し、一致すれば、「送信者が正しい」「データが改ざんされていない」ということを確認できる
	err = rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hashed, signDataByte)
	if err != nil {
		return err
	}

	return nil
}

少し応用したサンプルコードを書いてみる

受信側を実行しlocalhostにサーバーを立ててから、送信側を実行し、お互いの実行結果を確認。

リクエスト受信側

package main

import (
	"bufio"
	"crypto"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"

	"github.com/unrolled/render"
)

func main() {
// ローカルホストにサーバーをたてる
	http.HandleFunc("/foo", handler)
	http.ListenAndServe("localhost:8080", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
	render := render.New()

	// 公開鍵の読込み
	publicKeyStr, err := readPublicKey("public.key")
	if err != nil {
		log.Fatal(err)
	}

	// 署名文字列の取得
	signature := r.Header.Get("Signature")

	// 署名対象の受信データ
	body, _ := ioutil.ReadAll(r.Body)

	// 署名の検証
	if err := verifySignature(string(body), publicKeyStr, signature); err != nil {
		fmt.Println("err: ", err)
		fmt.Println("拒否")
		render.JSON(w, http.StatusForbidden, nil)
		return
	} else {
		fmt.Println("承認")
	}
	render.JSON(w, http.StatusOK, nil)
	return
}

func readPublicKey(filepath string) (string, error) {
	s := ""
	fp, err := os.Open(filepath)
	if err != nil {
		return "", err
	}
	defer fp.Close()
	scanner := bufio.NewScanner(fp)
	for scanner.Scan() {
		text := scanner.Text()
		if text == "-----BEGIN PUBLIC KEY-----" || text == "-----END PUBLIC KEY-----" {
			continue
		}
		s = s + text
	}
	if err := scanner.Err(); err != nil {
		return "", err
	}
	return s, nil
}

func verifySignature(message string, keystr string, signature string) error {
	keyBytes, err := base64.StdEncoding.DecodeString(keystr)
	if err != nil {
		return err
	}

	pub, err := x509.ParsePKIXPublicKey(keyBytes)
	if err != nil {
		return err
	}

	signDataByte, err := base64.StdEncoding.DecodeString(signature)
	if err != nil {
		return err
	}

	h := crypto.Hash.New(crypto.SHA256)
	h.Write([]byte(message))
	hashed := h.Sum(nil)

	err = rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hashed, signDataByte)
	if err != nil {
		return err
	}
	return nil
}

リクエスト送信側

package main

import (
	"bufio"
	"bytes"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"
)

func main() {
	// 秘密鍵の読込み
	privateKeyStr, err := readPrivateKey("private.key")
	if err != nil {
		log.Fatal(err)
	}

	// 署名対象の送信データをJson形式にしてみる 例)-> {"message":"Hello World","timestamp":1481610623}
	message := `{"message":"Hello World","timestamp":` + strconv.FormatInt(time.Now().Unix(), 10) + `}`

	// 送信データ(メッセージ)と秘密鍵を入力とし、署名を生成する
	signature, err := createSignature(message, privateKeyStr)
	if err != nil {
		log.Fatal(err)
	}

	// リクエストを送る際に、署名はヘッダに設定する
	req, err := http.NewRequest("POST", "http://localhost:8080/foo", bytes.NewBuffer(([]byte)(message)))
	defer req.Body.Close()
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Signature", signature)

	client := http.Client{}
	res, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.StatusCode)
}

func readPrivateKey(filepath string) (string, error) {
	s := ""
	fp, err := os.Open(filepath)
	if err != nil {
		return "", err
	}
	defer fp.Close()
	scanner := bufio.NewScanner(fp)
	for scanner.Scan() {
		text := scanner.Text()
		if text == "-----BEGIN RSA PRIVATE KEY-----" || text == "-----END RSA PRIVATE KEY-----" {
			continue
		}
		s = s + scanner.Text()
	}
	if err := scanner.Err(); err != nil {
		return "", err
	}

	return s, nil
}

func createSignature(message, keystr string) (string, error) {
	keyBytes, err := base64.StdEncoding.DecodeString(keystr)
	if err != nil {
		return "", err
	}

	private, err := x509.ParsePKCS1PrivateKey(keyBytes)
	if err != nil {
		return "", err
	}

	h := crypto.Hash.New(crypto.SHA256)
	h.Write(([]byte)(message))
	hashed := h.Sum(nil)

	signedData, err := rsa.SignPKCS1v15(rand.Reader, private, crypto.SHA256, hashed)
	if err != nil {
		return "", err
	}

	signature := base64.StdEncoding.EncodeToString(signedData)
	return signature, nil
}

Back to posts