Search code examples
rubygorsa

Interoperability between Go and Ruby: Verify an RSA signature


good day!

I have a sinatra application in ruby that expects a message encoded in base64 and a signature of the message, then uses a public key to verify it. We're running ruby 3.2 and using openssl. The application is working fine, many users have been using it for years. Now I'm writing a new client in Go, so I defined a struct for my message, marshalled it to JSON, encoded it in base64, hashed and signed it using my private key. Nothing fancy. But the validation in the ruby application always fails.

Hopefully someone can share any hints, any help is much appreciated.

Here's some sample code to reproduce the problem. I'm running this on a Mac with M1 chip (don't think that's relevant, but wouldn't be surprised to find it's apple magic at work). Go version 1.22.2 and Ruby 3.2.3.

Ruby part:

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

Where public_key is a string with the pem encoded public key, message is the base64 encoded payload, and signature is the signature.

Go part:

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)
}

The test in Go passes without errors, but the verification in the ruby snippet with the same key, message and signature always returns false. I have checked that the public key matches on both applications, I have played with strict, raw, url options for the base64 encoding. I also tried to sign the request in Ruby and verify in Go, with the same result. And I ran out of ideas.


Solution

  • In your go code you are signing the json payload before you base 64 encode it, but it looks like you are trying to verify the signature on the base 64 encoded payload on the Ruby side.

    Assuming that the Ruby code is what you want to keep, you need to ensure that the are signing the base 64 encoded payload, not the raw json.

    Your code also duplicates the json marshalling, so the simplest solution might be to merge Signature and ToBase64 with something like:

    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
    }
    

    Then change

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

    to

    encoded, signature := rq.EncodeAndSign(key)
    

    That should give you values that verify on the Ruby side.