verifyEnvelopedXmlSignature function
Verifies the enveloped signature (a ds:Signature child of the root)
of document. At least one reference must cover the whole document
(URI="" or the root element's Id, with the enveloped-signature
transform), so a signature over only its own properties is refused.
Supported: exclusive and inclusive C14N 1.0; SHA-1/256/384/512
digests; RSA PKCS#1 v1.5, RSASSA-PSS (the xmldsig-more#sha*-rsa-MGF1
family) and ECDSA signature methods.
Implementation
XmlSignatureVerification verifyEnvelopedXmlSignature(XmlLiteDocument document) {
final problems = <String>[];
XmlLiteElement? signature;
for (final e in document.root.childElements) {
if (e.localName == 'Signature' && e.namespaceUri == _dsigNs) {
signature = e;
}
}
if (signature == null) {
return XmlSignatureVerification(false, null, ['document is not signed']);
}
final signedInfo = signature.child('SignedInfo');
final signatureValue = signature.child('SignatureValue');
if (signedInfo == null || signatureValue == null) {
return XmlSignatureVerification(false, null, ['malformed ds:Signature']);
}
X509Certificate? signer;
final certText = signature
.descendantsNamed('X509Certificate')
.map((e) => e.text)
.firstOrNull;
if (certText != null) {
try {
signer = X509Certificate.parse(_base64(certText));
} on Object catch (e) {
problems.add('cannot parse the signing certificate: $e');
}
}
if (signer == null) {
return XmlSignatureVerification(
false, null, [...problems, 'no signing certificate in KeyInfo']);
}
final c14nUri =
signedInfo.child('CanonicalizationMethod')?.attribute('Algorithm') ?? '';
final c14n = XmlC14nMethod.fromUri(c14nUri);
if (c14n == null) {
return XmlSignatureVerification(
false, signer, ['unsupported canonicalization $c14nUri']);
}
var coversDocument = false;
var referencesOk = true;
for (final ref in signedInfo.childrenNamed('Reference')) {
final uri = ref.attribute('URI') ?? '';
final transforms = [
for (final t in ref.child('Transforms')?.childrenNamed('Transform') ??
const <XmlLiteElement>[])
t,
];
XmlLiteElement target;
if (uri.isEmpty) {
target = document.root;
} else if (uri.startsWith('#')) {
final found = document.elementById(uri.substring(1));
if (found == null) {
problems.add('reference $uri points at nothing');
referencesOk = false;
continue;
}
target = found;
} else {
problems.add('unsupported external reference $uri');
referencesOk = false;
continue;
}
var envelope = false;
var method = XmlC14nMethod.inclusive; // the default after node-set
var prefixes = const <String>[];
var supported = true;
for (final t in transforms) {
final alg = t.attribute('Algorithm') ?? '';
if (alg == _enveloped) {
envelope = true;
} else if (XmlC14nMethod.fromUri(alg) case final m?) {
method = m;
final list = t.child('InclusiveNamespaces')?.attribute('PrefixList');
if (list != null) {
prefixes =
list.split(RegExp(r'\s+')).where((p) => p.isNotEmpty).toList();
}
} else {
problems.add('unsupported transform $alg');
supported = false;
}
}
if (!supported) {
referencesOk = false;
continue;
}
final hash = _digestFor(ref.child('DigestMethod')?.attribute('Algorithm'));
if (hash == null) {
problems.add('unsupported digest method on reference "$uri"');
referencesOk = false;
continue;
}
final Uint8List bytes;
if (identical(target, document.root) && uri.isEmpty) {
bytes = canonicalizeDocument(document,
method: method,
omit: envelope ? signature : null,
inclusivePrefixes: prefixes);
} else {
bytes = canonicalizeElement(target,
method: method,
omit: envelope ? signature : null,
inclusivePrefixes: prefixes);
}
final expected = _base64(ref.child('DigestValue')?.text ?? '');
if (!_equal(hash.convert(bytes).bytes, expected)) {
problems.add('digest mismatch on reference "$uri"');
referencesOk = false;
continue;
}
if (identical(target, document.root) && envelope) coversDocument = true;
}
if (!coversDocument) {
problems.add('no reference covers the whole document');
}
final signed = canonicalizeElement(signedInfo,
method: c14n,
inclusivePrefixes: signedInfo
.child('CanonicalizationMethod')
?.child('InclusiveNamespaces')
?.attribute('PrefixList')
?.split(RegExp(r'\s+'))
.where((p) => p.isNotEmpty)
.toList() ??
const []);
final method =
signedInfo.child('SignatureMethod')?.attribute('Algorithm') ?? '';
final sigOk = _verify(method, signer, signed, _base64(signatureValue.text));
if (sigOk == null) {
problems.add('unsupported signature method $method');
} else if (!sigOk) {
problems.add('signature does not verify');
}
return XmlSignatureVerification(
referencesOk && coversDocument && sigOk == true, signer, problems);
}