LoginSignup
22

More than 5 years have passed since last update.

Goで書くはじめてのデジタル署名

Last updated at Posted at 2016-12-13

この記事は Origami Advent Calendar 11日目の記事になります。

最近学習しているデジタル署名について、わからない人でもわかる基礎的なところから、簡単なサンプルプログラムの実装までまとめてみました。最後のサンプルプログラムを組むところまで実施すれば、デジタル署名のメリットや仕組みなどの基本的な部分は理解できるかと思います。プログラムは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

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

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にサーバーを立ててから、送信側を実行し、お互いの実行結果を確認してみてください。

リクエスト受信側

main.go
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
}

リクエスト送信側

main.go
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
}

We're hiring!

Origamiではいろんなポジションのエンジニアを募集してます。
https://origami.com/jobs

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22