はじめに
普段意識しなくても暗号化に関する技術は身近にあります。たとえば、SSL証明書、GitHubなどに登録する公開鍵、JWTトークンなどです。
私もこれらの技術を利用していますが、それがどのような仕組みで動いているかについては、あまり知りませんでした。
そこで、今回は暗号化について深堀りするとともに、実装の例としてJWTトークンの生成、署名、検証をGo言語で実装してみたいと思います。
前提条件
一口に暗号化といっても様々な種類があります。そこで今回は、一般的によく使われるであろうRSA暗号に限定し、暗号鍵の種類もOpenSSH形式、PKCS #1およびPKCS #8に限定します。
また、Go言語のバージョンは、1.19.1を使用します。
暗号化と復号
それではまずはじめに、暗号化および復号についてざっくり説明します。
暗号化とは
暗号化とは、第三者に不正にデータを見られることを防ぐために、暗号鍵というデータを使用してデータを解読できない形に加工することです。この変換前のメッセージを「平文」といい、加工後のデータを「暗号文」といいます。
復号とは
複合とは暗号化の逆、つまり「暗号文」を加工して元の「平文」に戻す行為です。この時も暗号鍵を使用しますが、暗号の種類によっては、暗号化に使用する暗号鍵と複合に使用する暗号鍵は異なる場合があります(RSA暗号など)。
暗号化方式
暗号化方式には共通鍵暗号と公開鍵暗号の2種類があります。
共通鍵暗号
共通鍵暗号(対称鍵暗号とも言う)では、その名の通り暗号化と複合に同じ暗号鍵(共通鍵、対称鍵とも言う)を使用します。
共通鍵暗号で使用するアルゴリズムには「RC4、DES、3DES、AES」などがあり、現在の主流は「AES」です。
共通鍵暗号では、接続先ごとに共通鍵を生成する必要があります。共通鍵が盗まれてしまうと暗号文を複合できてしまうので、鍵交換を盗聴されないように安全に行う必要があります。
公開鍵暗号
公開鍵暗号(非対称暗号とも言う)では、暗号化と複合に別の暗号鍵を使用します。このとき、暗号化に使用する鍵を公開鍵といい、復号化に使用する鍵を秘密鍵といいます。
公開鍵暗号で使用するアルゴリズムには「RSA、EIGamal」などがあります。
公開鍵暗号では、受信者側にて公開鍵と秘密鍵を生成します。
署名と検証
公開鍵暗号に関わる署名および検証について説明します。
署名とは
電子文書に対して付与される署名です。その電子文書が正規なものであり、かつ、改ざんされていないことを証明するものです。
検証とは
電子署名の有効性を確認するための行為のことです。
具体的には、「電子署名が付与されたデータが改竄されていないか」、「電子署名が有効であるか」、「電子署名の信頼性が確認されているか」などを確認します。
RSA暗号とは
RSA暗号とは公開鍵暗号の一つで、素因数分解の難しさを利用した暗号アルゴリズムです。RSAは製作者の名前に由来しています。
RSA暗号は、整数論の定理であるオイラーの定理と2つの素数を使用して公開鍵を作成しており、膨大な数の素因数分解が困難なことが安全性の根拠となっています。
RSA暗号の暗号鍵の形式については冒頭で記載したとおり、OpenSSH形式、PKCS #1およびPKCS #8に絞って説明します。
OpenSSH形式
OpenSSH形式はOpenSSH独自の暗号鍵のフォーマットです。この形式の暗号鍵は、ssh-keygenコマンドで生成することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # ssh-keygen -f ./id_rsa -N "" Generating public/private rsa key pair. Your identification has been saved in ./id_rsa. Your public key has been saved in ./id_rsa.pub. The key fingerprint is: SHA256:eIhLv083+9ZaxVCioWPOelMovcGiWKcTtAg7JewDHCs user@machinname.lan The key's randomart image is: +---[RSA 3072]----+ | . . . .| |..o . o o | |Eo+ . . + . . | |.o = + + * o o | | = + * S B . o| | + = * + + . | | o = o * .. | | + o +... | | ... .oo. | +----[SHA256]-----+ |
OpenSSH形式の公開鍵
OpenSSH形式の公開鍵は以下のように、
ssh-rsa
から始まるのが特徴です。概ねRFC4716の形式と同じですが、ヘッダは含まれず改行もされていないOpenSSH独自の形式のようです。
1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDN+3V6idPovPOWTV0aJSqrWyxdThCAzx/aYE3AuEb3LbxEjXWAihtTYlRd2HS1iDMHS6wnsSQUoBlUyvp4Sicwez55Q8c6f5JY4I2LTqOh55KPdQps4fpZVlpF53hxcCF2L8gu+xE8NfCu56G8W2E3BTZQyQtktVW0OCXDzhjspPCinqNAE9W/mACFj4i3txCHQa9TZiZfIIM6+WZeJfnrEADJgDwwbOTLXG5g8QylJKFGWDjMYraqUEjKygSRIqS+WqrFLxrLIhxclGT7lXTXfr20tYMq7Y3Ch4j7ZlcyGEIzXTANt+E+zxmaES2LBuiQ4ExgDm5/wURDWns8PJolIvdIIj9WQm1KesouGrgpXZb9u0WNbzUfjwwYzPIC8Gsd3nAJtnDv0dnwilVw8k/cBA0ZLb4E6rSBjD4NK2+qd1qsydtq/Tv+rcUwsBSxuC8MIx1wVnPYrJEBCld7m3ihLpR9n8/QdtG/Stx6OCbXRk1zvhdku2GJFZb+hrBzRpU= user@machinmane.lan |
OpenSSH形式の秘密鍵
OpenSSH形式の秘密鍵は、OpenSSH7.8以降とそれより前で形式が異なります。OpenSSH7.8より前のOpenSSH形式の秘密鍵はOpenSSLの形式(おそらくPKCS #1)と同じです。それに対し、OpenSSH7.8以降のOpenSSH形式の秘密鍵はOpenSSLの秘密鍵とは互換性がありません。
10月30日補足
秘密鍵の新形式はOpenSSH6.5にて楕円曲線署名スキーマの「Ed25519」がサポートされた際に追加されたようです。この「Ed25519」は新形式のみ対応で、RSAの場合は新形式に加えて旧形式を使うこともできます。
現在は
ssh-keygen
で作成した秘密鍵の形式は、OpenSSH7.8以降の秘密鍵の形式がデフォルトとなっており、作成した秘密鍵はこの様になっています。
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 35 36 37 38 | -----BEGIN OPENSSH PRIVATE KEY----- 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 -----END OPENSSH PRIVATE KEY----- |
PKCS #1
PKCS #1の説明の前に、まずはPKCSとは何かについて説明します。
PKCS(Public-key Cryptography Standards)とは、RSAセキュリティにより考案させれた各種の技術仕様を定めた規格群です。PKCSには#1から#15まであり、それぞれRSA暗号に関する仕様が定められています。そのうちPKCS #1はRSA暗号の仕様を定めたものになります(最新はRFC 8017)。
このRFC 8017で定めている仕様とは具体的に、
- 暗号プリミティブ
- 暗号化スキーム
- (付録付き)署名スキーム(signature scheme with appendix)
などです。これらについては次に説明します。
また、RFC 8017ではAppendixにて、RSA鍵の表現として公開鍵と秘密鍵のANS.1オブジェクト識別子とRSAPublicKey型およびRSAPrivateKey型を定義しています。
ANS.1については後ほど改めて説明します。また、以下の暗号プリミティブおよび暗号化スキーム、(付録付き)署名スキームの説明については、RSA暗号の定義となります。
暗号プリミティブ
暗号プリミティブとは、暗号スキームを構築するための基本的な数式のことです。暗号プリミティブには以下の4つの種類のプリミティブがあります。
- 暗号化プリミティブ
- 複合化プリミティブ
- 署名プリミティブ
- 検証プリミティブ
これらのうち、暗号化プリミティブと復号化プリミティブ、署名プリミティブと検証プリミティブはペアになっています。
暗号化プリミティブは、公開鍵を用いてメッセージから暗号文を生成します。そして、復号化プリミティブは、秘密鍵を用いて暗号文からメッセージを復元します。これら2つは暗号化スキームで使用されます。
同様に、署名プリミティブは、秘密鍵を使ってメッセージから署名を生成します。そして、検証プリミティブは、公開鍵を使って署名からメッセージを復元します。これら2つは(付録付き)署名スキームで使用されます。
暗号化スキーム
暗号化スキームは、暗号化操作と復号化操作で構成されます。
暗号化操作では、受信者のRSA公開鍵を含むメッセージから暗号文が生成されます。また、復号化操作では、受信者のRSA秘密鍵を使って暗号文からメッセージを復元します。
署名スキーム
(付録付き)署名スキームは署名生成操作と署名検証操作で構成されています。
署名生成操作では、署名者のRSA秘密鍵を含むメッセージから署名が生成ハッシュ値から署名が生成(10月30日訂正)されます。また、署名検証操作では、署名者のRSA公開鍵を使って署名が検証されます。
(付録付き)署名スキームで構築された署名を検証するには、メッセージ自体が必要となります。
(付録付き)署名スキームは、X.509証明書などの様々な用途に使用できます。
ASN.1
ASN.1は情報の抽象構文です。定義の対象用途にはX.509証明書、PKCS #1、PKCS #8およびPKCS #12などがあげられます。
ANS.1では名前と型によって定義されるオブジェクトを列挙してデータ構造を定義し、定義された構文を用いて具体的な値を持つインスタンスを記述することができます。
例えば、「Price:==INTEGER」という記述により、integer型の値をもつPriceという名前のデータを定義することができます。
また、定義された構文に従って記述されたデータを特定のバイト列へ変換する方法もいくつか定められており、代表的なものとしてはBERやDERなどがあります。
PKCS #8
PKCS #8は、RSAに限らず全ての種類の秘密鍵を処理するための仕様を定めたものです(最新はRFC 5958)
秘密鍵情報および暗号メッセージ構文(CMS)コンテンツタイプのための構文を定義します。このCMS(Cryptographic Message Syntax)はRFC 5652で定義されています。
CMSはデジタル署名、ダイジェスト、認証、または非対称鍵形式コンテンツタイプを暗号化するために使用することができます。
PEMとDER
PEMとDERはどちらも証明書や暗号鍵の入れ物として使われます。両者の違いはエンコーディングと複数の証明書等のまとめ方です。
PEMは証明書や鍵をBase64エンコードし、それぞれを平文のヘッダーとフッターで囲います。
1 2 3 4 5 6 7 8 | -----BEGIN RSA PUBLIC KEY----- MIIBCgKCAQEAuniw7mDOEyiCkb3OTEgjxojEouz2JmzzAJF+kGzXNz2bZbw4yC/W WFbK1YMOxSL86cM6Lqk1eiuNNQHEYe49uAF8TWh9znguLssfLtbtJn8VAegwehY3 42bA+OrqkL4+vxsGRiz5GfBsnbqm4u++j2/S7gRMmaOsCBo0LB2xCyZqLuHtkngR FOshejIhUPgxitsZer4pI7JWM8F0go+6Q9oaFQHOZ0ucI1OXH+q3qwwXGJr45De6 rwTPiZElKFkYy228qqCSSB0UB0GWsuhxTnPPMAmPih8Jz4O/kFkzTKUIBQwUNEa2 p9TJvyx+hyu39Jk/A5ffcwgSP+gTCslQOQIDAQAB -----END RSA PUBLIC KEY----- |
DERは証明書や鍵をバイナリエンコードします。DERでエンコードされたファイルには、PEMとは異なりヘッダーとフッダーは含まれません。
実際に署名と検証を行ってみる
それでは、実際にプログラムで動かしてみましょう。まずはじめにJWTトークンを生成します。JWTトークンの生成にはjwt-goを使用しました。また、このサンプルではPKCS #1形式の公開鍵、秘密鍵をJWTトークンの暗号化および復号署名および検証(10月30日訂正)に使っています。
JWTトークンの生成
JWTトークンの生成では
jwt.NewWithClaims()
メソッドを使用します。引数として
jwt.SignningMethod
と
jwt.RegisteredClaims
を渡します。その後、生成したJWTトークンをパースした秘密鍵で暗号化署名(10月30日訂正)します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | func NewRSASignedStringWithClaims(claims *Claims) (string, error) { t := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) key, err := keys.ParsePrivateKey() if err != nil { return "", errors.Wrap(err, "failed to parse private key") } ss, err := t.SignedString(key) if err != nil { return "", errors.Wrap(err, "signing with PrivateKey failed") } return ss, nil } |
秘密鍵のパースはこのように行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | func ParsePrivateKey() (any, error) { // privateKeyはembedしたPKCS #1形式の秘密鍵 block, _ := pem.Decode(privateKey) if block == nil { return nil, errors.New("failed to parse private key") } if block.Type != "RSA PRIVATE KEY" { return nil, errors.New("invalid private key type") } parsedKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, errors.Wrap(err, "failed to parse private key") } return parsedKey, nil } |
JWTトークンの検証
生成したJWTトークンを検証します。
jwt.Parse()
メソッドではJWTトークンの文字列を解析、検証し、
jwt.Token
型の構造体のポインタを返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | func parse(tokenString string, key any) (*jwt.Token, error) { parsedToken, err := jwt.Parse(tokenString, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { return nil, errors.New("invalid signing method") } return key, nil }) if err != nil { return nil, errors.Wrap(err, "parsing token failed") } if !parsedToken.Valid { return nil, errors.New("invalid token") } return parsedToken, nil } |
parse()
メソッドに渡しているkeyはPKCS #1形式の公開鍵で、このようにパースしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | func ParsePublicKey() (any, error) { // publicKeyはembedされたPKCS #1形式の公開鍵 block, _ := pem.Decode(publicKey) if block == nil { return nil, errors.New("failed to parse public key") } if block.Type != "RSA PUBLIC KEY" { return nil, errors.New("invalid block type") } parsedKey, err := x509.ParsePKCS1PublicKey(block.Bytes) if err != nil { return nil, errors.Wrap(err, "failed to parsedKey for publicKey") } return parsedKey, nil } |
また、サンプルコードの全体はこちらをご確認ください。
さいごに
暗号化技術に関わる製品やサービスなどは生活の中に溶け込んでいますが、どのような技術なのかについてはあまり意識していませんでした。今回それらを調べることで暗号化技術の奥深さの一端を知ることができたのではないかと思います。
幾つか誤解があるように見えます。
> 実際に暗号化を行ってみる
JWTの操作は署名なので、「暗号化を行う」だと操作目的が全然合ってません。
※なんのためにその上で暗号化と署名の話を両方挙げたのか、意味がなくなります
> 同様に、署名プリミティブは、秘密鍵を使ってメッセージから署名を生成します。
署名プリミティブ・検証プリミティブは、メッセージからハッシュを計算した後の話なので「ハッシュ値から署名を生成」です。
> OpenSSH形式の秘密鍵は、OpenSSH7.8以降とそれより前で形式が異なります。
> OpenSSH7.8以降の秘密鍵の形式がデフォルト
デフォルトが変わったのはその通りですが、経緯にたいしてちょっと誤解を生む表現です。
新形式は ed25519用にOpenSSH6.5 で登場したもので、ed25519 は当初から新形式しか使えません。で、RSA等は新旧どちらでも使えましたが、作成時のデフォルトは旧形式でした。で、OpenSSH7.8 で作成時のデフォルトが新形式に変わっています。( 依然旧形式を使うことも作ることもできます )
ご指摘ありがとうございます。
該当箇所を修正しました。また、OpenSSHの新形式の件勉強になりました。