java签名"hash values do not match"

java signature "hash values do not match"

我正在尝试将 xml 文档发送到远程服务器。我使用包含私钥和证书的密钥库对文档进行签名。但是当远程服务器获得 xml 时,它会响应 "hash values do not match"。

对该错误的研究表明 xml 文档在签名后正在更改。据我所知,我的 java 代码在签名后没有进行任何此类更改。我也曾尝试在签名之前从 xml 中删除所有空格,但没有效果。

这是正在生成的签名 xml 的一部分:

    <SOAP-SEC:Signature SOAP:mustUnderstand="1">
      <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
          <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
          <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
          <Reference URI="#Body">
            <Transforms>
              <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            </Transforms>
            <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
            <DigestValue>yFfex/IpBJ4zrAFxJ5kkTKBrIR8=</DigestValue>
          </Reference>
        </SignedInfo>
        <SignatureValue>F9dnhEW/RN4IphLUfSu0kCJ/+0L6KtzJlxuptzWYL52su1/mfpnaQaqdHW/iJeLUL4PJZ47hxXwwMZj5y9GJMxnedz+XSu+4GJ5dwEY…
aqQTJg==</SignatureValue>
        <KeyInfo>
          <X509Data>
            <X509IssuerSerial>
              <X509IssuerName>CN=RapidSSL RSA CA 2018,OU=www.digicert.com,O=DigiCert Inc,C=US</X509IssuerName>
              <X509SerialNumber>8986524346372316412121820876514917638</X509SerialNumber>
            </X509IssuerSerial>
            <X509Certificate>MIIFtTCCBJ2gAwIBAgIQBsK927DS8wePBQjvzVX9BjANBgkqhkiG9w0BAQsFADBeMQswCQYDVQQGEwJVUzEVMBMGA1UEChM…
YRtXpTI5y30r02yhghe//nMCL3MthO/gjEqGiLb1CxOncQ+j4A8+cfN+RR2fgA==</X509Certificate>
          </X509Data>
        </KeyInfo>
      </Signature>
    </SOAP-SEC:Signature>

当远程服务器尝试解码这个值时,它显然没有提供原始签名版本。请注意,Reference URI=”Body” 表示只有 xml 中包含 Id=”Body” 的部分用于生成 DigestValue。

这是 xml 中包含“Id=Body”的部分:

  <SOAP:Body Id="Body" xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
    <ProcessCreditApplication xmlns="http://www.starstandards.org/STAR">
      <ApplicationArea>
        <CreationDateTime>2019-12-09T18:26Z</CreationDateTime>
        <Destination>
          <DestinationNameCode>RO</DestinationNameCode>
        </Destination>
      </ApplicationArea>
      <DataArea>
        <oa:Process xmlns:oa="http://www.openapplications.org/oagis"/>
        <CreditApplication>
          <Header>
            <DocumentDateTime>2019-12-09T18:26Z</DocumentDateTime>
          </Header>
          <Detail>
            <CreditVehicle> 
             <Model>GRAND CHEROKEE</Model>
             <ModelYear>2015</ModelYear> 
            </CreditVehicle> 
          </Detail>
        </CreditApplication>
      </DataArea>
    </ProcessCreditApplication>
  </SOAP:Body>

这里是 java 代码来签署 xml。它生成 Signature 块,然后将其插入 SOAP-SEC:Signature 元素,如上例所示:

    private void buildSignatureBlock5(String privateKeyPath, String publicKeyPath) {
    // Create a DOM XMLSignatureFactory that will be used to generate the signature.
    XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");

    // Create a Reference to the enveloped document (in this case,
    // you are signing just the element with Id="Body", so a URI of "#Body" signifies
    // that), and also specify the SHA1 digest algorithm and the xml-exc-c14n# Transform.
    Reference ref = null;
    try {
        ref = fac.newReference
         ("#Body", fac.newDigestMethod(DigestMethod.SHA1, null),
          Collections.singletonList
           (fac.newTransform
            ("http://www.w3.org/2001/10/xml-exc-c14n#", (TransformParameterSpec) null)),
             null, null);
    } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    // Create the SignedInfo.
    SignedInfo si = null;
    try {
        si = fac.newSignedInfo
         (fac.newCanonicalizationMethod
          (CanonicalizationMethod.EXCLUSIVE,
           (C14NMethodParameterSpec) null),
            fac.newSignatureMethod(SignatureMethod.RSA_SHA1, null),
             Collections.singletonList(ref));
    } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }       

    // Load the KeyStore and get the signing key and certificate.
    KeyStore ks = null;
    try {
        ks = KeyStore.getInstance("JKS");
    } catch (KeyStoreException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    try {
        ks.load(new FileInputStream(storage_path +"/keys/company.jks"), "changeit".toCharArray());
    } catch (NoSuchAlgorithmException | CertificateException | IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    KeyStore.PrivateKeyEntry keyEntry = null;
    try {
        keyEntry = (KeyStore.PrivateKeyEntry) ks.getEntry
            ("1", new KeyStore.PasswordProtection("changeit".toCharArray()));
    } catch (NoSuchAlgorithmException | UnrecoverableEntryException | KeyStoreException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    X509Certificate cert = (X509Certificate) keyEntry.getCertificate();

    // Create the KeyInfo containing the X509Data.
    KeyInfoFactory kif = fac.getKeyInfoFactory();
    List x509Content = new ArrayList();
    String issuerName = cert.getIssuerX500Principal().getName();
    BigInteger serialNumber = cert.getSerialNumber();
    X509IssuerSerial issuer = kif.newX509IssuerSerial(issuerName, serialNumber);
    x509Content.add(issuer);
    x509Content.add(cert);
    X509Data xd = kif.newX509Data(x509Content);
    KeyInfo ki = kif.newKeyInfo(Collections.singletonList(xd));    

    // Create a DOMSignContext and specify the RSA PrivateKey and location of the resulting XMLSignature's parent element.
    Element envHeaderSig = (Element) document.getElementsByTagName("SOAP-SEC:Signature").item(0);
    DOMSignContext dsc = new DOMSignContext(keyEntry.getPrivateKey(), envHeaderSig);

    // Create the XMLSignature, but don't sign it yet.
    XMLSignature signature = fac.newXMLSignature(si, ki);

    try {
        signature.sign(dsc);    //ResourceResolverException: Cannot resolve element with ID Body
    } catch (MarshalException | XMLSignatureException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    removeWhitespaceFromSignature();
}

//hack to remove unwanted CR at the end of each line in SignatureValue and X509Certificate
private void removeWhitespaceFromSignature() {
    Element sig = (Element) document.getElementsByTagName("SignatureValue").item(0);
    String sigValue = sig.getTextContent().replaceAll("\r\n", "");
    sig.setTextContent(sigValue);

    Element cert = (Element) document.getElementsByTagName("X509Certificate").item(0);
    String certValue = cert.getTextContent().replaceAll("\r\n", "");
    cert.setTextContent(certValue);
}

我提供 XML 登录以下行:

Element envHeaderSig = (Element) document.getElementsByTagName("SOAP-SEC:Signature").item(0);

但我不知道后面的“signature.sign”是如何处理那个数据的。它会删除空格吗?显然,无论它做什么都会导致远程服务器端的解码算法产生不同的 XML,从而在它计算的 DigestValue(哈希)中造成不匹配。

这是用于发送 soap 请求的 java 代码:

send(String xmlSoapString) throws IOException {
    HttpURLConnection connection = null;

    OutputStreamWriter wr = null;
    BufferedReader in = null;
    String result = "";
    try {
        URL connectionUrl = new URL(url);
        connection = (HttpURLConnection) connectionUrl.openConnection();
        connection.setDoOutput(true);  
        connection.setDoInput(true);    

        //set connection properties - guessing at equivalents from PHP headers
        connection.setRequestProperty("accept","image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*");
        connection.setDefaultUseCaches(false);  // "cache-control: no-cache"
        connection.setRequestProperty("content-type","text/html");
        connection.setRequestProperty("pragma","no-cache");
        connection.setRequestProperty("time_stamp",timestamp_unix);
        connection.setRequestProperty("timestamp",timestamp);
        connection.setRequestProperty("SentTimeStamp",timestamp);
        connection.setRequestProperty("version","2.0");
        connection.setRequestProperty("Content-length",String.valueOf(documentToString(xml).length()));

        Map<String,List<String>> requestProperties = connection.getRequestProperties();
        String userCredentials = userId + ":" + dmsId;
        String basicAuth = "Basic " + new String(Base64.getEncoder().encode(userCredentials.getBytes()));
        connection.setRequestProperty("Authorization", basicAuth); 
        setSoapHeader(connection);
        wr = new OutputStreamWriter(connection.getOutputStream());
        wr.write(xmlSoapString);    //send soap request to remote server
        wr.flush();      

        try {
            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        } catch (Exception e) {
            in = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
        }

        StringBuilder bodyBuilder = new StringBuilder();
        String inputLine;
        while ((inputLine = in.readLine()) != null) {
            bodyBuilder.append(inputLine);
        }
        in.close();
        result = String.valueOf( bodyBuilder.toString());
        System.out.println("Response=" +result);    

    } catch(Exception e) {
        e.printStackTrace();
    } finally {
        if (wr != null) {
            wr.close();
        }
        if (in != null) {
            in.close();
        }
        if ( connection != null ) {
            connection.disconnect();
        }
    }   
    return result;
}

有什么建议吗?

可能是由于远程服务器未使用您正在使用的 xml 规范化方法(独有)。这可能会导致在语法上不同的 xml 上计算散列。

我最终成功了。这非常复杂。显然没有正确选择#body 部分。这是正确的代码。

private void buildSignatureBlock() {
    // Create a DOM XMLSignatureFactory that will be used to generate the signature.
    XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");

    // Create a Reference to the document (in this case, you are signing just the element with
    // Id="Body", so a URI of "#Body" signifies that), and also specify the SHA1 digest algorithm
    // and the xml-exc-c14n# Transform.
    Reference ref = null;
    try {
        ref = fac.newReference
                ("#Body", fac.newDigestMethod(DigestMethod.SHA1, null),
                        Collections.singletonList
                                (fac.newTransform
                                        ("http://www.w3.org/2001/10/xml-exc-c14n#", (TransformParameterSpec) null)),
                        null, null);
    } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    // Create the SignedInfo.
    SignedInfo si = null;
    try {
        si = fac.newSignedInfo
                (fac.newCanonicalizationMethod
                                (CanonicalizationMethod.EXCLUSIVE,
                                        (C14NMethodParameterSpec) null),
                        fac.newSignatureMethod(SignatureMethod.RSA_SHA1, null),
                        Collections.singletonList(ref));
    } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    // Load the KeyStore and get the signing key and certificate.
    KeyStore ks = null;
    try {
        ks = KeyStore.getInstance("JKS");
    } catch (KeyStoreException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    try {
        ks.load(new FileInputStream("src/main/resources/xml/project/keys/project.jks"), "password".toCharArray());
    } catch (NoSuchAlgorithmException | CertificateException | IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    KeyStore.PrivateKeyEntry keyEntry = null;

    try {
        keyEntry = (KeyStore.PrivateKeyEntry) ks.getEntry
                ("1", new KeyStore.PasswordProtection("password".toCharArray()));
    } catch (NoSuchAlgorithmException | UnrecoverableEntryException | KeyStoreException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    X509Certificate cert = (X509Certificate) keyEntry.getCertificate();

    // Create the KeyInfo containing the X509Data.
    KeyInfoFactory kif = fac.getKeyInfoFactory();
    List x509Content = new ArrayList();
    String issuerName = cert.getIssuerX500Principal().getName();
    BigInteger serialNumber = cert.getSerialNumber();
    X509IssuerSerial issuer = kif.newX509IssuerSerial(issuerName, serialNumber);
    x509Content.add(issuer);
    x509Content.add(cert);
    X509Data xd = kif.newX509Data(x509Content);
    KeyInfo ki = kif.newKeyInfo(Collections.singletonList(xd));

    // Create a DOMSignContext and specify the RSA PrivateKey and location of the resulting XMLSignature's parent element.
    Element envHeaderSig = (Element) document.getElementsByTagName("SOAP-SEC:Signature").item(0);

    // Create the XMLSignature, but don't sign it yet.
    XMLSignature signature = fac.newXMLSignature(si, ki);

    try {
        Node envelope = document.getFirstChild();
        Node header = envelope.getFirstChild();
        DOMSignContext sigContext = new DOMSignContext(keyEntry.getPrivateKey(), header);
        // Need to distinguish the Signature element in DSIG (from that in SOAP)
        sigContext.putNamespacePrefix(XMLSignature.XMLNS, "ds");

        // register Body ID attribute   sigContext.setIdAttributeNS(getNextSiblingElement(header),"http://schemas.xmlsoap.org/soap/security/2000-12","id");
        signature.sign(sigContext);

    } catch (MarshalException | XMLSignatureException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

    ///VALIDATE the signature
    Element sigElement = envHeaderSig;
    DOMValidateContext valContext =
            new DOMValidateContext(cert.getPublicKey(), sigElement);
    Element envelope = getFirstChildElement(document);
    Element header = getFirstChildElement(envelope);
    valContext.setIdAttributeNS
            (getNextSiblingElement(header),
                    "http://schemas.xmlsoap.org/soap/security/2000-12","id");
    boolean isValid = false;
    try {
        isValid = signature.validate(valContext);
    } catch (XMLSignatureException e) {
        e.printStackTrace();
    }
    System.out.println("Validating the signature... " +
            (isValid ? "valid" : "invalid"));
}

这里有几个函数可以从文档中提取所需的元素以进行签名验证:

private static Element getFirstChildElement(org.w3c.dom.Node node) {
    org.w3c.dom.Node child = node.getFirstChild();
    while (child != null &&
            child.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE) {
        child = child.getNextSibling();
    }
    return (Element) child;
}

public static Element getNextSiblingElement(org.w3c.dom.Node node) {
    org.w3c.dom.Node sibling = node.getNextSibling();
    while (sibling != null &&
            sibling.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE) {
        sibling = sibling.getNextSibling();
    }
    return (Element) sibling;
}