Skip to content

Read certificate AIA hints without confusing retrieval with trust

Read certificate AIA hints without confusing retrieval with trust

Illustrative code, not executed.

Original English publication date: 2023-10-29T11:43:00.008+02:00. Original English article

Bouncy Castle library logo from the original article

Authority Information Access can describe issuer-certificate locations and OCSP services. They are different access methods: caIssuers is not an OCSP response endpoint. An AIA location is a discovery hint supplied inside a certificate, not permission to fetch a URL and not evidence that a returned issuer is trusted. The extension may be absent, and following it need not yield a complete chain. [1]

Extract hints without fetching them

This Bouncy Castle example handles a missing extension, limits input and entry counts, checks the GeneralName URI tag and returns only syntactically acceptable HTTPS candidates. HTTPS-only is a deliberately restrictive example policy and may exclude legitimate HTTP AIA locations. These checks do not make a URL safe to request: no DNS lookup or network call occurs. [2]

using System;
using System.Collections.Generic;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.X509;

public static class AiaHintsExample
{
    // Parse only. Returned URIs are untrusted, not approved network destinations.
    public static IReadOnlyList<Uri> ReadHttpsIssuerHints(X509Certificate certificate)
    {
        if (certificate == null) throw new ArgumentNullException(nameof(certificate));
        var output = new List<Uri>();
        var value = certificate.GetExtensionValue(X509Extensions.AuthorityInfoAccess);
        if (value == null) return output;
        byte[] encoded = value.GetOctets();
        if (encoded.Length == 0 || encoded.Length > 65536)
            throw new ArgumentException("AIA exceeds this parser's size policy.");
        var aia = AuthorityInformationAccess.GetInstance(Asn1Object.FromByteArray(encoded));
        AccessDescription[] descriptions = aia.GetAccessDescriptions();
        if (descriptions.Length > 32) throw new ArgumentException("Too many AIA entries.");
        var seen = new HashSet<string>(StringComparer.Ordinal);
        foreach (AccessDescription description in descriptions)
        {
            if (!description.AccessMethod.Equals(X509ObjectIdentifiers.IdADCAIssuers)) continue;
            GeneralName location = description.AccessLocation;
            if (location.TagNo != GeneralName.UniformResourceIdentifier) continue;
            string text = DerIA5String.GetInstance(location.Name).GetString();
            if (text.Length > 4096 || !Uri.TryCreate(text, UriKind.Absolute, out Uri? uri)) continue;
            if (uri.Scheme != Uri.UriSchemeHttps || uri.Port != 443 ||
                string.IsNullOrEmpty(uri.Host) || uri.UserInfo.Length != 0 || uri.Fragment.Length != 0)
                continue;
            if (seen.Add(uri.AbsoluteUri)) output.Add(uri);
        }
        return output;
    }
}

If retrieval is added later

A separate approved fetcher needs an exact host/port allowlist and network egress enforcement. Reject loopback, private, link-local, multicast and other non-approved destination addresses, including IPv4-mapped IPv6 forms. Validate every DNS answer and pin the approved connection destination or use a controlled proxy to prevent DNS rebinding. Disable redirects or reapply the entire policy to each hop. Do not forward credentials, cookies or ambient authentication, and do not permit file, LDAP or arbitrary URL schemes. [3]

Bound connect/read/total time, response bytes after decompression, certificate count, retries, recursion depth and total requests. Deduplicate certificate fingerprints and visited URLs to stop cycles. Parse only the explicitly supported certificate/container formats; caIssuers can return a certificate or a certs-only CMS container, so assuming one DER certificate is not universal. Preserve diagnostic failures and try permitted alternatives within the budget; do not swallow every exception and call the result a full chain. [1][3]

Validate under an independently chosen trust policy

A matching issuer/subject distinguished name is not a signature check. Even a correctly self-signed certificate is not automatically a trust anchor. Use a chain validator to verify signatures, CA/basic constraints, key usage, validity, path/name constraints and the intended application purpose. Trust anchors must be selected independently of downloaded AIA certificates; an intermediate goes into an extra store, not a trusted-root store. [1]

The following modern-.NET illustration accepts a leaf, candidate intermediates and approved roots as inputs, disables AIA downloads and uses cached revocation information. Platform chain engines may also consult local intermediate caches; this is not a hermetic validator. Missing or stale revocation information is a validation failure, not a reason to turn revocation off. A valid result still requires application-specific identity/hostname and authorization checks. This code was not executed, and no chain or trust store was inspected or changed. [4][5][6]

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

public static class OfflineChainExample
{
    public static bool Validate(X509Certificate2 leaf,
        IReadOnlyCollection<X509Certificate2> candidateIntermediates,
        IReadOnlyCollection<X509Certificate2> independentlyApprovedRoots,
        string requiredApplicationOid, DateTime verificationTimeUtc)
    {
        if (leaf == null || candidateIntermediates == null || independentlyApprovedRoots == null)
            throw new ArgumentNullException("Certificate inputs");
        if (candidateIntermediates.Count > 32 || independentlyApprovedRoots.Count < 1 ||
            independentlyApprovedRoots.Count > 32 || string.IsNullOrWhiteSpace(requiredApplicationOid) ||
            verificationTimeUtc.Kind != DateTimeKind.Utc)
            throw new ArgumentException("Explicit bounded trust, purpose and UTC time are required.");
        using var chain = new X509Chain();
        chain.ChainPolicy.DisableCertificateDownloads = true;
        chain.ChainPolicy.RevocationMode = X509RevocationMode.Offline;
        chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
        chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
        chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
        chain.ChainPolicy.VerificationTime = verificationTimeUtc;
        chain.ChainPolicy.ApplicationPolicy.Add(new Oid(requiredApplicationOid));
        foreach (var root in independentlyApprovedRoots) chain.ChainPolicy.CustomTrustStore.Add(root);
        foreach (var issuer in candidateIntermediates) chain.ChainPolicy.ExtraStore.Add(issuer);
        bool valid = chain.Build(leaf);
        // Retain ChainStatus for controlled diagnostics in a real application.
        return valid && chain.ChainStatus.Length == 0 && chain.ChainElements.Count <= 10;
    }
}

Primary references

  1. RFC 5280 sections 4.2.2.1 and 6
  2. Bouncy Castle AuthorityInformationAccess source
  3. OWASP SSRF Prevention Cheat Sheet
  4. Microsoft DisableCertificateDownloads
  5. Microsoft X509RevocationMode
  6. Microsoft X509ChainPolicy.TrustMode

Post a Comment

Previous Post Next Post