Skip to content

Commit 26d492a

Browse files
committed
Follow xml-crypto 3.2.1 (fix CVE-2025-29774 and CVE-2025-29775)
1 parent 3053731 commit 26d492a

File tree

8 files changed

+181
-23
lines changed

8 files changed

+181
-23
lines changed

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
## 3.2.0
1+
## 3.2.1
22

33
* The last version that supports Dart 2.x
4+
* Follow xml-crypto 3.2.1
5+
6+
This addresses two critical CVE:
7+
8+
* CVE-2025-29774
9+
* CVE-2025-29775
10+
11+
## 3.2.0
12+
413
* Follow xml-crypto 3.2.0
514
* Use inclusiveNamespacesPrefixList to generate InclusiveNamespaces
615
* Add support for appending attributes to KeyInfo element

lib/src/signed_xml.dart

Lines changed: 99 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,48 @@ class SignedXml {
202202

203203
final doc = parseFromString(xml);
204204

205+
// Reset the references as only references from our re-parsed signedInfo node can be trusted
206+
this.references.clear();
207+
208+
final unverifiedSignedInfoCanon = _getCanonSignedInfoXml(doc);
209+
if (unverifiedSignedInfoCanon.isEmpty) {
210+
if (callback != null) {
211+
callback(ArgumentError('Canonical signed info cannot be empty'), false);
212+
return false;
213+
}
214+
215+
throw ArgumentError('Canonical signed info cannot be empty');
216+
}
217+
218+
// unsigned, verify later to keep with consistent callback behavior
219+
final parsedUnverifiedSignedInfo =
220+
parseFromString(unverifiedSignedInfoCanon);
221+
final unverifiedSignedInfoDoc = parsedUnverifiedSignedInfo.document;
222+
if (unverifiedSignedInfoDoc == null) {
223+
if (callback != null) {
224+
callback(
225+
ArgumentError('Could not parse signedInfoCanon into a document'),
226+
false);
227+
return false;
228+
}
229+
230+
throw ArgumentError('Could not parse signedInfoCanon into a document');
231+
}
232+
233+
final references = findChilds(unverifiedSignedInfoDoc, 'Reference');
234+
if (references.isEmpty) {
235+
if (callback != null) {
236+
callback(ArgumentError('could not find any Reference elements'), false);
237+
return false;
238+
}
239+
240+
throw ArgumentError('could not find any Reference elements');
241+
}
242+
243+
for (var reference in references) {
244+
_loadReference(reference);
245+
}
246+
205247
if (!_validateReferences(doc)) {
206248
if (callback == null) {
207249
return false;
@@ -211,6 +253,7 @@ class SignedXml {
211253
}
212254
}
213255

256+
// Stage B: Take the signature algorithm and key and verify the SignatureValue against the canonicalized SignedInfo
214257
if (callback == null) {
215258
//Synchronous flow
216259
if (!_validateSignatureValue(doc)) {
@@ -237,6 +280,10 @@ class SignedXml {
237280
if (signedInfo.isEmpty) {
238281
throw ArgumentError('could not find SignedInfo element in the message');
239282
}
283+
if (signedInfo.length > 1) {
284+
throw ArgumentError(
285+
'could not get canonicalized signed info for a signature that contains multiple SignedInfo nodes');
286+
}
240287

241288
// Since in Dart the doc is always a XmlDocument
242289
// if (canonicalizationAlgorithm == 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'
@@ -321,7 +368,9 @@ class SignedXml {
321368

322369
bool _validateReferences(XmlDocument doc) {
323370
for (final ref in references) {
324-
final uri = ref.uri.startsWith('#') ? ref.uri.substring(1) : ref.uri;
371+
final uri = ref.uri != null
372+
? (ref.uri!.startsWith('#') ? ref.uri!.substring(1) : ref.uri!)
373+
: '';
325374
final elem = <XPathNode<XmlNode>>[];
326375

327376
if (uri == '') {
@@ -399,15 +448,46 @@ class SignedXml {
399448
throw ArgumentError('could not find SignatureMethod/@Algorithm element');
400449
}
401450
signatureAlgorithm = nodes.attr ?? '';
402-
references.clear();
403-
final refs = XmlXPath.node(signatureNode)
404-
.query(".//*[local-name()='SignedInfo']/*[local-name()='Reference']");
405-
if (refs.nodes.isEmpty) {
451+
final signedInfoNodes = findChilds(signatureNode, 'SignedInfo');
452+
if (signedInfoNodes.isEmpty) {
453+
throw ArgumentError('no signed info node found');
454+
}
455+
if (signedInfoNodes.length > 1) {
456+
throw ArgumentError(
457+
'could not load signature that contains multiple SignedInfo nodes');
458+
}
459+
460+
// Try to operate on the c14n version of signedInfo. This forces the initial getReferences()
461+
// API call to always return references that are loaded under the canonical SignedInfo
462+
// in the case that the client access the .references **before** signature verification.
463+
464+
// Ensure canonicalization algorithm is exclusive, otherwise we'd need the entire document
465+
var canonicalizationAlgorithmForSignedInfo = canonicalizationAlgorithm;
466+
if (canonicalizationAlgorithmForSignedInfo ==
467+
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315" ||
468+
canonicalizationAlgorithmForSignedInfo ==
469+
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments") {
470+
canonicalizationAlgorithmForSignedInfo =
471+
"http://www.w3.org/2001/10/xml-exc-c14n#";
472+
}
473+
474+
final temporaryCanonSignedInfo = _getCanonXml(
475+
[canonicalizationAlgorithm],
476+
signedInfoNodes.first,
477+
);
478+
final temporaryCanonSignedInfoXml =
479+
parseFromString(temporaryCanonSignedInfo);
480+
final signedInfoDoc = temporaryCanonSignedInfoXml.rootElement;
481+
482+
this.references.clear();
483+
484+
final references = findChilds(signedInfoDoc, 'Reference');
485+
if (references.isEmpty) {
406486
throw ArgumentError('could not find any Reference elements');
407487
}
408488

409-
for (final ref in refs.nodes) {
410-
_loadReference(ref.node);
489+
for (final ref in references) {
490+
_loadReference(ref);
411491
}
412492

413493
signatureValue =
@@ -439,12 +519,16 @@ class SignedXml {
439519
if (nodes.isEmpty) {
440520
throw ArgumentError('could not find DigestValue in reference $ref');
441521
}
442-
if (nodes.first.firstChild == null ||
443-
(nodes.first.firstChild?.value ?? '') == '') {
522+
523+
if (nodes.length > 1) {
444524
throw ArgumentError(
445-
'could not find the value of DigestValue in ${nodes.first}');
525+
'could not load reference for a node that contains multiple DigestValue nodes: $ref');
526+
}
527+
528+
final digestValue = nodes.first.innerText;
529+
if (digestValue.isEmpty) {
530+
throw ArgumentError('could not find the value of DigestValue in $ref');
446531
}
447-
final digestValue = nodes.first.firstChild!.value;
448532

449533
final transforms = <String>[];
450534
String? inclusiveNamespacesPrefixList;
@@ -490,14 +574,9 @@ class SignedXml {
490574
transforms.add('http://www.w3.org/TR/2001/REC-xml-c14n-20010315');
491575
}
492576

493-
addReference(
494-
null,
495-
transforms,
496-
digestAlgo,
497-
findAttr(ref, 'URI')?.value ?? '',
498-
digestValue,
499-
inclusiveNamespacesPrefixList,
500-
false);
577+
final refUri = ref.getAttribute('URI');
578+
addReference(null, transforms, digestAlgo, refUri, digestValue,
579+
inclusiveNamespacesPrefixList, false);
501580
}
502581

503582
void addReference(String? xpath,
@@ -1079,7 +1158,7 @@ class _Reference {
10791158
String? xpath;
10801159
final List<String> transforms;
10811160
final String digestAlgorithm;
1082-
String uri;
1161+
String? uri;
10831162
final String digestValue;
10841163
final String? inclusiveNamespacesPrefixList;
10851164
final bool isEmptyUri;

pubspec.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
name: xml_crypto
2-
version: 3.2.0
2+
version: 3.2.1
33
description: |
44
Xml digital signature library for Dart. For signing and verifying XML documents.
55
homepage: https://github.com/rikulo/xml-crypto
66
repository: https://github.com/rikulo/xml-crypto
77
issue_tracker: https://github.com/rikulo/xml-crypto/issues
88

99
environment:
10-
sdk: '>=2.12.0 <3.0.0'
10+
sdk: '>=2.12.0 <4.0.0'
1111

1212
dependencies:
1313
xml: ^6.3.0

test/saml_response_test.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,56 @@ void main() {
8181
sig.loadSignature(signature);
8282
expect(sig.checkSignature(xml), isFalse);
8383
});
84+
85+
test('test validating SAML response with digest comment', () {
86+
final xml = File('./test/static/valid_saml_with_digest_comment.xml').readAsStringSync();
87+
final doc = parseFromString(xml);
88+
final signature = XmlXPath.node(doc)
89+
.query("//*[local-name()='Signature' and namespace()='ds']") // FIXME should use namespace-uri()
90+
.node?.node;
91+
92+
final sig = SignedXml();
93+
sig.keyInfoProvider = FileKeyInfo('./test/static/feide_public.pem');
94+
sig.loadSignature(signature);
95+
expect(sig.checkSignature(xml), isFalse);
96+
expect(sig.references.first.digestValue, 'RnNjoyUguwze5w2R+cboyTHlkQk=');
97+
});
98+
99+
test('test signature contains a `SignedInfo` node', () {
100+
final xml = File('./test/static/invalid_saml_no_signed_info.xml').readAsStringSync();
101+
final doc = parseFromString(xml);
102+
final signature = XmlXPath.node(doc)
103+
.query("/*/*[local-name()='Signature' and namespace()='ds']") // FIXME should use namespace-uri()
104+
.node?.node;
105+
106+
final sig = SignedXml();
107+
sig.keyInfoProvider = FileKeyInfo('./test/static/feide_public.pem');
108+
expect(() => sig.loadSignature(signature), throwsArgumentError);
109+
});
110+
111+
test('test validation ignores an additional wrapped `SignedInfo` node', () {
112+
final xml = File('./test/static/saml_wrapped_signed_info_node.xml').readAsStringSync();
113+
final doc = parseFromString(xml);
114+
final signature = XmlXPath.node(doc)
115+
.query("//*[local-name()='Signature' and namespace()='ds']") // FIXME should use namespace-uri()
116+
.node?.node;
117+
118+
final sig = SignedXml();
119+
sig.keyInfoProvider = FileKeyInfo('./test/static/saml_external_ns.pem');
120+
sig.loadSignature(signature);
121+
expect(sig.references.length, 1);
122+
expect(sig.checkSignature(xml), isTrue);
123+
});
124+
125+
test('test signature does not contain multiple `SignedInfo` nodes', () {
126+
final xml = File('./test/static/saml_multiple_signed_info_nodes.xml').readAsStringSync();
127+
final doc = parseFromString(xml);
128+
final signature = XmlXPath.node(doc)
129+
.query("//*[local-name()='Signature' and namespace()='ds']") // FIXME should use namespace-uri()
130+
.node?.node;
131+
132+
final sig = SignedXml();
133+
sig.keyInfoProvider = FileKeyInfo('./test/static/saml_external_ns.pem');
134+
expect(() => sig.loadSignature(signature), throwsArgumentError);
135+
});
84136
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="pfx94e4a319-b6f7-4a40-25d1-01fcb642e4c5" Version="2.0" IssueInstant="2012-07-03T11:32:20Z" Destination="http://localhost:3000/login/callback" InResponseTo="_d766d16611ac0d14121b"><saml:Issuer>https://openidp.feide.no</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
2+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
3+
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
4+
<ds:SignatureValue>dkONrkxW+LSuDvnNMG/mWYFa47d2WGyapLhXSTYqrlT9Td+tT7ciojNJ55WTaPaCMt7IrGtIxxskPAZIjdIn5pRyDxHr0joWxzZ7oZHCOI1CnQV5HjOq+rzzmEN2LctCZ6S4hbL7SQ1qJ3vp2BCXAygy4tmJOURQdnk0KLwwRS8=</ds:SignatureValue>
5+
<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIICizCCAfQCCQCY8tKaMc0BMjANBgkqhkiG9w0BAQUFADCBiTELMAkGA1UEBhMCTk8xEjAQBgNVBAgTCVRyb25kaGVpbTEQMA4GA1UEChMHVU5JTkVUVDEOMAwGA1UECxMFRmVpZGUxGTAXBgNVBAMTEG9wZW5pZHAuZmVpZGUubm8xKTAnBgkqhkiG9w0BCQEWGmFuZHJlYXMuc29sYmVyZ0B1bmluZXR0Lm5vMB4XDTA4MDUwODA5MjI0OFoXDTM1MDkyMzA5MjI0OFowgYkxCzAJBgNVBAYTAk5PMRIwEAYDVQQIEwlUcm9uZGhlaW0xEDAOBgNVBAoTB1VOSU5FVFQxDjAMBgNVBAsTBUZlaWRlMRkwFwYDVQQDExBvcGVuaWRwLmZlaWRlLm5vMSkwJwYJKoZIhvcNAQkBFhphbmRyZWFzLnNvbGJlcmdAdW5pbmV0dC5ubzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAt8jLoqI1VTlxAZ2axiDIThWcAOXdu8KkVUWaN/SooO9O0QQ7KRUjSGKN9JK65AFRDXQkWPAu4HlnO4noYlFSLnYyDxI66LCr71x4lgFJjqLeAvB/GqBqFfIZ3YK/NrhnUqFwZu63nLrZjcUZxNaPjOOSRSDaXpv1kb5k3jOiSGECAwEAATANBgkqhkiG9w0BAQUFAAOBgQBQYj4cAafWaYfjBU2zi1ElwStIaJ5nyp/s/8B8SAPK2T79McMyccP3wSW13LHkmM1jwKe3ACFXBvqGQN0IbcH49hu0FKhYFM/GPDJcIHFBsiyMBXChpye9vBaTNEBCtU3KjjyG0hRT2mAQ9h+bkPmOvlEo/aH0xR68Z9hw4PF13w==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status><saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="pfx66496e6c-3c29-230d-6d47-b245434b872d" Version="2.0" IssueInstant="2012-07-03T11:32:20Z"><saml:Issuer>https://openidp.feide.no</saml:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
6+
<ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
7+
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
8+
<ds:Reference URI="#pfx66496e6c-3c29-230d-6d47-b245434b872d"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>RnNjoyUguwze5w2R+cboyTHlkQk=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>aw5711jKP7xragunjRRCAD4mT4xKHc37iohBpQDbdSomD3ksOSB96UZQp0MtaC3xlVSkMtYw85Om96T2q2xrxLLYVA50eFJEMMF7SCVPStWTVjBlaCuOPEQxIaHyJs9Sy3MCEfbBh4Pqn9IJBd1kzwdlCrWWjAmksbFFg5wHQJA=</ds:SignatureValue>
9+
<ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIICizCCAfQCCQCY8tKaMc0BMjANBgkqhkiG9w0BAQUFADCBiTELMAkGA1UEBhMCTk8xEjAQBgNVBAgTCVRyb25kaGVpbTEQMA4GA1UEChMHVU5JTkVUVDEOMAwGA1UECxMFRmVpZGUxGTAXBgNVBAMTEG9wZW5pZHAuZmVpZGUubm8xKTAnBgkqhkiG9w0BCQEWGmFuZHJlYXMuc29sYmVyZ0B1bmluZXR0Lm5vMB4XDTA4MDUwODA5MjI0OFoXDTM1MDkyMzA5MjI0OFowgYkxCzAJBgNVBAYTAk5PMRIwEAYDVQQIEwlUcm9uZGhlaW0xEDAOBgNVBAoTB1VOSU5FVFQxDjAMBgNVBAsTBUZlaWRlMRkwFwYDVQQDExBvcGVuaWRwLmZlaWRlLm5vMSkwJwYJKoZIhvcNAQkBFhphbmRyZWFzLnNvbGJlcmdAdW5pbmV0dC5ubzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAt8jLoqI1VTlxAZ2axiDIThWcAOXdu8KkVUWaN/SooO9O0QQ7KRUjSGKN9JK65AFRDXQkWPAu4HlnO4noYlFSLnYyDxI66LCr71x4lgFJjqLeAvB/GqBqFfIZ3YK/NrhnUqFwZu63nLrZjcUZxNaPjOOSRSDaXpv1kb5k3jOiSGECAwEAATANBgkqhkiG9w0BAQUFAAOBgQBQYj4cAafWaYfjBU2zi1ElwStIaJ5nyp/s/8B8SAPK2T79McMyccP3wSW13LHkmM1jwKe3ACFXBvqGQN0IbcH49hu0FKhYFM/GPDJcIHFBsiyMBXChpye9vBaTNEBCtU3KjjyG0hRT2mAQ9h+bkPmOvlEo/aH0xR68Z9hw4PF13w==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature><saml:Subject><saml:NameID SPNameQualifier="passport-saml" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_6c5dcaa3053321ff4d63785fbc3f67c59a129cde82</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2012-07-03T11:37:20Z" Recipient="http://localhost:3000/login/callback" InResponseTo="_d766d16611ac0d14121b"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2012-07-03T11:31:50Z" NotOnOrAfter="2012-07-03T11:37:20Z"><saml:AudienceRestriction><saml:Audience>passport-saml</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2012-07-03T11:32:20Z" SessionNotOnOrAfter="2012-07-03T19:32:20Z" SessionIndex="_c8e6823fe38ddbce125f9be6e5118b8c352d04bcae"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">bergie</saml:AttributeValue></saml:Attribute><saml:Attribute Name="givenName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">Henri</saml:AttributeValue></saml:Attribute><saml:Attribute Name="sn" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">Bergius</saml:AttributeValue></saml:Attribute><saml:Attribute Name="cn" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">Henri Bergius</saml:AttributeValue></saml:Attribute><saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">[email protected]</saml:AttributeValue></saml:Attribute><saml:Attribute Name="eduPersonPrincipalName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">[email protected]</saml:AttributeValue></saml:Attribute><saml:Attribute Name="eduPersonTargetedID" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">8216c78fe244502efa13f62e6615c94acb7bdf3e</saml:AttributeValue></saml:Attribute><saml:Attribute Name="urn:oid:0.9.2342.19200300.100.1.1" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">bergie</saml:AttributeValue></saml:Attribute><saml:Attribute Name="urn:oid:2.5.4.42" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">Henri</saml:AttributeValue></saml:Attribute><saml:Attribute Name="urn:oid:2.5.4.4" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">Bergius</saml:AttributeValue></saml:Attribute><saml:Attribute Name="urn:oid:2.5.4.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">Henri Bergius</saml:AttributeValue></saml:Attribute><saml:Attribute Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">[email protected]</saml:AttributeValue></saml:Attribute><saml:Attribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">[email protected]</saml:AttributeValue></saml:Attribute><saml:Attribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"><saml:AttributeValue xsi:type="xs:string">8216c78fe244502efa13f62e6615c94acb7bdf3e</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>

0 commit comments

Comments
 (0)