使用 spring-ws 验证 SOAP 服务中的请求签名和签名响应

问题描述 投票:0回答:1

前提:我需要创建一个使用 WS-Security 标准的 SOAP 服务来执行标题中描述的操作,以便 SOAP 客户端可以使用此服务。

我有什么

  • 用java 17 + spring boot 3.3.3完成的Soap服务
  • 我的客户用来签署请求的 .cer
  • 我自己的证书的.cer和.key
  • 使用 chatgpt 命令生成的 .p12 文件和 .jks 文件

基本上,它涉及在发送/接收的信封的soap:标头中发送证书(.cer 文件)。然而,我在实现这一目标的过程中遇到了一些困难。根据我所看到的,我只有两个选择:

  • 将 Spring Boot 中的 WS-Security 与名为 Wss4jSecurityInterceptor 的类结合使用。
  • 在 SoapEndpointInterceptor 中捕获传出的 SOAP 消息并添加相关字段。

这些方法对我来说都不起作用。

场景一:

在我的 chat-gpt/gemini 提出许多问题和微调之后,我得到的只是带有虚构方法或不能满足我需要的方法的代码。我阅读了库的文档,这些方法没有解释从哪里检索值或如何计算它们。

因此,我遵循可用信息,进行反复试验。我传递了一个 crypto.properties 文件以及一个 .p12 文件,其中包含我的 .cer 和 .key 以及有效的密码和别名。我也尝试过使用 .jks 文件,但在这两种情况下,我都会收到“无法识别的字符 111”之类的错误,或者库未正确初始化。

总的来说,我使用 p12 和 jks 格式以及 crypto.properties 文件尝试了类中的所有方法,但服务要么没有启动,要么无法签名,而是向我的服务器发送了带有空正文的 202 响应。终点。

结果? 由于上述错误,服务甚至无法启动。

场景2:

使用 thisthat,我创建了所有必要的标签。

  • BinarySecurityToken:我使用 X.509Certificate 类将 .cer 转换为 base64。
  • DigestMethodSignatureMethod 分别使用 SHA-1 和 SHA-1withRSA。
  • DigestValue:从原始 XML 内容生成的 SHA-1 哈希值,作为来自soap:Body 的字符串(它不包括主体本身,只包括其中的根元素)。
  • SignatureValue:使用 Java 的 Signer 类,我从原始 .cer 和 .key(与 BinarySecurityToken 中使用的相同 .cer)生成的 .p12 文件中提取私钥,并对 SignedInfo 的原始 XML 字符串进行签名元素(包括其本身及其所有内容)。

如果您查看我正在使用的两个参考,您会发现需要考虑一些属性,例如 ID,对吧?

  • Reference(在 SecurityTokenReference 内部)有一个 URI 属性,该属性指向 BinarySecurityToken 中的 wsu:Id 属性(这是我自己生成的 UUID)。
  • 根据我在网上找到的示例,
  • Reference(SecurityTokenReference 之外)有一个 URI 属性,该属性指向soap:Body 中存在的 wsu:id 属性。我也用 UUID 生成它。
  • 时间戳:我只是设置了当前时间,提前一小时到期。

结果? 当客户端验证我的响应时,他们会收到“签名无效”异常。我尝试使用自签名证书、CA 颁发的证书和不带特殊字符的证书,但这些变化都没有改变结果。我们还验证了他们这边的 .cer 与我正在使用的匹配。

备注:

  • 不,REST 服务不是一种选择。
  • 不,不使用该标准不是一种选择;必须这样做。
  • 必须在 Java 17 + Spring Boot 3+ 中完成。
  • 您可能已经注意到,我目前没有附加代码,那是因为我不知道什么是什么了,发现的其他问题只会导致我出现其他错误,而且我尝试的方法也不起作用,所以我更喜欢说我不知道如何做这个问题的标题所说的事情,而我正在寻找的答案是一个关于如何实现我所拥有的非常具体的场景的代码示例(我将发布我之前失败的尝试作为此问题的答案)线程)。
java spring soap ws-security
1个回答
0
投票

尝试根据问题中给出的示例手动创建标签,这应该可以解决问题,但事实并非如此

public class ServerHeaderInterceptor implements SoapEndpointInterceptor {

    @Override
    public boolean handleRequest(MessageContext messageContext, Object endpoint) throws Exception {
        SoapMessage soapMessage = (SoapMessage) messageContext.getRequest();
        SoapHeader soapHeader = soapMessage.getSoapHeader();
        
        Iterator<SoapHeaderElement> headerElements = soapHeader.examineAllHeaderElements();
        while (headerElements.hasNext()) {
            SoapHeaderElement headerElement = headerElements.next();
            
            if (headerElement.getName().getLocalPart().equals("Security")) {
                
                Node securityNode = ((DOMSource) headerElement.getSource()).getNode();
                Element binarySecurityTokenElement = findBinarySecurityToken(securityNode);
                if (binarySecurityTokenElement != null) {
                    String binarySecurityTokenValue = binarySecurityTokenElement.getTextContent();
                    RequestContext.put("security.signature.binarytoken", binarySecurityTokenValue);
                    byte encodedCert[] = Base64.decodeBase64(binarySecurityTokenValue);
                    ByteArrayInputStream inputStream = new ByteArrayInputStream(encodedCert);
                    CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
                    X509Certificate cert = (X509Certificate) certFactory.generateCertificate(inputStream);
                    return isValidClientCertificate(cert);
                }
            }
        }
        
        return false;
    }
    
    private Element findNode(Node parentNode, String tag) {
        if (parentNode.getNodeType() == Node.ELEMENT_NODE) {
            Element element = (Element) parentNode;
            if (element.getLocalName().equals(tag)) {
                return element;
            }
        }

        NodeList childNodes = parentNode.getChildNodes();
        for (int i = 0; i < childNodes.getLength(); i++) {
            Node childNode = childNodes.item(i);
            if (childNode.getNodeType() == Node.ELEMENT_NODE) {
                Element foundElement = findNode(childNode, tag);
                if (foundElement != null) {
                    return foundElement;
                }
            }
        }

        return null;
    }

    private Element findBinarySecurityToken(Node parentNode) {
        if (parentNode.getNodeType() == Node.ELEMENT_NODE) {
            Element element = (Element) parentNode;
            if (element.getLocalName().equals("BinarySecurityToken")) {
                return element;
            }
        }

        NodeList childNodes = parentNode.getChildNodes();
        for (int i = 0; i < childNodes.getLength(); i++) {
            Node childNode = childNodes.item(i);
            if (childNode.getNodeType() == Node.ELEMENT_NODE) {
                Element foundElement = findBinarySecurityToken(childNode);
                if (foundElement != null) {
                    return foundElement;
                }
            }
        }

        return null;
    }

    private boolean isValidClientCertificate(X509Certificate certificate) {
        try {
            certificate.checkValidity();
            String isuerDN = certificate.getSubjectX500Principal().getName().replace("\\", "");
            String expectedIssuerDN = "stuff";
            boolean isIssuerDN = isuerDN.equals(expectedIssuerDN);

            PublicKey publicKey = certificate.getPublicKey();
//          byte[] signature = certificate.getSignature();

            Signature verifier = Signature.getInstance(certificate.getSigAlgName());
            verifier.initVerify(publicKey);
            verifier.update(certificate.getTBSCertificate());
//          boolean isSignatureValid = verifier.verify(signature);

            boolean[] keyUsage = certificate.getKeyUsage();

            if (keyUsage != null) {
                if (keyUsage[0]) {
                    System.out.println("allows.");
                }
                if (keyUsage[1]) {
                    System.out.println("no allows.");
                }
                
            } else {
                System.out.println("no key.");
            }

            return isIssuerDN; // && isSignatureValid;
        } catch (CertificateExpiredException | CertificateNotYetValidException e) {
            return false;
        }
        catch (NoSuchAlgorithmException | InvalidKeyException | CertificateEncodingException | SignatureException e) {
            return false;
        }
    }

    @SuppressWarnings("deprecation")
    @Override
    public boolean handleResponse(MessageContext messageContext, Object endpoint) throws Exception {
        X509Certificate cert = getCertificate();
        String certBase64 = Base64.encodeBase64String(cert.getEncoded());

        SoapMessage message = (SoapMessage) messageContext.getResponse();
        Element payload = (Element) message.getDocument().getFirstChild().getLastChild().getFirstChild();
        
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document document = builder.newDocument();
        
        // Create the root element (soap:Envelope)
        Element envelope = document.createElementNS("http://schemas.xmlsoap.org/soap/envelope/", "soap:Envelope");
        
        // Add namespaces to the Envelope element
        envelope.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
        envelope.setAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
        envelope.setAttribute("xmlns:wsa", "http://schemas.xmlsoap.org/ws/2004/08/addressing");
        envelope.setAttribute("xmlns:wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
        envelope.setAttribute("xmlns:wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
        
        // Create the Header element
        Element header = document.createElementNS("http://schemas.xmlsoap.org/soap/envelope/", "soap:Header");
        
        // Create the Body element
        Element body = document.createElementNS("http://schemas.xmlsoap.org/soap/envelope/", "soap:Body");
        String bodyId = "Id-" + UUID.randomUUID().toString();
        body.setAttribute("wsu:Id", bodyId);
        this.stripNamespaces(payload, body, document);
        
        // Append Header and Body to Envelope
        envelope.appendChild(header);
        envelope.appendChild(body);
        
        // Append Envelope to Document
        document.appendChild(envelope);
        
        Element messageHeader = document.createElementNS("http://www.uc-council.org/smp/schemas/eanucc", "messageHeader");
        Element messageHeaderTo = document.createElement("to");
        Element messageHeaderToGln = document.createElement("gln");
        messageHeaderToGln.setTextContent((String) RequestContext.get("to.gln"));
        messageHeaderTo.appendChild(messageHeaderToGln);
        messageHeader.appendChild(messageHeaderTo);
        Element messageHeaderFrom = document.createElement("from");
        Element messageHeaderFromGln = document.createElement("gln");
        messageHeaderFromGln.setTextContent((String) RequestContext.get("from.gln"));
        messageHeaderFrom.appendChild(messageHeaderFromGln);
        messageHeader.appendChild(messageHeaderFrom);
        Element messageHeaderRepresentingParty = document.createElement("representingParty");
        Element messageHeaderRepresentingPartyGln = document.createElement("gln");
        messageHeaderRepresentingPartyGln.setTextContent((String) RequestContext.get("representingparty.gln"));
        messageHeaderRepresentingParty.appendChild(messageHeaderRepresentingPartyGln);
        messageHeader.appendChild(messageHeaderRepresentingParty);
        
        Element action = document.createElement("wsa:Action");
        action.setTextContent("http://vesta.com.br/pse/FEServices/" + payload.getLocalName());
        
        Element messageId = document.createElement("wsa:MessageID");
        String messageIdValue = UUID.randomUUID().toString();
        messageId.setTextContent("urn:uuid:" + messageIdValue);
        
        Element relatesTo = document.createElement("wsa:RelatesTo");
        relatesTo.setTextContent((String) RequestContext.get("messageid"));
        
        Element to = document.createElement("wsa:To");
        to.setTextContent("http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous");
        
        Element security = document.createElement("wsse:Security");
        security.setAttribute("soap:mustUnderstand", "1");
        Element securityTimestamp = document.createElement("wsu:Timestamp");
        String timestampid = UUID.randomUUID().toString();
        securityTimestamp.setAttribute("wsu:Id", "Timestamp-" + timestampid);
        Element securityTimestampCreated = document.createElement("wsu:Created");
        Element securityTimestampExpires = document.createElement("wsu:Expires");
        Date now = new Date();
        Date expires = (Date) now.clone();
        expires.setHours(expires.getHours() + 1);
        
        DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneOffset.UTC);
        Instant instant = now.toInstant();
        String createdT = formatter.format(instant);
        instant = expires.toInstant();
        String expiresT = formatter.format(instant);
        
        securityTimestampCreated.setTextContent(createdT);
        securityTimestampExpires.setTextContent(expiresT);
        securityTimestamp.appendChild(securityTimestampCreated);
        securityTimestamp.appendChild(securityTimestampExpires);
        security.appendChild(securityTimestamp);
        
        Element binaryToken = document.createElement("wsse:BinarySecurityToken");
        binaryToken.setAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
        binaryToken.setAttribute("ValueType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3");
        String tokenId = "SecurityToken-" + UUID.randomUUID().toString();
        binaryToken.setAttribute("wsu:Id", tokenId);
        binaryToken.setTextContent(certBase64);
        security.appendChild(binaryToken);
        
        Element securitySignature = document.createElementNS("http://www.w3.org/2000/09/xmldsig#", "Signature");
        Element securitySignatureSigned = document.createElement("SignedInfo");
        Element securitySignatureSignedCanon = document.createElement("CanonicalizationMethod");
        securitySignatureSignedCanon.setAttribute("Algorithm", "http://www.w3.org/2001/10/xml-exc-c14n#");
        securitySignatureSignedCanon.setAttribute("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#");
        securitySignatureSigned.appendChild(securitySignatureSignedCanon);
        Element securitySignatureSignedMethod = document.createElement("SignatureMethod");
        securitySignatureSignedMethod.setAttribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
        securitySignatureSigned.appendChild(securitySignatureSignedMethod);
        Element securitySignatureSignedReference = document.createElement("Reference");
        securitySignatureSignedReference.setAttribute("URI", bodyId);
        Element securitySignatureSignedReferenceTransW = document.createElement("Transforms");
        Element securitySignatureSignedReferenceTrans = document.createElement("Transform");
        securitySignatureSignedReferenceTrans.setAttribute("Algorithm", "http://www.w3.org/2001/10/xml-exc-c14n#");
        Element securitySignatureSignedReferenceDigestM = document.createElement("DigestMethod");
        securitySignatureSignedReferenceDigestM.setAttribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
        Element securitySignatureSignedReferenceDigestV = document.createElement("DigestValue");
        
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        transformer.setOutputProperty(OutputKeys.INDENT, "no");
        
        DOMSource domSource = new DOMSource(body.getFirstChild());
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        StreamResult streamResult = new StreamResult(outputStream);
        
        transformer.transform(domSource, streamResult);
        String digestable = outputStream.toString("UTF-8");
        
        MessageDigest digest = MessageDigest.getInstance("SHA-1");
        byte[] hashedBody = digest.digest(digestable.getBytes());
        String digestValue = Base64.encodeBase64String(hashedBody);
        securitySignatureSignedReferenceDigestV.setTextContent(digestValue);
        
        securitySignatureSignedReferenceTransW.appendChild(securitySignatureSignedReferenceTrans);
        securitySignatureSignedReference.appendChild(securitySignatureSignedReferenceTransW);
        securitySignatureSignedReference.appendChild(securitySignatureSignedReferenceDigestM);
        securitySignatureSignedReference.appendChild(securitySignatureSignedReferenceDigestV);
        securitySignatureSigned.appendChild(securitySignatureSignedReference);
        securitySignature.appendChild(securitySignatureSigned);
        
        Element securitySignatureValue = document.createElement("SignatureValue");
        String signatureValue = this.signDigest(securitySignatureSigned, "keystore.p12", "password of jks/p12", "alias of the p12/jks");
        securitySignatureValue.setTextContent(signatureValue);
        
        Element securitySignatureKey = document.createElement("KeyInfo");
        Element securitySignatureKeyToken = document.createElement("wsse:SecurityTokenReference");
        Element securitySignatureKeyTokenKey = document.createElement("wsse:Reference");
        securitySignatureKeyTokenKey.setAttribute("ValueType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3");
        securitySignatureKeyTokenKey.setAttribute("URI", "#" + tokenId);
        securitySignatureKeyToken.appendChild(securitySignatureKeyTokenKey);
        securitySignatureKey.appendChild(securitySignatureKeyToken);
        securitySignature.appendChild(securitySignatureValue);
        securitySignature.appendChild(securitySignatureKey);
        
        security.appendChild(securitySignature);
        
        header.appendChild(messageHeader);
        header.appendChild(action);
        header.appendChild(messageId);
        header.appendChild(relatesTo);
        header.appendChild(to); 
        header.appendChild(security);
        message.setDocument(document);
        
        return true;
    }
    
    public String signDigest(Element element, String keystorePath, String keystorePassword, String keyAlias) throws Exception {
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        transformer.setOutputProperty(OutputKeys.INDENT, "no");
        
        DOMSource domSource = new DOMSource(element);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        StreamResult streamResult = new StreamResult(outputStream);
        
        transformer.transform(domSource, streamResult);
        String signable = outputStream.toString("UTF-8");
        
        // Load the keystore
        KeyStore keystore = KeyStore.getInstance("PKCS12");
        try (InputStream is = new FileInputStream(keystorePath)) {
            keystore.load(is, keystorePassword.toCharArray());
        }
        
        PrivateKey privateKey = (PrivateKey) keystore.getKey(keyAlias, keystorePassword.toCharArray());
        Signature signature = Signature.getInstance("SHA1withRSA");
        signature.initSign(privateKey);
        signature.update(signable.getBytes());
        
        // Base64 encode the signature
        byte[] signedHash = signature.sign();
        return Base64.encodeBase64String(signedHash);
    }

    private void stripNamespaces(Element originalElement, Element newElement, Document document) {
        // Create a new element with no namespace
        Element newChildElement = document.createElement(originalElement.getLocalName());

        // Copy attributes
        for (int i = 0; i < originalElement.getAttributes().getLength(); i++) {
            newChildElement.setAttribute(originalElement.getAttributes().item(i).getLocalName(),
                    originalElement.getAttributes().item(i).getNodeValue());
        }

        // Process child nodes
        for (int i = 0; i < originalElement.getChildNodes().getLength(); i++) {
            if (originalElement.getChildNodes().item(i) instanceof Element) {
                Element childElement = (Element) originalElement.getChildNodes().item(i);
                stripNamespaces(childElement, newChildElement, document);
            } else {
                newChildElement.appendChild(document.importNode(originalElement.getChildNodes().item(i), true));
            }
        }

        // Append the newly created child to the new element
        newElement.appendChild(newChildElement);
    }
    private X509Certificate getCertificate() {
        X509Certificate certificate = null;
        try {
            FileInputStream certFileInputStream = new FileInputStream("file.cer");
            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
            certificate = (X509Certificate) certFactory.generateCertificate(certFileInputStream);
            certFileInputStream.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
        return certificate;
    }
    
    @Override
    public boolean handleFault(MessageContext messageContext, Object endpoint) throws Exception {
        return true;
    }

    @Override
    public void afterCompletion(MessageContext messageContext, Object endpoint, Exception ex) throws Exception {
    }

    @Override
    public boolean understands(SoapHeaderElement header) {
        return true; // without this the server fails to understand the request
    }
}

© www.soinside.com 2019 - 2024. All rights reserved.