【发布时间】:2022-11-30 11:47:44
【问题描述】:
在阅读了 eBay 的将数字签名包含到他们的某些 REST API 调用的指南后,我在生成签名标头时遇到了问题。我不会在此处包含所有文档(有很多!),而是提供指向相应页面和一些文档的链接。以下页面是 eBay 提供的起点: https://developer.ebay.com/develop/guides/digital-signatures-for-apis 下一页是我从上一页描述如何创建签名的地方: https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-13.html#name-eddsa-using-curve-edwards25 这让我想到以下几点: https://www.rfc-editor.org/rfc/rfc8032#section-5.1.6
5.1.6. Sign
The inputs to the signing procedure is the private key, a 32-octet
string, and a message M of arbitrary size. For Ed25519ctx and
Ed25519ph, there is additionally a context C of at most 255 octets
and a flag F, 0 for Ed25519ctx and 1 for Ed25519ph.
1. Hash the private key, 32 octets, using SHA-512. Let h denote the
resulting digest. Construct the secret scalar s from the first
half of the digest, and the corresponding public key A, as
described in the previous section. Let prefix denote the second
half of the hash digest, h[32],...,h[63].
2. Compute SHA-512(dom2(F, C) || prefix || PH(M)), where M is the
message to be signed. Interpret the 64-octet digest as a little-
endian integer r.
3. Compute the point [r]B. For efficiency, do this by first
reducing r modulo L, the group order of B. Let the string R be
the encoding of this point.
4. Compute SHA512(dom2(F, C) || R || A || PH(M)), and interpret the
64-octet digest as a little-endian integer k.
5. Compute S = (r + k * s) mod L. For efficiency, again reduce k
modulo L first.
6. Form the signature of the concatenation of R (32 octets) and the
little-endian encoding of S (32 octets; the three most
significant bits of the final octet are always zero).
我从同一网页 (https://www.rfc-editor.org/rfc/rfc8032#section-6) 的附录中获得了一些 Python 代码:
## First, some preliminaries that will be needed.
import hashlib
def sha512(s):
return hashlib.sha512(s).digest()
# Base field Z_p
p = 2**255 - 19
def modp_inv(x):
return pow(x, p-2, p)
# Curve constant
d = -121665 * modp_inv(121666) % p
# Group order
q = 2**252 + 27742317777372353535851937790883648493
def sha512_modq(s):
return int.from_bytes(sha512(s), "little") % q
## Then follows functions to perform point operations.
# Points are represented as tuples (X, Y, Z, T) of extended
# coordinates, with x = X/Z, y = Y/Z, x*y = T/Z
def point_add(P, Q):
A, B = (P[1]-P[0]) * (Q[1]-Q[0]) % p, (P[1]+P[0]) * (Q[1]+Q[0]) % p;
C, D = 2 * P[3] * Q[3] * d % p, 2 * P[2] * Q[2] % p;
E, F, G, H = B-A, D-C, D+C, B+A;
return (E*F, G*H, F*G, E*H);
# Computes Q = s * Q
def point_mul(s, P):
Q = (0, 1, 1, 0) # Neutral element
while s > 0:
if s & 1:
Q = point_add(Q, P)
P = point_add(P, P)
s >>= 1
return Q
def point_equal(P, Q):
# x1 / z1 == x2 / z2 <==> x1 * z2 == x2 * z1
if (P[0] * Q[2] - Q[0] * P[2]) % p != 0:
return False
if (P[1] * Q[2] - Q[1] * P[2]) % p != 0:
return False
return True
## Now follows functions for point compression.
# Square root of -1
modp_sqrt_m1 = pow(2, (p-1) // 4, p)
# Compute corresponding x-coordinate, with low bit corresponding to
# sign, or return None on failure
def recover_x(y, sign):
if y >= p:
return None
x2 = (y*y-1) * modp_inv(d*y*y+1)
if x2 == 0:
if sign:
return None
else:
return 0
# Compute square root of x2
x = pow(x2, (p+3) // 8, p)
if (x*x - x2) % p != 0:
x = x * modp_sqrt_m1 % p
if (x*x - x2) % p != 0:
return None
if (x & 1) != sign:
x = p - x
return x
# Base point
g_y = 4 * modp_inv(5) % p
g_x = recover_x(g_y, 0)
G = (g_x, g_y, 1, g_x * g_y % p)
def point_compress(P):
zinv = modp_inv(P[2])
x = P[0] * zinv % p
y = P[1] * zinv % p
return int.to_bytes(y | ((x & 1) << 255), 32, "little")
def point_decompress(s):
if len(s) != 32:
raise Exception("Invalid input length for decompression")
y = int.from_bytes(s, "little")
sign = y >> 255
y &= (1 << 255) - 1
x = recover_x(y, sign)
if x is None:
return None
else:
return (x, y, 1, x*y % p)
## These are functions for manipulating the private key.
def secret_expand(secret):
if len(secret) != 32:
raise Exception("Bad size of private key")
h = sha512(secret)
a = int.from_bytes(h[:32], "little")
a &= (1 << 254) - 8
a |= (1 << 254)
return (a, h[32:])
def secret_to_public(secret):
(a, dummy) = secret_expand(secret)
return point_compress(point_mul(a, G))
## The signature function works as below.
def sign(secret, msg):
a, prefix = secret_expand(secret)
A = point_compress(point_mul(a, G))
r = sha512_modq(prefix + msg)
R = point_mul(r, G)
Rs = point_compress(R)
h = sha512_modq(Rs + A + msg)
s = (r + h * a) % q
return Rs + int.to_bytes(s, 32, "little")
## And finally the verification function.
def verify(public, msg, signature):
if len(public) != 32:
raise Exception("Bad public key length")
if len(signature) != 64:
Exception("Bad signature length")
A = point_decompress(public)
if not A:
return False
Rs = signature[:32]
R = point_decompress(Rs)
if not R:
return False
s = int.from_bytes(signature[32:], "little")
if s >= q: return False
h = sha512_modq(Rs + public + msg)
sB = point_mul(s, G)
hA = point_mul(h, A)
return point_equal(sB, point_add(R, hA))
现在,我遇到的问题是这段代码坚持由 32 字节数组组成的“秘密”:
if len(secret) != 32: raise Exception("Bad size of private key")
然而,这个秘密被描述为 eBay 的密钥管理 API (https://developer.ebay.com/api-docs/developer/key-management/overview.html) 提供的私钥,它不是一个 32 字节的数组,而是一个 64 字符的 ASCII 字符串(参见https://developer.ebay.com/api-docs/developer/key-management/resources/signing_key/methods/createSigningKey#h2-samples):
"privateKey": "MC4CAQAwBQYDK2VwBCIEI******************************************n"
当我尝试使用此 Python 代码使用 eBay 私钥生成签名时,它给我一个错误,指出它是“私钥大小错误”。如果我将私钥从 eBay 转换为字节数组,则它有 64 个字节长。如何使用 Python 代码使用 eBay 提供的私钥生成签名标头?
更复杂的是,我实际上是在使用 Python 生成签名后使用 Excel VBA (Visual Basic) 进行 API 调用(仅仅是因为 Python 更擅长这种事情!)。 eBay 的 PAID FOR 技术支持已确认以下标头是正确的,并且没有 https://www.rfc-editor.org/rfc/rfc8032#section-5.1.6 中描述的“消息”,但除了暗示可能存在“错误”之外,他们还没有提供任何进一步的帮助。
http.setRequestHeader "signature-input", "sig1=(""x-ebay-signature-key"" ""@method"" ""@path"" ""@authority"");created=1667386210"
http.setRequestHeader "x-ebay-signature-key", "<jwe returned by eBay>"
http.setRequestHeader "x-ebay-enforce-signature", "true"
一旦我可以生成有效签名,剩余的标头将如下所示:
http.setRequestHeader "signature" "sig1=:<signature>:"
我尝试过的所有内容都会产生相同的响应:
{
"errors": [
{
"errorId": 215122,
"domain": "ACCESS",
"category": "REQUEST",
"message": "Signature validation failed",
"longMessage": "Signature validation failed to fulfill the request."
}
]
}
以下是一些示例密钥,例如 eBay 生成的密钥。 https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html#appendix-B.1.4
“以下密钥是爱德华兹曲线 ed25519 上的椭圆曲线密钥,在本文档中称为 test-key-ed25519。此密钥是以 PEM 格式编码的 PCKS#8,没有加密。”
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=
-----END PUBLIC KEY-----
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF
-----END PRIVATE KEY-----
这是私钥的格式,我认为我需要将其转换为 32 字节数组才能使用上述 Python 代码。我认为链接到网页上有错字,应该是“PKCS”,而不是“PCKS”。
更新: 如果我运行以下命令:
openssl ec -in test.pem -text
其中 test.pem 是一个包含以下内容的文本文件:
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF
-----END PRIVATE KEY-----
它将私钥和公钥显示为 32 字节十六进制转储,但即使使用这些值,我也会得到与上面相同的响应,并出现 215122 错误。当我使用上面代码中的 Python“验证”方法对这些 32 字节十六进制转储密钥进行验证时,验证成功。
【问题讨论】:
-
我在这方面取得了一些进展,但如果这个网站上没有人感兴趣,我不会同时更新它。
-
我正在尝试用 PHP 来做这件事,真是一场噩梦!我从未见过没有代码 sn-ps 的 API 如此缺乏文档、过于复杂的示例。 :(
-
我同意。文档很糟糕而且过于复杂。你卡在哪一部分了?我不理解实际的 Signature 算法,因为我使用了文档中提供的 Python 代码,它确实有效。这是我坚持使用的签名算法的“签名输入”字段和输入消息。
-
很想知道你取得了什么进展——比如@Renegade_Mtl 试图在 PHP 中实现它,这是一场噩梦……
标签: python python-3.x rest digital-signature ebay-api