ebayAPI请求新增的签名机制处理流程,java

Posted on Aug 18, 2022

部分站点因为新的安全政策,API请求需要额外新增四个header

  • x-ebay-signature-key将RSA公钥或ed25519公钥 转为jwe格式,公钥文件从ebay后续api下载
  • Content-Digest http请求body的内容摘要,使用SHA-256算法处理body生成摘要
  • Signature-Input 决定由哪些字段拼接为signature-base
  • Signature 通过私钥对signature-base进行签名的得到
  • 请求头的名字大小写不敏感

流程

  • 通过ebay未来提供的api下载公钥私钥文件。

  • 将公钥转为jwe格式的字符串作为请求header x-ebay-signature-key。

  • 为api请求的body内容计算摘要content-digest,作为请求header Content-Digest

  • ebay已经指定 header signature-Input 为sig1=(“content-digest” “x-ebay-signature-key” “@method” “@path” “@authority”);created=1658272908代表要根据content-digest,x-ebay-signature-key两个请求头的内容和http请求的"@method" “@path” “@authority"属性拼接signatureBase。 可直接通过URI对象获取这些@开头的属性。

    URI uri = URI.create(url); String method = uri.getMethod();

  • 使用私钥文件为刚刚构造的signatureBase签名作为请求header Signature。

拼接signature base的格式要求

  • 详细的格式要求和签名机制可参考文档https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-11.html

  • signature base每一行都是"key”: value\n的形式,key用"“括起来并且全部小写,冒号后附加单独的空格,value结束后换行。最后一行是@signature-params。 ebay指定了请求header signature-Input为

signature-Input:sig1=("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created=1658272908
  • created后面附加的是时间戳。

  • @signature-params指拼接signture-base用到的所有参数,与signature-Input是对应的,放在最后一行

  • 拼接完成的sinatureBase

"content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=: 
"x-ebay-signature-key": publicKey 
"@method": POST   
"@path": /verifysignature  
"@authority": localhost:8080  
"@signature-params": ("content-digest" "x-ebay-signature-key" "@method" "@path" "@authority");created=1660628866  
  • 拼接signatureBase并将签名加入请求headers
    //与signature-input对应
    private List<String> signatureParams = Arrays.asList("content-digest", "x-ebay-signature-key", "@method", "@path", "@authority"); 
   //构造http message签名header
    void buildSignatureHeaders(Map<String, String> headers, String url) throws SignatureException, IOException, CryptoException {
        URI uri = URI.create(url);
        StringBuilder buf = new StringBuilder();
        if (!headers.containsKey("content-digest")) {
            //post请求没有body时,则不需要内容摘要content-digest
            signatureParams = signatureParams.stream().filter(a -> !a.equals("content-digest")).collect(Collectors.toList());
        }
        for (String header : signatureParams) {
            buf.append("\"");
            buf.append(header.toLowerCase());
            buf.append("\": ");
            if (header.startsWith("@")) {
                switch (header.toLowerCase()) {
                    case "@method":
                        buf.append("POST");
                        break;
                    case "@authority":
                        buf.append(uri.getAuthority());
                        break;
                    case "@target-uri":
                        buf.append(url);
                        break;
                    case "@scheme":
                        buf.append(uri.getScheme());
                        break;
                    case "@path":
                        buf.append(uri.getPath());
                        break;
                    case "@query":
                        buf.append(uri.getQuery());
                        break;
                    default:
                        throw new SignatureException("Unknown pseudo header " + header);
                }
            } else {
                buf.append(headers.get(header));
            }
            buf.append("\n");
        }

        buf.append("\"@signature-params\": ");
        StringBuilder signatureInputBuf = new StringBuilder();
        signatureInputBuf.append("(");
        for (int i = 0; i < signatureParams.size(); i++) {
            String param = signatureParams.get(i);
            signatureInputBuf.append("\"");
            signatureInputBuf.append(param);
            signatureInputBuf.append("\"");
            if (i < signatureParams.size() - 1) {
                signatureInputBuf.append(" ");
            }
        }
        signatureInputBuf.append(");created=");
        signatureInputBuf.append(Instant.now().getEpochSecond());
        buf.append(signatureInputBuf);
        String signatureInput = "sig1=" + signatureInputBuf;
        String signatureBase = buf.toString();
        headers.put("signature", getSignature(signatureBase));
        headers.put("signature-input", signatureInput);
    }

计算Content-Digest

    /**
     * 获得body的内容摘要
     *
     * @param content
     * @return
     */
    public static String getContentDigest(String content) {
        byte[] hash = org.apache.commons.codec.digest.DigestUtils.sha256(content);
        String encoded = Base64.getEncoder().encodeToString(hash);
        return "sha-256=:" + encoded + ":";
    }

读取密钥对文件

    private PrivateKey privateKey;
    private PublicKey publicKey;
    @Test
    public void readPublicKey() throws SignatureException {
        Path path = Paths.get("xxxx\\rsa\\publickey.pem");
        try (FileReader keyReader = new FileReader(path.toFile())) {
            PEMParser pemParser = new PEMParser(keyReader);
            JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
            SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(pemParser.readObject());
            //方法要调用核心类库的keyFactory,java-8不支持ed25519算法,无法根据对应的ed25519公钥文件构造对象
            this.publicKey = converter.getPublicKey(publicKeyInfo);
        } catch (IOException ex) {
            throw new SignatureException("Error loading public key: " + ex.getMessage(), ex);
        }
    }

    @Test
    public void readPrivateKey() throws SignatureException {
        Path path = Paths.get("xxxx\\rsa\\privatekey.pem");
        try (FileReader keyReader = new FileReader(path.toFile())) {
            PEMParser pemParser = new PEMParser(keyReader);
            JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
            PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(pemParser.readObject());
            //方法要调用核心类库的keyFactory,java-8不支持ed25519算法,无法根据对应的ed25519私钥文件构造对象
            this.privateKey = converter.getPrivateKey(privateKeyInfo);
        } catch (IOException ex) {
            throw new SignatureException("Error loading private key: " + ex.getMessage(), ex);
        }
    }

使用私钥为signature-base签名

    public String getSignature(String baseString) throws CryptoException, IOException {
        byte[] base = baseString.getBytes(StandardCharsets.UTF_8);
        Signer signer;
        //如果要使用ed25519密钥签名使用signer = new Ed25519Signer(new SHA256Digest()) 
        signer = new RSADigestSigner(new SHA256Digest());
        AsymmetricKeyParameter privateKeyParameters = PrivateKeyFactory.createKey(this.privateKey.getEncoded());
        signer.init(true, privateKeyParameters);
        signer.update(base, 0, base.length);
        byte[] signature = signer.generateSignature();
        return "sig1=:" + new String(org.bouncycastle.util.encoders.Base64.encode(signature)) + ":";
    }

将公钥转为JWE格式的字符串

  //把公钥文件转为JWE格式字符串
    public String getJWE(PublicKey publicKey) throws JOSEException , IOException {
            //masterkey.key文件 ebay提供
            Path masterkeyPath = Paths.get("xxx\\masterkey.key");
            String secretKeyBase64 = Files.readAllLines(masterkeyPath).get(0);//目前文件只有一行
            final byte[] secretKey = Base64.getDecoder().decode(secretKeyBase64);
            JWEEncrypter jweEncrypter = new AESEncrypter(secretKey);
            JWEDecrypter jweDecrypter = new AESDecrypter(secretKey);
            // Compose the JWT claims set
            Date now = new Date();

            JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
                    .expirationTime(new Date(now.getTime() + 1000L * 60 * 60 * 24 * 365 * 10)) // expires in 3 years
                    .notBeforeTime(now)
                    .issueTime(now)
                    .jwtID(UUID.randomUUID().toString())
                    .claim("appid", "app1") // this is set to the appId
                    .claim("pkey", new String(org.bouncycastle.util.encoders.Base64.encode(publicKey.getEncoded()))) // public ed25519 key
                    .build();


            // Request JWT encrypted with DIR and 256-bit AES/GCM
            JWEHeader header = new JWEHeader.Builder(JWEAlgorithm.A256GCMKW, EncryptionMethod.A256GCM)
                    .compressionAlgorithm(CompressionAlgorithm.DEF)
                    .build();


            // Create the encrypted JWT object
            EncryptedJWT jwt = new EncryptedJWT(header, jwtClaims);

            // Do the actual encryption
            jwt.encrypt(jweEncrypter);

            // Serialise to JWT compact form
            String jwtString = jwt.serialize();

            return jwtString;
    }