在 Python 中使用 RSA 公钥验证签名的许可证密钥

Posted

技术标签:

【中文标题】在 Python 中使用 RSA 公钥验证签名的许可证密钥【英文标题】:Signed license key verification with RSA public key in Python 【发布时间】:2019-02-26 17:59:35 【问题描述】:

免责声明:我的密码学很糟糕,我对 RSA 或密码验证知之甚少。

我需要在 Python 中验证已签名的许可证。我使用了“带有 SHA256 摘要的 RSA PKCS1 v1.5 填充”。背景信息是我正在尝试使用https://keygen.sh 分发软件。

基本上,二进制软件安装在服务器上,并验证存储在服务器上的许可证文件是真实的。为此,该软件可以访问我的 RSA 公钥。我有一段代码在 Node.js 中工作,来自the official keygen documentation,我试图将它移植到 Python,但没有成功。我已选择使用 cryptodome 库,但我对其他选项持开放态度。

这是 Node.js 中的工作代码:

const crypto = require('crypto')

const public_key = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmwlw+mDo2ZVBlRXa7Em\ncj7cVrlwnwrIPC+Ij5KpltadJfwvRFvCr37USJvkc+FIND2dKk2mmbY32cvtxl3F\nYLpjRwwlFuajbP8ZEdJl1YJyJDnLlKHWEfTSvTzZhpT939yjuBKoZ9A+wiIQ9tzY\nF/ytb9zwPkOF7/XmPAaukah5xRgwsb3fo7E0CsBQuHZxFX83+nfdZ/60MWpSCWL6\nAjNWDEmoLFEHVRm69+lwXTW51wojfurZy/wUw42sciHLV5A8mz7gJJGO5y+sGzzD\nM5VxtmLz51Fl1Rl3fMzUAjPK77i9UDWo11EuNPrzMAgjmuuMLfpIDMlMR3n/ZsW7\nXwIDAQAB\n-----END PUBLIC KEY-----\n"
const key = 'somerandomkey'
const encodedSignature = "oMTrvIz3IX4kre5UTzvkzCn712wulPvl9knSYBduYcGsX2W703zWMC9ZVepDytxLdpUIiCUtx6wx5OzmLx3rTzgaKqptrbf2wYHrCIPBgrhcHdJ3fLJRh8ASC_NdLK6i1jC_bEAq84d7QNLlTPC20aCmNLdxEJFy-DValGG0iFdxx6n6-Vp5oL8jSyWubAvBSqEQ4ubptcYirxpbDdC4DRpNzBuA48DGxWg6Pxq5HdGZWKS05iohNlrFkW-K8NJYHuLKszT0FN5UWcghx1oklagCm72aDvXm3CzKL2id7yL78X_V69JYsExx3fjRsU0pUe-f5lzKLB_HLTAdc0e1gQ=="

const verifier = crypto.createVerify('sha256')
verifier.write(key)
verifier.end()

const ok = verifier.verify(public_key, encodedSignature, 'base64')
if (ok) 
  console.log('License key is valid!')
 else 
  console.log('License key is invalid!')

运行这段代码 (nodejs verify.js) 会打印出License key is valid!

这是失败的代码,在 Python 中:

import base64

# pip install pycryptodome
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
import Crypto.Signature.pkcs1_15
import Crypto.Util.Padding

public_key = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmwlw+mDo2ZVBlRXa7Em\ncj7cVrlwnwrIPC+Ij5KpltadJfwvRFvCr37USJvkc+FIND2dKk2mmbY32cvtxl3F\nYLpjRwwlFuajbP8ZEdJl1YJyJDnLlKHWEfTSvTzZhpT939yjuBKoZ9A+wiIQ9tzY\nF/ytb9zwPkOF7/XmPAaukah5xRgwsb3fo7E0CsBQuHZxFX83+nfdZ/60MWpSCWL6\nAjNWDEmoLFEHVRm69+lwXTW51wojfurZy/wUw42sciHLV5A8mz7gJJGO5y+sGzzD\nM5VxtmLz51Fl1Rl3fMzUAjPK77i9UDWo11EuNPrzMAgjmuuMLfpIDMlMR3n/ZsW7\nXwIDAQAB\n-----END PUBLIC KEY-----\n"
license_key = b'somerandomkey'

encoded_license_signature = """oMTrvIz3IX4kre5UTzvkzCn712wulPvl9knSYBduYcGsX2W703zWMC9ZVepDytxLdpUIiCUtx6wx5OzmLx3rTzgaKqptrbf2wYHrCIPBgrhcHdJ3fLJRh8ASC_NdLK6i1jC_bEAq84d7QNLlTPC20aCmNLdxEJFy-DValGG0iFdxx6n6-Vp5oL8jSyWubAvBSqEQ4ubptcYirxpbDdC4DRpNzBuA48DGxWg6Pxq5HdGZWKS05iohNlrFkW-K8NJYHuLKszT0FN5UWcghx1oklagCm72aDvXm3CzKL2id7yL78X_V69JYsExx3fjRsU0pUe-f5lzKLB_HLTAdc0e1gQ=="""
license_signature = base64.b64decode(encoded_license_signature)
# Padding: none of these solutions work
# license_signature = Crypto.Util.Padding.pad(license_signature, 8, style='pkcs7')
# license_signature = Crypto.Util.Padding.pad(license_signature, 8, style='iso7816')
# license_signature = Crypto.Util.Padding.pad(license_signature, 8, style='x923')
# Custom zero-padding (doesn't work either)
#license_signature = (8 - len(license_signature) % 8)*bytes([0]) + license_signature
#license_signature = license_signature + (8 - len(license_signature) % 8)*bytes([0])

rsa_public_key = RSA.import_key(public_key)
signature = Crypto.Signature.pkcs1_15.new(rsa_public_key)

license_hash = SHA256.new(data=license_key)
print(signature.verify(license_hash, license_signature))

运行这段代码 (python3 verify.py) 会引发错误:

Traceback (most recent call last):
  File "verify.py", line 30, in <module>
    print(signature.verify(license_hash, license_signature))
  File "/home/user/venvs/tutor/lib/python3.6/site-packages/Crypto/Signature/pkcs1_15.py", line 111, in verify
    raise ValueError("Invalid signature")
ValueError: Invalid signature

这对应于signature length error:

# Step 1
if len(signature) != k:
    raise ValueError("Invalid signature")

我认为这是由不正确的填充引起的,因此我对签名填充进行了不同的尝试,您可以在我的代码中看到这些尝试,但都不起作用。但至少,它们让我超越了签名验证的第一步。我现在卡在fourth and final step:

Traceback (most recent call last):
  File "verify.py", line 30, in <module>
    print(signature.verify(license_hash, license_signature))
  File "/home/user/venvs/tutor/lib/python3.6/site-packages/Crypto/Signature/pkcs1_15.py", line 137, in verify
    raise ValueError("Invalid signature")
ValueError: Invalid signature

知道如何解决这个问题吗?如有必要,我已准备好使用不同于 cryptodome 的东西。

【问题讨论】:

【参考方案1】:

您的签名是 base64url 而不是标准的 base64。我认为后者是罪魁祸首。所以你需要用+替换-,用/替换_。当然也可以使用 base64url 解码器。

【讨论】:

YEEEEEEEEEEEEEEEES!事实上,以下工作:license_signature = base64.urlsafe_b64decode(encoded_signature)。不需要填充。非常感谢。【参考方案2】:

我是 Keygen 的创始人。这是我缺乏适当的文档,我会纠正的。与Maarten mentioned 一样,许可证密钥的签名内容(包含两部分:密钥有效负载及其签名)是使用RFC 4648 编码的base64url,这是大多数编程语言都支持的base64 的URL 安全版本(但不是总是)。如 Maarten 的回答中所述,这种 base64url 编码方案与正常的 base64 编码略有不同。

大多数编程语言都有一个单独的函数来解码 URL base64 编码值, 但如果不是,就像 Maarten 在他的回答中概述的那样,您可以简单地将所有“-”base64 字符替换为“+”,并将所有“_”字符替换为“/”。

以下是使用 PKCS1 v1.5 填充验证使用 RSA-SHA256 签名的许可证密钥的完整示例:

from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
import base64

# This should be your Keygen account's public key
PUBLIC_KEY = \
"""-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtmwlw+mDo2ZVBlRXa7Em
cj7cVrlwnwrIPC+Ij5KpltadJfwvRFvCr37USJvkc+FIND2dKk2mmbY32cvtxl3F
YLpjRwwlFuajbP8ZEdJl1YJyJDnLlKHWEfTSvTzZhpT939yjuBKoZ9A+wiIQ9tzY
F/ytb9zwPkOF7/XmPAaukah5xRgwsb3fo7E0CsBQuHZxFX83+nfdZ/60MWpSCWL6
AjNWDEmoLFEHVRm69+lwXTW51wojfurZy/wUw42sciHLV5A8mz7gJJGO5y+sGzzD
M5VxtmLz51Fl1Rl3fMzUAjPK77i9UDWo11EuNPrzMAgjmuuMLfpIDMlMR3n/ZsW7
XwIDAQAB
-----END PUBLIC KEY-----"""

# This should be the license key that you're cryptographically verifying
LICENSE_KEY = \
"""c29tZXJhbmRvbWtleQ==.oMTrvIz3IX4kre5UTzvkzCn712wulPvl9knSYBduYcGsX2W703zWMC9ZVepDytxLdpUIiCUtx6wx5OzmLx3rTzgaKqptrbf2wYHrCIPBgrhcHdJ3fLJRh8ASC_NdLK6i1jC_bEAq84d7QNLlTPC20aCmNLdxEJFy-DValGG0iFdxx6n6-Vp5oL8jSyWubAvBSqEQ4ubptcYirxpbDdC4DRpNzBuA48DGxWg6Pxq5HdGZWKS05iohNlrFkW-K8NJYHuLKszT0FN5UWcghx1oklagCm72aDvXm3CzKL2id7yL78X_V69JYsExx3fjRsU0pUe-f5lzKLB_HLTAdc0e1gQ=="""

# Split license key to obtain key and signature, then decode urlbase64 encoded values
enc_key, enc_sig = LICENSE_KEY.split(".")
key = base64.urlsafe_b64decode(enc_key)
sig = base64.urlsafe_b64decode(enc_sig)

# Verify the key's signature
pub_key = RSA.importKey(PUBLIC_KEY)
verifier = PKCS1_v1_5.new(pub_key)
digest = SHA256.new(data=key)

print(
  verifier.verify(digest, sig)
)

我更新了 Keygen 的文档以阐明所有这些内容,并提供有关如何以加密方式验证许可证的更好示例。

【讨论】:

太棒了,谢谢!请注意,您可能应该导入 pkcs1_15 而不是 PKCS1_v1_5:github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/… 顺便说一句,在此处发布之前,我确实向 Keygen 支持发送了一封电子邮件;)

以上是关于在 Python 中使用 RSA 公钥验证签名的许可证密钥的主要内容,如果未能解决你的问题,请参考以下文章

RSA加密解密及RSA签名和验证

php RSA公钥私钥加解密和验证用法

Python rsa公私钥生成 rsa公钥加密(分段加密)私钥加签实战

iOS RSA加解密签名和验证

密码学基础:非对称加密(RSA算法原理)

使用 RSA C# 签名和验证签名