verifyEnvelopedXmlSignature function

XmlSignatureVerification verifyEnvelopedXmlSignature(
  1. XmlLiteDocument document
)

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);
}