使用RSA私钥加密消息(如OpenSSL中的RSA_private_encrypt)

13
I希望您能够在Go中实现Chef API客户端,但是在尝试创建正确的请求头RSA签名时遇到了困难。根据文档

规范标头使用客户端机器使用的私钥进行签名,并且还使用Base64进行编码。

下面的ruby调用OpenSSL :: PKey :: RSA.private_encrypt()可以在mixlib-authentication gem code中找到,它使用OpenSSL绑定private_encrypt()方法调用RSA_private_encryptopenssl function
抱歉,我在 Go 的标准库中没有找到匹配的函数;crypto/rsa 看起来很接近,但它仅实现传统的加密方法:使用 公钥 进行加密,使用 私钥 进行哈希签名。 OpenSSL 的 RSA_private_encrypt 则相反:它使用私钥(类似于从消息哈希创建签名)对 (小) 消息进行加密。
此“签名”也可使用此命令实现:
openssl rsautl -sign -inkey path/to/private/key.pem \
    -in file/to/encrypt -out encrypted/output

是否有任何本地Go库可以实现与OpenSSL的RSA_private_encrypt相同的结果,或者唯一的方法是使用Cgo从OpenSSL库中调用此函数?也许我遗漏了什么。我的想法是在不依赖非Go的情况下实现客户端。

作为Go新手,我不确定能否深入了解crypto/rsa模块源代码。


发现了相似的问题,但是回答使用SignPKCS1v15显然是错误的(这个函数加密消息的哈希值而不是消息本身)。


那个Chef API的文档真是令人困惑,但我觉得你应该对头部进行签名,这意味着SignPKCS1v15可能是你想要的。 - President James K. Polk
@GregS,不幸的是,这并不是事实,我已经通过mixlib-authentication sources进行了双重检查。 - artyom
我认为你可能是正确的。我怀疑它正在使用PKCS1 v15 块类型1填充,但这真的对你没有帮助。抱歉。 - President James K. Polk
4个回答

5

在Golang社区的巨大帮助下,问题得到了解决:

Alex在邮件列表上发布的原始代码,请参见http://play.golang.org/p/jrqN2KnUEM

我已经按照rfc2313第8节规定添加了输入块大小检查:http://play.golang.org/p/dGTl9siO8E

以下是代码:

package main

import (
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"
    "errors"
    "fmt"
    "io/ioutil"
    "math/big"
    "os/exec"
)

var (
    ErrInputSize  = errors.New("input size too large")
    ErrEncryption = errors.New("encryption error")
)

func PrivateEncrypt(priv *rsa.PrivateKey, data []byte) (enc []byte, err error) {

    k := (priv.N.BitLen() + 7) / 8
    tLen := len(data)
    // rfc2313, section 8:
    // The length of the data D shall not be more than k-11 octets
    if tLen > k-11 {
        err = ErrInputSize
        return
    }
    em := make([]byte, k)
    em[1] = 1
    for i := 2; i < k-tLen-1; i++ {
        em[i] = 0xff
    }
    copy(em[k-tLen:k], data)
    c := new(big.Int).SetBytes(em)
    if c.Cmp(priv.N) > 0 {
        err = ErrEncryption
        return
    }
    var m *big.Int
    var ir *big.Int
    if priv.Precomputed.Dp == nil {
        m = new(big.Int).Exp(c, priv.D, priv.N)
    } else {
        // We have the precalculated values needed for the CRT.
        m = new(big.Int).Exp(c, priv.Precomputed.Dp, priv.Primes[0])
        m2 := new(big.Int).Exp(c, priv.Precomputed.Dq, priv.Primes[1])
        m.Sub(m, m2)
        if m.Sign() < 0 {
            m.Add(m, priv.Primes[0])
        }
        m.Mul(m, priv.Precomputed.Qinv)
        m.Mod(m, priv.Primes[0])
        m.Mul(m, priv.Primes[1])
        m.Add(m, m2)

        for i, values := range priv.Precomputed.CRTValues {
            prime := priv.Primes[2+i]
            m2.Exp(c, values.Exp, prime)
            m2.Sub(m2, m)
            m2.Mul(m2, values.Coeff)
            m2.Mod(m2, prime)
            if m2.Sign() < 0 {
                m2.Add(m2, prime)
            }
            m2.Mul(m2, values.R)
            m.Add(m, m2)
        }
    }

    if ir != nil {
        // Unblind.
        m.Mul(m, ir)
        m.Mod(m, priv.N)
    }
    enc = m.Bytes()
    return
}

func main() {
    // o is output from openssl
    o, _ := exec.Command("openssl", "rsautl", "-sign", "-inkey", "t.key", "-in", "in.txt").Output()

    // t.key is private keyfile
    // in.txt is what to encode
    kt, _ := ioutil.ReadFile("t.key")
    e, _ := ioutil.ReadFile("in.txt")
    block, _ := pem.Decode(kt)
    privkey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
    encData, _ := PrivateEncrypt(privkey, e)
    fmt.Println(encData)
    fmt.Println(o)
    fmt.Println(string(o) == string(encData))
}

更新:我们可以期望在Go 1.3中有原生支持这种签名方式,详见适当的提交


实际提交的链接似乎已经失效了,我需要这种加密方式来签署HTTP请求。你知道这种加密方式的确切名称吗?因为你提到了PKCS命名等。 - Jerry Jacobs
Jerry,我已经更新了提交到Github的链接,你需要的函数是rsa.SignPKCS1v15 - artyom

3
自从go 1.3 版本以后,你可以很容易地使用SignPKCS1v15来完成此操作。
rsa.SignPKCS1v15(nil, priv, crypto.Hash(0), signedData) 

参考:https://groups.google.com/forum/#!topic/Golang-Nuts/Vocj33WNhJQ

这篇文章讨论了在Go中使用cgo的一些问题。cgo是一个允许Go代码与C代码交互的工具,但在使用时需要注意内存管理和性能问题。作者建议尽可能避免使用cgo,除非没有其他选择。如果必须使用cgo,应该小心谨慎,并尝试将其限制在较小的代码块中,以便更容易管理和调试。

1
我曾经卡在这个问题上很久。最终,我通过以下代码解决了它: https://github.com/bitmartexchange/bitmart-go-api/blob/master/bm_client.go
// Sign secret with rsa with PKCS 1.5 as the padding algorithm
// The result should be exactly same as "openssl rsautl -sign -inkey "YOUR_RSA_PRIVATE_KEY" -in "YOUR_PLAIN_TEXT""
signer, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivateKey.(*rsa.PrivateKey), crypto.Hash(0), []byte(message))

0
欢迎来到 OpenSSL 的乐趣... 这是一个名字极其不恰当的函数。如果你在 Ruby 代码中瞧瞧,它调用了这个 OpenSSL 函数。

http://www.openssl.org/docs/crypto/RSA_private_encrypt.html

阅读文档,实际上是使用私钥签署缓冲区,而不是加密缓冲区。

描述

这些函数在低级别处理RSA签名。

RSA_private_encrypt() 使用私钥rsa对from中的flen字节(通常是具有算法标识符的消息摘要)进行签名,并将签名存储在to中。 to必须指向RSA_size(rsa) 字节的内存。


尽管有文档说明,但实际上这是一个签名操作:您可以对结果执行解密步骤并获取原始消息。我认为它旨在加密签名,这个结论得到了括号中的注释的支持(请参阅您引用的描述):“通常是带有算法标识符的消息摘要”。 - artyom
在之前的消息中,我实际上是指 RSA_private_encrypt 是一种加密操作,而不是签名。 - artyom
@artyom 问题在于,如果你在任何人都可以用公钥解密的东西中进行“加密”,那么你实际上并没有隐藏任何东西。使用此调用来实现任何安全性的好理由非常少。在这种程度上,“私有加密”确实更接近签名(其中你会“加密”一个摘要)。 - Bruno

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接