Ruby Java 的这种 DSA 加密算法用 Ruby 改如何写?

huacnlee · 2012年03月30日 · 最后由 pathbox 回复于 2015年08月28日 · 10433 次阅读
本帖已被设为精华帖!
public static String sign(byte[] data, String privateKey) throws Exception {
    // 解密由base64编码的私钥
    byte[] keyBytes = decryptBASE64(privateKey);

    // 构造PKCS8EncodedKeySpec对象
    PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);

    // KEY_ALGORITHM 指定的加密算法
    KeyFactory keyFactory = KeyFactory.getInstance("DSA");

    // 取私钥匙对象
    PrivateKey priKey = keyFactory.generatePrivate(pkcs8KeySpec);

    // 用私钥对信息生成数字签名
    Signature signature = Signature.getInstance("DSA");
    signature.initSign(priKey);
    signature.update(data);

    return encryptBASE64(signature.sign());
}

public static boolean verify(byte[] data, String publicKey, String sign) throws Exception {

    // 解密由base64编码的公钥
    byte[] keyBytes = decryptBASE64(publicKey);

    // 构造X509EncodedKeySpec对象
    X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);

    // KEY_ALGORITHM 指定的加密算法
    KeyFactory keyFactory = KeyFactory.getInstance("DSA");

    // 取公钥匙对象
    PublicKey pubKey = keyFactory.generatePublic(keySpec);

    Signature signature = Signature.getInstance("DSA");
    signature.initVerify(pubKey);
    signature.update(data);

    // 验证签名是否正常
    return signature.verify(decryptBASE64(sign));
}

public static void main(String[] args) throws Exception {
  String privateKey = "MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBAMMai0rQ+0dTbXjJhesuXbnAIX8bOQAWPG3cv5VDjSUPK5alJWKqtlDrkR2qn+gOKHZELZEhUNX+Zej0x2fRfYMaQ8/GoCm/PUZ79irt/+dg9pkkhUBQ+gPAcpt8vGA/VJHlExgd/vTXiCJyslJFlP6W34rnAnO9lZTIAwF9kXNFAhUA8I+b9ZS1MoHketM8YGPLqR/s3uECgYA+ZqrPhRW5BwOpb46c0x11tAOryfGWrByYYWb6ONjTTKCA1vVcCYEdBr6gNr8noO+xRsUtvlZ2Mar0xi9kwYr2CWBr/bFcvhvjRx7e24s6oC0AGpuwgTFjUkb7LkYcM7tyemLOmCs+Ir8gx+OXjC3ukFWawFnJtbB4BfH81Tk5ZAQWAhQwJA67UTJSe4Ft6eg7bEKGOQx/OA==";
  String publicKey = "MIIBtjCCASsGByqGSM44BAEwggEeAoGBAMMai0rQ+0dTbXjJhesuXbnAIX8bOQAWPG3cv5VDjSUPK5alJWKqtlDrkR2qn+gOKHZELZEhUNX+Zej0x2fRfYMaQ8/GoCm/PUZ79irt/+dg9pkkhUBQ+gPAcpt8vGA/VJHlExgd/vTXiCJyslJFlP6W34rnAnO9lZTIAwF9kXNFAhUA8I+b9ZS1MoHketM8YGPLqR/s3uECgYA+ZqrPhRW5BwOpb46c0x11tAOryfGWrByYYWb6ONjTTKCA1vVcCYEdBr6gNr8noO+xRsUtvlZ2Mar0xi9kwYr2CWBr/bFcvhvjRx7e24s6oC0AGpuwgTFjUkb7LkYcM7tyemLOmCs+Ir8gx+OXjC3ukFWawFnJtbB4BfH81Tk5ZAOBhAACgYAtBXiK0XxLdhqhk2u/lh/xMpGIzlgAp+recKY9DHZI+DQidhFMNrAxzw3ptlKfV4jZeDallOMWe55m/Dn6EmP74BPzot1i+Pcz3VvQ0W5ts1QrR6A1w/STwP9RXKWERnV+YjMhyzWx39E9cDxeZMo0zonlqTjYAPI4+kDVhjU4uQ==";

  // 加密
  String signResult = sign(data, privateKey);
  // 加密验证
  boolean status = verify(data, publicKey, signResult); 
}



我叉,这玩意儿我昨天研究了一下午, Ruby 的 OpenSSL DSA 库的用法和 Java 的非常不同。

http://ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/PKey/DSA.html

Private Key 的生成是这么出来的:

$ openssl dsaparam -rand -genkey -out dsa.pem 1024
$ openssl dsaparam -noout -text -in dsa.pem
$ openssl gendsa -des3 -out ca.key dsa.pem
$ openssl dsa -noout -text -in ca.key
$ cat ca.key

-----BEGIN DSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,45C63797FDBBA034

Hp/mU7/jPnMmUcL7KEcnoRo654KM+gHetJHAIEuUgEjCS7RSLHh1f7URfr+TZYLv
vkTA8iQoDWtVPJ59lH1IKYPlEkbsBnu/wuI4W5L1V119x7YPzrLrP/eXG5p8FPZ1
k6M2H3XHzQW1eZD2ejJ1zD6MVkoXyLT0w/jNy1F6PcnhmMP7Vwfm/ALlXR4SILsK
I1YhnF0QRH2udrpbm09vGvUtay/Km3R1YtM+GJWT8IqQmR5Ju2LxPp3lRND7FdV9
o2bix3ehyz7sOS8Sam5I6U8D22glZBXX0zbDFHwckItSp7FquqAkFQ2J0cFAE4vR
0sckKpFcRd9wa6uhXumj3bBhmJcf9IN8BqbCly2gY8c/G08uF3dCdOXwdkCj1ack
hNHPFtywKNgv9zxzk2cA0o6ZvdAvxwKHKHJU1QsWkW5o0vOjEC8XFIJTf+2L0Obu
xJqAoqSWFPtn1sfjqtxRGuc+HV+P5sNetToyJ6Yu+cAGR77M336+l5k5KkvoYLAc
YfIgjNh3TC3HQiNQBr1JsW5WKSljyudoYL/s7wUXCHrxy74bXJ569OBnqrUjOoJ3
xBYA1tRk91aZ5SSHWraD1g==
-----END DSA PRIVATE KEY-----


看 Ruby OpenSSL::PKey::DSA 的文档,与 Java 对应的似乎是 syssign 方法和 sysverify 这两个方法,我尝试用他们,但是:

ca.key 可以用 DSA 类 加载进来

irb> priv_key = OpenSSL::PKey::DSA.new(File.read("ca.key"), "密码")


但是 dsa.pem 就不行

irb>pub_key = OpenSSL::PKey::DSA.new(File.read("dsa.pem"), "密码")
OpenSSL::PKey::DSAError: Neither PUB key nor PRIV key


并且 priv_key 加载进来以后 syssign 方法传入进去提示内容过大

irb> priv_key.syssign("this is a demo string")
OpenSSL::PKey::DSAError: data too large for key size


是需要另外加密一下字符么?但是我不清楚 Java 是怎么做的,就没法与之对应...

看这个 Java 代码的算法是这样

  1. 把 private key 字符串转换成 PKCS8 的格式,根据维基百科,它只是用来记录 key pair 的数据结构(通讯协定)。
  2. 通过该 PKCS8 数据结构,取得 DSA PrivateKey 对象
  3. 用这个 private key 套进去 DSA signature 的算法,并算出原始 data 的 signature

所以重点在于该怎么把 private key 转成 Ruby 的 OpenSSL::PKey::DSA 可以接受的格式,然后:

  1. 建立 OpenSSL::PKey::DSA 对象
  2. 使用 #syssign 方法 对原始信息做 signature

https://github.com/twg/openssl_pkcs8 这里有个 gem,可以把 private key 导出为 PKCS8 格式。

#3 楼 @daqing 说不定也不需要用到 PKCS8 ?我看 OpenSSL::PKey::DSA.new 的范例是直接开一个 pem 档案。

#4 楼 @chitsaou @huacnlee 如果知道了一个 privateKey(不管是从 pem 文件读还是一个已知的字符串),都可以直接 OpenSSL::PKey::DSA.new(privateKey), 这个 privateKey 也就是 java 方法中的 privateKey 吧。

测试时我没有用 pem 文件,而是先使用 dsa1 = OpenSSL::PKey::DSA.new(2048) 创建第一个随机的 DSA 实例,然后使用这个实例的 privateKey 创建另一个 DSA 实例, dsa2 = OpenSSL::PKey::DSA.new(dsa1.to_pem), 经验证二者的 public 和 private key 都是一样的。

后续的工作就应该类似了

doc = "Sign me" digest = OpenSSL::Digest::DSS1.digest(doc) sig = dsa2.syssign(digest)

只不过查看文档 DSA 的 Digest 似乎是 DSS

希望有帮助

public static String sign(byte[] data, String privateKey) throws Exception

我想了解这里面传入的String privateKey是什么?

#6 楼 @daqing private_key 不是一个简单的 DSA 密钥,似乎是经过 DES3 加密的

@daqing @donnior @chitsaou 这个 Java 的加密方式和 Ruby DSA 类接受的格式是不同的,我补充了一下 private_key 的格式,看正文。

#3 楼 @daqing openssl_pkcs8 是 for RSA 而不是 DSA

最新的进展, 我发现只用 private_key 来进行加密和验证加密是可以的

# coding: utf-8
require "rubygems"
require "base64"
require "cgi"
require "openssl"


# 1.先生成dsa参数
# openssl dsaparam -rand -genkey -out dsa.pem 1024
# 查看生成的DSA参数
# openssl dsaparam -noout -text -in dsa.pem
# 2.根据生成的DSA参数来生成DSA密钥
# openssl gendsa -des3 -out ca.key dsa.pem
# 查看DSA密钥
# openssl dsa -noout -text -in ca.key

params = {
  :_input_charset => "utf-8",
  :partner => "2088101115793806",
  :sns_user_id => "12345",
  :a => "1",
  :z => "z1",
  :sign_type => "DSA",
  :sign => "SEcr/GJlGSEMF2QX6J3ictLEPLG+ygG++Peq+DAuIg3qNE1P5kYmoA=="
}

# 排序后待签名的串
params.delete(:sign_type)
params.delete(:sign)
input_str = params.sort.collect do |s|
  unless s[0] == "notify_id"
    [s[0],CGI.unescape(s[1])].join("=")
  else
    [s[0],s[1]].join("=")
  end
end.join("&")


# Encoding
priv_key = OpenSSL::PKey::DSA.new(File.read("/Users/jason/ca.key"),"1234")
puts priv_key
puts priv_key.public_key
sign = Base64.encode64(priv_key.syssign(OpenSSL::Digest::DSS1.digest("ruby")))
puts sign.inspect

# Decoding
puts priv_key.sysverify(OpenSSL::Digest::DSS1.digest("ruby"),Base64.decode64(sign))

-----BEGIN DSA PRIVATE KEY-----
MIIBuwIBAAKBgQDm9DUtw99CIYscVHMcHpffdE4hghQweiS72FjFHs4GarmnE6Ul
YfmbQdm7WWaEbASWZwMH5pBbm6pDSEPrsCxmZMhwphllXa0qI+UWVvDrjcZLP1KF
pYwtNYND7RiWv6L8xGg43QAjLHhA88lTQb5ONSXsyaVnUhZu77Sa7lzLNwIVAKrO
mPOOcQP3a3Mb0uMkD16kAFnnAoGACjAmqtPh5peu84TtmIk8Zso0xs48OSaJzpYc
y/o/CLx4ZsurAY0z2WbaZLY/XWiBlj2EsA+FgZjeQvekAm6QjHEKzGu1KZntGlWP
TD7uv5D5QI9PD+YaHzQ9Umr5o6I8BizFvu/dXuXuGzGGm/FdYdHIPpW84WagzoBi
GZ6lvBUCgYBm4/CUmIptuBBWGF8MMywW0p7mx25EhQMWH7cLvG+fUbxKZXZMNclk
YQfQWpBG27SlFStJv8P/UzOqSYg//GY5CdurPf+OawPNNLE9NATp8SrcBZ/pTbOy
fVdQBBpAHdQoWaUByNS0V1SdGN1GNzRSPzzApAAYXMB7tHjyjoJW4QIVAJ2UAT/G
K6mIyQp1o4k/RWAq3Izr
-----END DSA PRIVATE KEY-----
-----BEGIN PUBLIC KEY-----
MIIBtjCCASsGByqGSM44BAEwggEeAoGBAOb0NS3D30IhixxUcxwel990TiGCFDB6
JLvYWMUezgZquacTpSVh+ZtB2btZZoRsBJZnAwfmkFubqkNIQ+uwLGZkyHCmGWVd
rSoj5RZW8OuNxks/UoWljC01g0PtGJa/ovzEaDjdACMseEDzyVNBvk41JezJpWdS
Fm7vtJruXMs3AhUAqs6Y845xA/drcxvS4yQPXqQAWecCgYAKMCaq0+Hml67zhO2Y
iTxmyjTGzjw5JonOlhzL+j8IvHhmy6sBjTPZZtpktj9daIGWPYSwD4WBmN5C96QC
bpCMcQrMa7Upme0aVY9MPu6/kPlAj08P5hofND1SavmjojwGLMW+791e5e4bMYab
8V1h0cg+lbzhZqDOgGIZnqW8FQOBhAACgYBm4/CUmIptuBBWGF8MMywW0p7mx25E
hQMWH7cLvG+fUbxKZXZMNclkYQfQWpBG27SlFStJv8P/UzOqSYg//GY5CdurPf+O
awPNNLE9NATp8SrcBZ/pTbOyfVdQBBpAHdQoWaUByNS0V1SdGN1GNzRSPzzApAAY
XMB7tHjyjoJW4Q==
-----END PUBLIC KEY-----
"MC0CFQCHbZLZU+3JngAkVg//2Uh803IriwIUTaHLHp8LOiyjzYrjIuzO4n8E\n8kY=\n"
true

看起来似乎是通了,给 Java 那边的 public_key 原来是从 priv_key.public_key 出来的,而不是 dsa.pem,我之前一直理解错了... 具体还得用 Java 跑起来测试看对不对,希望能通过,不想在折腾了.

刚才找会 Java 的同事来帮我把 Java 的算法编译出来跑,把 Ruby 的结果放进去测试,结果对了。对应最上面 Java 的算法的 Ruby 写法就是 #10 楼 希望以后可以帮助到其他同学参考。

@huacnleeopenssl dsaparam -rand -genkey -out dsa.pem 1024 生成的这个文件只是用来生成最后秘钥的参数文件,不是最后的秘钥,而传给 java 的那个参数得是秘钥

我一些理解:

DSA 生成过程

# 1.先生成dsa参数
# openssl dsaparam -rand -genkey -out dsa.pem 1024
# 查看生成的DSA参数
# openssl dsaparam -noout -text -in dsa.pem
# 2.根据生成的DSA参数来生成DSA密钥
# openssl gendsa -des3 -out ca.key dsa.pem
# 查看DSA密钥
# openssl dsa -noout -text -in ca.key

上面会生成两个文件 ca.keydsa.pemca.key 是私钥文件,可以直接读出来给 key = OpenSSL::PKey::DSA.new(File.read("ca.key")) 得到 dsa 对象,同时这个时候用 key.public_key 就可以看到公钥。

反过来验证来之 Java 的 sign

Java 会提供 public_key, sign, 和需要明文数据 data,以楼顶上面的 Java 例子为例

public_key = "MIIBtjCCASsGByqGSM44BAEwggEeAoGBAMMai0rQ+0dTbXjJhesuXbnAIX8bOQAWPG3cv5VDjSUPK5alJWKqtlDrkR2qn+gOKHZELZEhUNX+Zej0x2fRfYMaQ8/GoCm/PUZ79irt/+dg9pkkhUBQ+gPAcpt8vGA/VJHlExgd/vTXiCJyslJFlP6W34rnAnO9lZTIAwF9kXNFAhUA8I+b9ZS1MoHketM8YGPLqR/s3uECgYA+ZqrPhRW5BwOpb46c0x11tAOryfGWrByYYWb6ONjTTKCA1vVcCYEdBr6gNr8noO+xRsUtvlZ2Mar0xi9kwYr2CWBr/bFcvhvjRx7e24s6oC0AGpuwgTFjUkb7LkYcM7tyemLOmCs+Ir8gx+OXjC3ukFWawFnJtbB4BfH81Tk5ZAOBhAACgYAtBXiK0XxLdhqhk2u/lh/xMpGIzlgAp+recKY9DHZI+DQidhFMNrAxzw3ptlKfV4jZeDallOMWe55m/Dn6EmP74BPzot1i+Pcz3VvQ0W5ts1QrR6A1w/STwP9RXKWERnV+YjMhyzWx39E9cDxeZMo0zonlqTjYAPI4+kDVhjU4uQ=="

Ruby 用 OpenSSL::PKey::DSA.new 传入 public_key 是可以得到公钥的对象,然后用来 sysverify ,可是 Java 提供的那个 public_key 实际上是被 Base64 加密过的,需要解一下再能给 OpenSSL::PKey::DSA.new

require "base64"
require "openssl"
# 上面的 public_key 反解 base64 以后得到的是个 DER 类型([Ruby DSA 文档里面](http://ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/PKey/DSA.html) 有说 new 是可以接受 DER 格式的
public_der = Base64.decode64(public_key)
pub_key = OpenSSL::PKey::DSA.new(public_der)
pub_key.sysverify(OpenSSL::Digest::DSS1.digest(input_str),Base64.decode64("Java 给的 sign 字符"))


@huacnlee Java 代码那个 privateKey 参数看上去像是 base64 编码过的私钥,它通过 decodeBase64() 来转成 byte array (实际套进 cipher 算法的数据) ,你后来是怎么把它交给 Ruby 的 DSA 的?还是干脆就从 openssl 产生的文件读?

我回完才发现你有回 XD

之前用的 X509 Cert 代码,PKey DSA 应该也是差不多的,改写一下试试看:

cert = OpenSSL::X509::Certificate.new(File.read("#{Rails.root}/cert/rsa.20140728.cer"))
cert.public_key.verify("sha1", Base64.decode64(sign), message)

#15 楼 @chitsaou Java 的 privateKey 那个我现在还在研究这么形成 DSA 对象,publicKey 是可以,但是 privateKey 不行,会提示 Neither PUB key nor PRIV key

#14 楼 @huacnlee 这段 Java 代码应该是产品环境下的一段代码吧?看起来是作者故意用 Base64 把各种数据再次加密了一遍,故意提高了复杂度而已,其实跟 DSA 的加密没有什么关系。。。。

@donnior 这个是支付宝的某个接口的数据校验方式

@huacnlee Java 里面的 PKCS8EncodedKeySpec 表示用 ASN.1 对 private key 进行编码,似乎你要对他解密之后才能得到原始的 private key,查了下,ruby 里面有这个似乎: http://www.ruby-doc.org/stdlib-1.9.3/libdoc/openssl/rdoc/OpenSSL/ASN1.html

但是我硬是没找到怎么还原的方法,可以最多解析出那个 ASN1 的序列。。。

我挺蛋疼的⋯⋯

OpenSSL::PKey 对应的 private key 格式不是 PKCS#8.

PKCS#8 的格式是:

sequence { version, sequence { "DSA", sequence { p, q, g } }, private_key }

OpenSSL::PKey::DSA 需要的格式是:

sequence { version, p, q, g, (p, q, g, y) , x }

什么是 p, q, g, y, x 就涉及到 DSA 算法了,细节不表。。。

所以呢,可以用 ASN1 转换成它认得的格式,和楼主给的 java 代码对应的 ruby 代码是:

# coding: utf-8

require "openssl"
require "base64"

# 转换 PKCS8 到 OpenSSL 认识的格式
def convert_der pkcs8
  der = OpenSSL::ASN1.decode(pkcs8).value
  version = der[0]
  p, q, g = der[1].value[1].value
  x = OpenSSL::ASN1::Integer.new OpenSSL::BN.new(der[2].value[2..-1], 2).to_i
  y = g.value.mod_exp(p.value, x.value)
  pub_key = OpenSSL::ASN1::Integer.new [p.value, q.value, g.value, y].map{|i|i.to_i.to_s(2)}.join.to_i(2)
  OpenSSL::ASN1::Sequence.new([version, p, q, g, pub_key, x]).to_der
end

digest = 'whatever'

# 签名
der = Base64.decode64 "MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBAMMai0rQ+0dTbXjJhesuXbnAIX8bOQAWPG3cv5VDjSUPK5alJWKqtlDrkR2qn+gOKHZELZEhUNX+Zej0x2fRfYMaQ8/GoCm/PUZ79irt/+dg9pkkhUBQ+gPAcpt8vGA/VJHlExgd/vTXiCJyslJFlP6W34rnAnO9lZTIAwF9kXNFAhUA8I+b9ZS1MoHketM8YGPLqR/s3uECgYA+ZqrPhRW5BwOpb46c0x11tAOryfGWrByYYWb6ONjTTKCA1vVcCYEdBr6gNr8noO+xRsUtvlZ2Mar0xi9kwYr2CWBr/bFcvhvjRx7e24s6oC0AGpuwgTFjUkb7LkYcM7tyemLOmCs+Ir8gx+OXjC3ukFWawFnJtbB4BfH81Tk5ZAQWAhQwJA67UTJSe4Ft6eg7bEKGOQx/OA=="
priv_dsa = OpenSSL::PKey::DSA.new convert_der der
# puts priv_dsa.private? #=> true
sig = priv_dsa.syssign digest

# 验证
pub = Base64.decode64 "MIIBtjCCASsGByqGSM44BAEwggEeAoGBAMMai0rQ+0dTbXjJhesuXbnAIX8bOQAWPG3cv5VDjSUPK5alJWKqtlDrkR2qn+gOKHZELZEhUNX+Zej0x2fRfYMaQ8/GoCm/PUZ79irt/+dg9pkkhUBQ+gPAcpt8vGA/VJHlExgd/vTXiCJyslJFlP6W34rnAnO9lZTIAwF9kXNFAhUA8I+b9ZS1MoHketM8YGPLqR/s3uECgYA+ZqrPhRW5BwOpb46c0x11tAOryfGWrByYYWb6ONjTTKCA1vVcCYEdBr6gNr8noO+xRsUtvlZ2Mar0xi9kwYr2CWBr/bFcvhvjRx7e24s6oC0AGpuwgTFjUkb7LkYcM7tyemLOmCs+Ir8gx+OXjC3ukFWawFnJtbB4BfH81Tk5ZAOBhAACgYAtBXiK0XxLdhqhk2u/lh/xMpGIzlgAp+recKY9DHZI+DQidhFMNrAxzw3ptlKfV4jZeDallOMWe55m/Dn6EmP74BPzot1i+Pcz3VvQ0W5ts1QrR6A1w/STwP9RXKWERnV+YjMhyzWx39E9cDxeZMo0zonlqTjYAPI4+kDVhjU4uQ=="
pub_dsa = OpenSSL::PKey::DSA.new pub
# puts pub_dsa.private? #=> false
p pub_dsa.sysverify(digest, sig)



DSS 就是 SHA, DSS1 就是 SHA1, 用不用是没关系的

DSA 是对 digest 进行签名的算法,不能给它传很大的数据

private key 可以进一步用 DES / 3 重 DES / AES 加密码 (例如楼主的 ca.key),不过和 pkcs8 没什么关系

@night_song 我卡 ASN1 转换那了,怎么都没找到。。。 算法细节不清楚,转换到 BN 出错。。。。兄弟你太牛了,万分感谢,不然晚上睡不着了

#24 楼 @donnior ns 确实很牛

3des 我搞过,在 github

汗啊,你们两年前就折腾过这个,我今天还在折腾,我们现在的项目要从 java 移植过来,涉及到支付宝的移动快捷支付,涉及到 rsa 加密,正愁怎么用 ruby 搞定呢

这种接口只支持 rsa,我用 php 的方式加密,支付宝最后返回的结果是系统繁忙,这个坑貌似绕不过去了

#28 楼 @xwf286 同入坑中, 求指导

huacnlee [该话题已被删除] 中提及了此贴 11月25日 11:27
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册