You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

xml-crypto v2.1.5生成的无Id根元素XML签名仍被API以“Invalid File”拒绝的排查求助

xml-crypto v2.1.5生成的无Id根元素XML签名仍被API以“Invalid File”拒绝的排查求助

我现在在基于Node.js开发一个XML签名方案,用来对接一个要求极其严格的外部API——需要给指定的「Seed」XML签名后获取访问令牌,但每次提交签名后的XML,API都会返回通用的「Invalid File」错误,试了好几种调整都没解决,想请教大家有没有遇到类似的问题或者排查方向。

我的技术栈

  • Node.js
  • xml-crypto: 2.1.5(项目依赖限制,没办法升级到最新版本)
  • @xmldom/xmldom: ^0.8.11

问题背景与已做的调整

这个API要求使用enveloped signature,而且根元素绝对不能被添加新属性(比如Id)。最开始用xml-crypto的时候,它会自动给根元素注入类似Id="_0"的属性用来做引用,这直接不符合API要求,后来我在addReference里传了最后一个参数true(也就是isEmptyUri),把URI设为"",这时候根元素不再被注入Id了,以为解决了问题,但API还是拒绝我的请求。

我现在怀疑问题出在XML规范化(Canonicalization)或者属性排序上——因为这个API对属性顺序有硬性要求,比如xmlns:xsd必须在xmlns:xsi前面。

为了满足这个属性排序要求,我写了一个自定义的Digest类,用来强制指定属性的排序规则,然后注册给xml-crypto使用,以下是关键代码:

签名核心函数

import { SignedXml } from "xml-crypto";
import { DOMParser } from "@xmldom/xmldom";
import Digest from "./Digest"; // 自定义摘要类

// ... 其他初始化逻辑 ...

signXml(xml) {
  // 注册自定义摘要算法
  SignedXml.HashAlgorithms["http://myDigestAlgorithm"] = Digest;

  const sig = new SignedXml();
  sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
  sig.canonicalizationAlgorithm = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";

  // 添加对整个文档的引用(URI=""),最后一个参数true是isEmptyUri,避免注入Id
  sig.addReference(
    "/*",
    ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
    "http://myDigestAlgorithm",
    undefined,
    undefined,
    undefined,
    true // <--- 关键参数:禁用根元素Id注入
  );

  sig.signingKey = this._privateKey;

  // 解析XML并清理空白文本节点
  const doc = new DOMParser().parseFromString(xml, "text/xml");
  this.cleanNodes(doc);

  sig.computeSignature(doc.toString());
  return sig.getSignedXml();
}

自定义Digest类核心代码

import crypto from "crypto";
import { DOMParser } from "@xmldom/xmldom";

class Digest {
  sortElements(elements) {
    // 按节点名称排序,确保xmlns:xsd在xmlns:xsi前面
    const comparator = (a, b) => {
      const nameA = a.nodeName || a.name || "";
      const nameB = b.nodeName || b.name || "";
      return nameA < nameB ? -1 : nameA > nameB ? 1 : 0;
    };
    const items = Array.from(elements || []);
    return items.sort(comparator);
  }

  getHash(xml) {
    const doc = new DOMParser().parseFromString(xml);
    const rootNode = doc?.childNodes?.[0];

    // 重新按排序后的顺序赋值属性,再计算哈希
    const attrs = rootNode.attributes;
    const sorted = this.sortElements(attrs);
    Object.assign(rootNode.attributes, sorted);

    const shasum = crypto.createHash("sha256");
    shasum.update(doc.toString(), "utf8");
    return shasum.digest("base64");
  }
}

export default Digest;

输入与输出XML示例

原始输入Seed XML

<?xml version="1.0" encoding="utf-8"?>
<SemillaModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <valor>random_base64_string</valor>
  <fecha>2025-11-23T11:07:44.9770592-04:00</fecha>
</SemillaModel>

签名后输出XML(被API拒绝)

<?xml version="1.0" encoding="utf-8"?>
<SemillaModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <valor>random_base64_string</valor>
  <fecha>2025-11-23T11:07:44.9770592-04:00</fecha>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
      <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
        <DigestValue>CALCULATED_DIGEST_HERE</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>CALCULATED_SIGNATURE_HERE</SignatureValue>
    <KeyInfo>
      <X509Data>
        <X509Certificate>BASE64_CERTIFICATE</X509Certificate>
      </X509Data>
    </KeyInfo>
  </Signature>
</SemillaModel>

核心疑问

因为API只返回通用错误,没有具体原因提示,实在有点无从下手,想请教大家两个关键问题:

  1. 在xml-crypto v2.1.5版本中,使用isEmptyUri: true这个参数有没有什么已知的实现细节,会导致生成的签名不符合标准enveloped signature的预期?比如对整个文档的引用处理是否和标准有差异?
  2. 我在自定义Digest的getHash方法里,对根元素属性排序后用doc.toString()序列化,这个方法真的能保证属性是按我排序后的顺序输出吗?会不会xmldom的toString方法有自己的属性排序逻辑,把我的排序覆盖了?

希望有经验的朋友能给点排查思路或者解决办法,谢谢大家!

火山引擎 最新活动