parseAatlSecuritySettings function

PdfAatlSnapshot parseAatlSecuritySettings(
  1. Uint8List file, {
  2. bool verifySignature = true,
  3. String rootFingerprint = PdfAatl.adobeRootFingerprint,
  4. Duration? maxAge,
  5. DateTime? now,
})

Reads the trusted roots out of an AATL .acrobatsecuritysettings file.

Unless verifySignature is false, the file's PDF signature must be intact, cover the whole file, and chain to Adobe Root CA G2 (PdfAatl.adobeRootFingerprint); otherwise a FormatException is thrown. Identities are kept when the file adds them (ImportAction 1 or 2, not 3 = remove) and marks them as trusted roots (<Root>1</Root>).

rootFingerprint overrides the pinned root (tests, or a future Adobe root).

Unlike an ETSI trusted list, the AATL file carries no NextUpdate or expiry field - only the date Adobe signed it. Pass maxAge to refuse a file signed longer ago than that before now (Acrobat itself refreshes it periodically); by default the age is not judged.

Implementation

PdfAatlSnapshot parseAatlSecuritySettings(Uint8List file,
    {bool verifySignature = true,
    String rootFingerprint = PdfAatl.adobeRootFingerprint,
    Duration? maxAge,
    DateTime? now}) {
  final document = PdfDocument.open(file);
  DateTime? signedAt;
  String? signer;
  if (verifySignature) {
    final signatures = PdfSignature.of(document);
    if (signatures.isEmpty) {
      throw const FormatException('the AATL file is not signed');
    }
    final signature = signatures.last;
    final unanchored = signature.validate();
    final root = unanchored.certificates.where(
        (c) => crypto.sha256.convert(c.der).toString() == rootFingerprint);
    if (root.isEmpty) {
      throw const FormatException(
          'the AATL file is not signed under Adobe Root CA G2');
    }
    final result = signature.validate(
        trustStore: PdfTrustStore()..addCertificate(root.first));
    if (!result.intact ||
        !result.coversWholeDocument ||
        result.chainTrusted != true) {
      throw FormatException('the AATL file signature does not verify: '
          '${[...result.problems, ...result.chainProblems].join('; ')}');
    }
    signedAt = result.signedAt ?? signature.signingTime;
    signer = result.signerCertificate?.subjectCommonName;
    if (maxAge != null) {
      final at = (now ?? DateTime.now()).toUtc();
      if (signedAt == null || at.difference(signedAt) > maxAge) {
        throw FormatException('the AATL file is older than $maxAge '
            '(signed ${signedAt?.toIso8601String() ?? 'at an unknown time'})');
      }
    }
  }
  Uint8List? xml;
  for (final attachment in PdfAttachments.of(document).all) {
    if (attachment.name.toLowerCase().endsWith('securitysettings.xml')) {
      xml = attachment.bytes();
    }
  }
  if (xml == null) {
    throw const FormatException('no SecuritySettings.xml in the AATL file');
  }
  final settings = XmlLiteDocument.parse(xml);
  final anchors = <Uint8List>[];
  for (final identity in settings.root.descendantsNamed('Identity')) {
    final action = identity.child('ImportAction')?.text.trim();
    final isRoot = identity.child('Trust')?.child('Root')?.text.trim() == '1';
    final cert = identity.child('Certificate')?.text;
    if ((action != '1' && action != '2') || !isRoot || cert == null) continue;
    try {
      final der = base64.decode(cert.replaceAll(RegExp(r'\s'), ''));
      X509Certificate.parse(der);
      anchors.add(der);
    } on Object {
      // skip an unparsable identity
    }
  }
  return PdfAatlSnapshot(anchors, signedAt: signedAt, signer: signer);
}