【问题标题】:eBay Digital Signatures for APIs signature header generation用于 API 签名标头生成的 eBay 数字签名
【发布时间】: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


【解决方案1】:

好吧,这就是我现在的位置,不使用内容摘要,因为它只是一个 GET 请求,所以只是试图让基础工作正常,但这些似乎都不起作用。

    $public = "xxx";
    $private = "yyy";
    $jwe = "jwe";
    $path = "/sell/fulfillment/v1/order/" . "11-xxxx-yyyy";
    $signature_input_txt = '("x-ebay-signature-key" "@method" "@path" "@authority");created=' . time();

    // $signature_base = '"content-digest": sha-256=:' . base64_encode($contentDigest) . ":
";
    $signature_base = '"x-ebay-signature-key": ' . $jwe;
    $signature_base .= '"@method": POST';
    $signature_base .= '"@path": ' . $path;
    $signature_base .= '"@authority": ' . "apiz.ebay.com";
    $signature_base .= '"@signature-params": ' . $signature_input_txt;

    // ensure signature_base is UTF-8
    if (!mb_check_encoding($signature_base, 'UTF-8')) {
        $signature_base = mb_convert_encoding($signature_base, 'UTF-8');
    }


    // dd($signature_base);
    // base 64 encode our signature_base
    $signature_base_base64_encoded = base64_encode($signature_base);

    // format the private key as required
    $formatted_private_key = "-----BEGIN RSA PRIVATE KEY-----" . PHP_EOL . $private . PHP_EOL . "-----END RSA PRIVATE KEY-----";

    // sign
    openssl_sign($signature_base_base64_encoded, $signed_signature, $formatted_private_key, "sha256WithRSAEncryption");

    return [
        'Authorization' => 'Bearer ' . $this->marketplace->getToken('oauth2.access_token', 'production'),
        'Accept'        => 'application/json',
        'Content-Type'  => 'application/json',
        'Signature-Input' => 'sig1=' . $signature_input_txt,
        'Signature' => 'sig1=:' . base64_encode($signed_signature) . ':',
        'x-ebay-signature-key' => $jwe,
        'x-ebay-enforce-signature' => true
    ];

【讨论】:

  • 感觉你很痛苦,有Java和Node的SDK却没有PHP的,真是一头雾水
  • 代码对我来说看起来不错。但是,我选择了 ED25519,因为 RSA 看起来要复杂得多。另外,我认为您不需要为/sell/fulfillment/v1/order 调用添加数字签名...如果财务在您的范围内,请尝试使用$path = /sell/finances/v1/seller_funds_summary。实时通话的完整 URL 是https://apiz.ebay.com/sell/finances/v1/seller_funds_summary。还有一个主题有更多信息:stackoverflow.com/questions/74530218/…
  • @Renegade_Mtl - 我设法让它工作,我把完整的工作放在下面,谢谢你和 atl-it 提供的信息
【解决方案2】:

我要把它放在这里,供任何努力使用 PHP 的人使用,改编自 Renegade_Mtl 答案(你错过了为每个 signature_base 换行的需要,它不需要编码)。

/**
 * @param $method - e.g. POST, GET
 * @param $path - e.g /sell/finances/v1/seller_funds_summary
 * @param $host - e.g. api.ebay.com
 * @param $keyset // public, private and jwt keys generated from https://apiz.ebay.com/developer/key_management/v1/signing_key
 * @param $timestamp - e.g. time()
 * @return array of headers
 */
private function createHeaders(string $method, string $path, string $host, array $tokens, int $time) {
    $signature_input_txt = '("x-ebay-signature-key" "@method" "@path" "@authority");created=' . $time;

    // $signature_base = '"content-digest": sha-256=:' . base64_encode($contentDigest) . ":
";
    $signature_base = '"x-ebay-signature-key": ' . $tokens['jwe']."
";
    $signature_base .= '"@method": ' . $method."
";
    $signature_base .= '"@path": ' . $path."
";
    $signature_base .= '"@authority": ' . $host."
";
    $signature_base .= '"@signature-params": ' . $signature_input_txt;
  
    // format the private key as required
    $formatted_private_key = "-----BEGIN RSA PRIVATE KEY-----" . PHP_EOL . $tokens['privateKey'] . PHP_EOL . "-----END RSA PRIVATE KEY-----";

    openssl_sign($signature_base, $signed_signature, $formatted_private_key, "sha256WithRSAEncryption");
    return [
        'Signature-Input' => 'sig1=' . $signature_input_txt,
        'Signature' => 'sig1=:' . base64_encode($signed_signature) . ':',
        'x-ebay-signature-key' => $tokens['jwe'],
        'x-ebay-enforce-signature' => "true"
    ];
}

我们只使用 GET,但如果您还使用 POST,那么您还需要内容摘要...希望这可以帮助人们避免浪费时间和时间来弄清楚它。

【讨论】:

    猜你喜欢
    • 2014-04-06
    • 1970-01-01
    • 2020-07-19
    • 2019-04-18
    • 1970-01-01
    • 2010-10-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多