Techioz Blog

Go と Ruby 間の相互運用性: RSA 署名を検証する

概要

良い一日!

Ruby の Sinatra アプリケーションがあります。このアプリケーションは、base64 でエンコードされたメッセージとメッセージの署名を期待し、公開キーを使用してそれを検証します。 Ruby 3.2 を実行し、openssl を使用しています。このアプリケーションは正常に動作しており、多くのユーザーが長年にわたって使用しています。現在、Go で新しいクライアントを作成しているので、メッセージの構造体を定義し、JSON にマーシャリングし、base64 でエンコードし、秘密キーを使用してハッシュし、署名しました。何も派手なことはありません。ただし、Ruby アプリケーションでの検証は常に失敗します。

誰かがヒントを共有してくれることを願っています。助けていただければ幸いです。

問題を再現するサンプル コードを次に示します。私はこれを M1 チップを搭載した Mac で実行しています (これは関係ないと思いますが、これが Apple マジックだとわかっても驚かないでしょう)。 Go バージョン 1.22.2 および Ruby 3.2.3。

ルビ部分:

def validate(public_key, message, signature)
  p_key = OpenSSL::PKey::RSA.new(public_key)
  return p_key.verify('sha256', Base64.strict_decode64(signature), message)
end

ここで、public_key は pem エンコードされた公開キーを含む文字列、message は Base64 エンコードされたペイロード、signature は署名です。

ゴーパート:

func Test_RequestSignature(t *testing.T) {
  const pemKey = `-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
`

  data := []byte(pemKey)
  block, _ := pem.Decode(data)
  key, _ := x509.ParsePKCS1PrivateKey(block.Bytes)

  rq := request{
    CreatedAt: time.Now().Unix(),
    AppId:     1,
    Message:   "something we'd like to sign of course",
    Uuid:      "030D842B-700D-41F6-BFB6-5CB10ADCA4EF",
  }

  encoded := rq.ToBase64()
  signature := rq.Signature(key)

  // Print stuff so I can copy and paste in my ruby snippet
  fmt.Println("message: ", encoded)
  fmt.Println("signature: ", signature)

  // Validate here, as sanity check
  publicKey := key.PublicKey

  decoded, _ := base64.StdEncoding.Strict().DecodeString(encoded)
  hashed := sha256.Sum256(decoded)

  sig, _ := base64.StdEncoding.Strict().DecodeString(signature)
  err := rsa.VerifyPKCS1v15(&publicKey, crypto.SHA256, hashed[:], sig)
  assert.Nil(t, err)
}

type request struct {
  CreatedAt int64  `json:"created_at"`
  AppId     int    `json:"app_id"`
  Message   string `json:"message"`
  Uuid      string `json:"uuid"`
}

func (r request) ToBase64() string {
  data, err := json.Marshal(&r)
  if err != nil {
    panic(err)
  }

  return base64.StdEncoding.Strict().EncodeToString(data)
}

func (r request) Signature(key *rsa.PrivateKey) string {
  data, err := json.Marshal(&r)
  if err != nil {
    panic(err)
  }

  hashed := sha256.Sum256(data)

  signature, err := rsa.SignPKCS1v15(nil, key, crypto.SHA256, hashed[:])
  if err != nil {
    panic(err)
  }

  return base64.StdEncoding.Strict().EncodeToString(signature)
}

Go でのテストはエラーなしで合格しますが、同じキー、メッセージ、署名を使用した Ruby スニペットでの検証では常に false が返されます。両方のアプリケーションで公開キーが一致することを確認し、base64 エンコードの strict、raw、url オプションを使用してみました。また、Ruby でリクエストに署名し、Go で検証しようとしましたが、同じ結果でした。そしてアイデアも尽きました。

解決策

Go コードでは、Base 64 でエンコードする前に JSON ペイロードに署名していますが、Ruby 側で Base 64 でエンコードされたペイロードの署名を検証しようとしているようです。

Ruby コードを保持したいと仮定すると、生の JSON ではなく、Base 64 でエンコードされたペイロードに署名していることを確認する必要があります。

コードでは json マーシャリングも複製されるため、最も簡単な解決策は、Signature と ToBase64 を次のようなものとマージすることかもしれません。

func (r request) EncodeAndSign(key *rsa.PrivateKey) (string, string) {

    data, err := json.Marshal(&r)
    if err != nil {
        panic(err)
    }

    // Base 64 encode the data before signing.
    data_b64 := base64.StdEncoding.Strict().EncodeToString(data)

    hashed := sha256.Sum256([]byte(data_b64))

    signature, err := rsa.SignPKCS1v15(nil, key, crypto.SHA256, hashed[:])

    if err != nil {
        panic(err)
    }

    signature_b64 := base64.StdEncoding.Strict().EncodeToString(signature)

    // Return both the base 64 encoded json data and the base 64 encoded
    // signature.
    return data_b64, signature_b64
}

それから変更してください

encoded := rq.ToBase64()
signature := rq.Signature(key)

encoded, signature := rq.EncodeAndSign(key)

これにより、Ruby 側で検証される値が得られるはずです。