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