Взаимодействие между Go и Ruby: проверка подписи RSA

добрый день!

У меня есть приложение Sinatra на Ruby, которое ожидает сообщение, закодированное в Base64, и подпись сообщения, а затем использует открытый ключ для его проверки. Мы запускаем Ruby 3.2 и используем openssl. Приложение работает нормально, многие пользователи используют его уже много лет. Сейчас я пишу новый клиент на Go, поэтому определил структуру для своего сообщения, упорядочил ее в JSON, закодировал в base64, хешировал и подписал с помощью своего закрытого ключа. Ничего особенного. Но проверка в приложении Ruby всегда терпит неудачу.

Надеюсь, кто-нибудь поделится подсказками, любая помощь будет очень признательна.

Вот пример кода, воспроизводящего проблему. Я запускаю это на Mac с чипом M1 (не думаю, что это актуально, но не удивлюсь, если обнаружит, что это яблочная магия в действии). 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 проходит без ошибок, но проверка в рубиновом фрагменте с тем же ключом, сообщением и подписью всегда возвращает false. Я проверил, что открытый ключ совпадает в обоих приложениях, я играл со строгими, необработанными параметрами URL для кодировки base64. Я также пытался подписать запрос на Ruby и проверить на Go, с тем же результатом. И у меня закончились идеи.

Попробуйте использовать простую фиксированную строку, чтобы исключить различия JSON.

Ry- 07.05.2024 02:17

Как уже сообщалось в ответе, в коде Ruby отсутствует декодирование сообщения Base64. Чаще всего подписываются необработанные данные, а не данные в кодировке Base64. Если вы хотите придерживаться этого, измените код Ruby и используйте Base64.strict_decode64(message) вместо message в verify(), тогда проверка пройдет успешно.

Topaco 07.05.2024 09:52
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Шаг 1: Создание приложения Slack Чтобы создать Slackbot, вам необходимо создать приложение Slack. Войдите в свою учетную запись Slack и перейдите на...
2
2
88
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В вашем коде go вы подписываете полезную нагрузку json до того, как закодируете ее в Base 64, но похоже, что вы пытаетесь проверить подпись в полезной нагрузке в кодировке Base 64 на стороне Ruby.

Предполагая, что вы хотите сохранить код Ruby, вам необходимо убедиться, что они подписывают полезную нагрузку в кодировке Base 64, а не необработанный json.

Ваш код также дублирует маршалинг 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.

Вы правы, мы подписываем и проверяем сообщение в кодировке Base 64, я это совершенно упустил из виду. Большое спасибо!

lfrg 07.05.2024 14:37

Другие вопросы по теме