добрый день!
У меня есть приложение 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, с тем же результатом. И у меня закончились идеи.
Как уже сообщалось в ответе, в коде Ruby отсутствует декодирование сообщения Base64. Чаще всего подписываются необработанные данные, а не данные в кодировке Base64. Если вы хотите придерживаться этого, измените код Ruby и используйте Base64.strict_decode64(message)
вместо message
в verify()
, тогда проверка пройдет успешно.
В вашем коде 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, я это совершенно упустил из виду. Большое спасибо!
Попробуйте использовать простую фиксированную строку, чтобы исключить различия JSON.