Szenario 5.5: Name Constraints

Kategorie: Validierung & Vertrauen
Komplexität: ⭐⭐⭐⭐ (Hoch)
Voraussetzungen: CA-Zertifikat mit Name Constraints Extension
Geschätzte Zeit: 15-20 Minuten


Beschreibung

Dieses Szenario beschreibt die Validierung von Name Constraints (RFC 5280 §4.2.1.10). Name Constraints schränken ein, welche Namen eine CA in Zertifikaten verwenden darf:

Anwendungsfälle:


Workflow

flowchart TD CERT[End-Entity Zertifikat] --> EXTRACT[Subject + SANs extrahieren] EXTRACT --> CHECK_CHAIN[Für jede CA in Chain] CHECK_CHAIN --> HAS_NC{Name Constraints?} HAS_NC -->|Nein| NEXT[Nächste CA] HAS_NC -->|Ja| PERMITTED{In permitted?} PERMITTED -->|Nein| FAIL[Constraint verletzt] PERMITTED -->|Ja| EXCLUDED{In excluded?} EXCLUDED -->|Ja| FAIL EXCLUDED -->|Nein| NEXT NEXT --> CHECK_CHAIN NEXT --> OK[Alle Constraints erfüllt] style OK fill:#e8f5e9 style FAIL fill:#ffebee


Code-Beispiel: Name Constraints setzen

using WvdS.Security.Cryptography.X509Certificates.Extensions.PQ;
 
using var ctx = PqCryptoContext.Initialize();
 
// Root-CA laden
var rootCert = ctx.LoadCertificate("root-ca.crt.pem");
var rootKey = ctx.LoadPrivateKey("root-ca.key.pem", "RootPassword!");
 
// Intermediate-CA-Schlüssel generieren
using var intKey = ctx.GenerateKeyPair(PqAlgorithm.MlDsa65);
 
var dn = new DnBuilder()
    .AddCN("Internal CA - example.com Only")
    .AddO("Example GmbH")
    .AddC("DE")
    .Build();
 
var csr = ctx.CreateCertificateRequest(intKey, dn);
 
// Intermediate-CA mit Name Constraints erstellen
var intermediateCert = ctx.IssueCertificate(
    csr,
    issuerCert: rootCert,
    issuerKey: rootKey,
    validDays: 1825,  // 5 Jahre
    extensions: new ExtBuilder()
        .BasicConstraints(ca: true, pathLengthConstraint: 0, critical: true)
        .KeyUsage(KeyUsageFlags.KeyCertSign | KeyUsageFlags.CrlSign, critical: true)
        // Name Constraints Extension
        .NameConstraints(
            permitted: new NameConstraint[]
            {
                // Nur *.example.com erlaubt
                new DnsNameConstraint("example.com"),
                // E-Mail nur @example.com
                new EmailNameConstraint("example.com"),
                // IP-Bereich 192.168.0.0/16
                new IpNameConstraint("192.168.0.0", "255.255.0.0")
            },
            excluded: new NameConstraint[]
            {
                // Keine *.dev.example.com (Entwickler-Subdomains)
                new DnsNameConstraint("dev.example.com")
            },
            critical: true  // MUSS critical sein
        )
        .SubjectKeyIdentifier(intKey.PublicKey)
        .AuthorityKeyIdentifier(rootCert)
        .Build()
);
 
intermediateCert.ToPemFile("internal-intermediate-ca.crt.pem");
intKey.ToEncryptedPemFile("internal-intermediate-ca.key.pem", "IntPassword!");
 
Console.WriteLine("Intermediate-CA mit Name Constraints erstellt:");
Console.WriteLine("  Erlaubt: *.example.com, *@example.com, 192.168.0.0/16");
Console.WriteLine("  Verboten: *.dev.example.com");

Code-Beispiel: Name Constraints validieren

public class NameConstraintValidator
{
    public ValidationResult ValidateNameConstraints(
        X509Certificate2 endEntity,
        X509Certificate2Collection chain)
    {
        var result = new ValidationResult { IsValid = true };
 
        // Alle Namen aus End-Entity extrahieren
        var names = ExtractAllNames(endEntity);
 
        // Jede CA in der Chain prüfen
        foreach (var ca in chain)
        {
            var constraints = ExtractNameConstraints(ca);
            if (constraints == null) continue;
 
            foreach (var name in names)
            {
                // Permitted Subtrees prüfen
                if (constraints.Permitted.Any())
                {
                    bool isPermitted = constraints.Permitted.Any(p => MatchesConstraint(name, p));
                    if (!isPermitted)
                    {
                        result.IsValid = false;
                        result.Errors.Add($"Name '{name.Value}' nicht in permitted subtrees von {ca.Subject}");
                    }
                }
 
                // Excluded Subtrees prüfen
                bool isExcluded = constraints.Excluded.Any(e => MatchesConstraint(name, e));
                if (isExcluded)
                {
                    result.IsValid = false;
                    result.Errors.Add($"Name '{name.Value}' in excluded subtrees von {ca.Subject}");
                }
            }
        }
 
        return result;
    }
 
    private List<GeneralName> ExtractAllNames(X509Certificate2 cert)
    {
        var names = new List<GeneralName>();
 
        // Subject CN
        var cn = cert.GetNameInfo(X509NameType.SimpleName, false);
        if (!string.IsNullOrEmpty(cn))
        {
            names.Add(new GeneralName { Type = GeneralNameType.DnsName, Value = cn });
        }
 
        // Subject E-Mail
        var email = cert.GetNameInfo(X509NameType.EmailName, false);
        if (!string.IsNullOrEmpty(email))
        {
            names.Add(new GeneralName { Type = GeneralNameType.Email, Value = email });
        }
 
        // Subject Alternative Names
        var sanExt = cert.Extensions["2.5.29.17"];
        if (sanExt != null)
        {
            names.AddRange(ParseSan(sanExt.RawData));
        }
 
        return names;
    }
 
    private bool MatchesConstraint(GeneralName name, NameConstraint constraint)
    {
        if (name.Type != constraint.Type) return false;
 
        return constraint.Type switch
        {
            GeneralNameType.DnsName => MatchesDnsConstraint(name.Value, constraint.Value),
            GeneralNameType.Email => MatchesEmailConstraint(name.Value, constraint.Value),
            GeneralNameType.IpAddress => MatchesIpConstraint(name.Value, constraint.Value, constraint.Mask),
            GeneralNameType.DirectoryName => MatchesDnConstraint(name.Value, constraint.Value),
            _ => false
        };
    }
 
    private bool MatchesDnsConstraint(string dnsName, string constraint)
    {
        // Constraint ".example.com" matched "www.example.com" und "example.com"
        if (constraint.StartsWith("."))
        {
            return dnsName.EndsWith(constraint, StringComparison.OrdinalIgnoreCase) ||
                   dnsName.Equals(constraint.TrimStart('.'), StringComparison.OrdinalIgnoreCase);
        }
        // Constraint "example.com" matched auch alle Subdomains
        return dnsName.Equals(constraint, StringComparison.OrdinalIgnoreCase) ||
               dnsName.EndsWith("." + constraint, StringComparison.OrdinalIgnoreCase);
    }
 
    private bool MatchesEmailConstraint(string email, string constraint)
    {
        // Constraint "@example.com" matched alle E-Mails dieser Domain
        if (constraint.StartsWith("@"))
        {
            return email.EndsWith(constraint, StringComparison.OrdinalIgnoreCase);
        }
        // Constraint "example.com" matched auch alle Subdomains
        var domain = email.Split('@').LastOrDefault();
        return MatchesDnsConstraint(domain ?? "", constraint);
    }
 
    private bool MatchesIpConstraint(string ip, string network, string mask)
    {
        var ipBytes = IPAddress.Parse(ip).GetAddressBytes();
        var networkBytes = IPAddress.Parse(network).GetAddressBytes();
        var maskBytes = IPAddress.Parse(mask).GetAddressBytes();
 
        for (int i = 0; i < ipBytes.Length; i++)
        {
            if ((ipBytes[i] & maskBytes[i]) != (networkBytes[i] & maskBytes[i]))
            {
                return false;
            }
        }
        return true;
    }
}

Name Constraint Typen

Typ ASN.1 Tag Beispiel Beschreibung
dNSName 2 example.com DNS-Domäne
rfc822Name 1 @example.com E-Mail-Domäne
iPAddress 7 192.168.0.0/255.255.0.0 IP-Bereich
directoryName 4 O=Example DN-Präfix
uniformResourceIdentifier 6 https://example.com URI-Präfix

Typische Constraint-Szenarien

Szenario Permitted Excluded
Interne CA .internal.local (leer)
Abteilungs-CA .dept.example.com .admin.dept.example.com
Partner-CA .partner.com .example.com
IoT-CA 10.0.0.0/8 (leer)

X509Chain Name Constraint Prüfung

// .NET Framework führt Name Constraint Prüfung automatisch durch
var chain = new X509Chain();
chain.ChainPolicy.ExtraStore.Add(intermediateCert);
 
bool isValid = chain.Build(endEntityCert);
 
// Name Constraint Fehler erkennen
var ncErrors = chain.ChainElements
    .SelectMany(e => e.ChainElementStatus)
    .Where(s => s.Status == X509ChainStatusFlags.InvalidNameConstraints ||
                s.Status == X509ChainStatusFlags.HasNotSupportedNameConstraint ||
                s.Status == X509ChainStatusFlags.HasNotPermittedNameConstraint ||
                s.Status == X509ChainStatusFlags.HasExcludedNameConstraint)
    .ToList();
 
if (ncErrors.Any())
{
    Console.WriteLine("Name Constraint Verletzung:");
    foreach (var error in ncErrors)
    {
        Console.WriteLine($"  {error.StatusInformation}");
    }
}

Verwandte Szenarien

Beziehung Szenario Beschreibung
Voraussetzung 5.4 Policy Validation Policies prüfen
Verwandt 1.2 Intermediate-CA CA mit Constraints
Verwandt 1.3 CA-Hierarchie Constraint-Design

« ← 5.4 Policy Validation | ↑ Validierung-Übersicht | → Alle Szenarien »


Wolfgang van der Stille @ EMSR DATA d.o.o. - Post-Quantum Cryptography Professional