Scenario 5.5: Name Constraints

Categoria: Validazione e fiducia
Complessità: Alta
Prerequisiti: Certificato CA con estensione Name Constraints
Tempo stimato: 15-20 minuti


Descrizione

Questo scenario descrive la validazione dei Name Constraints (RFC 5280 paragrafo 4.2.1.10). I Name Constraints limitano quali nomi una CA può utilizzare nei certificati:

Casi d'uso:


Workflow

flowchart TD CERT[Certificato End-Entity] --> EXTRACT[Estrarre Subject + SAN] EXTRACT --> CHECK_CHAIN[Per ogni CA nella chain] CHECK_CHAIN --> HAS_NC{Name Constraints?} HAS_NC -->|No| NEXT[CA successiva] HAS_NC -->|Si| PERMITTED{In permitted?} PERMITTED -->|No| FAIL[Constraint violato] PERMITTED -->|Si| EXCLUDED{In excluded?} EXCLUDED -->|Si| FAIL EXCLUDED -->|No| NEXT NEXT --> CHECK_CHAIN NEXT --> OK[Tutti i constraints soddisfatti] style OK fill:#e8f5e9 style FAIL fill:#ffebee


Esempio codice: Impostare Name Constraints

using WvdS.Security.Cryptography.X509Certificates.Extensions.PQ;
 
using var ctx = PqCryptoContext.Initialize();
 
// Caricare Root-CA
var rootCert = ctx.LoadCertificate("root-ca.crt.pem");
var rootKey = ctx.LoadPrivateKey("root-ca.key.pem", "RootPassword!");
 
// Generare chiave Intermediate-CA
using var intKey = ctx.GenerateKeyPair(PqAlgorithm.MlDsa65);
 
var dn = new DnBuilder()
    .AddCN("Internal CA - Solo example.com")
    .AddO("Example GmbH")
    .AddC("DE")
    .Build();
 
var csr = ctx.CreateCertificateRequest(intKey, dn);
 
// Creare Intermediate-CA con Name Constraints
var intermediateCert = ctx.IssueCertificate(
    csr,
    issuerCert: rootCert,
    issuerKey: rootKey,
    validDays: 1825,  // 5 anni
    extensions: new ExtBuilder()
        .BasicConstraints(ca: true, pathLengthConstraint: 0, critical: true)
        .KeyUsage(KeyUsageFlags.KeyCertSign | KeyUsageFlags.CrlSign, critical: true)
        // Estensione Name Constraints
        .NameConstraints(
            permitted: new NameConstraint[]
            {
                // Solo *.example.com consentito
                new DnsNameConstraint("example.com"),
                // E-mail solo @example.com
                new EmailNameConstraint("example.com"),
                // Range IP 192.168.0.0/16
                new IpNameConstraint("192.168.0.0", "255.255.0.0")
            },
            excluded: new NameConstraint[]
            {
                // Nessun *.dev.example.com (sottodomini sviluppatori)
                new DnsNameConstraint("dev.example.com")
            },
            critical: true  // DEVE essere critical
        )
        .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 con Name Constraints creata:");
Console.WriteLine("  Consentiti: *.example.com, *@example.com, 192.168.0.0/16");
Console.WriteLine("  Vietati: *.dev.example.com");

Esempio codice: Validare Name Constraints

public class NameConstraintValidator
{
    public ValidationResult ValidateNameConstraints(
        X509Certificate2 endEntity,
        X509Certificate2Collection chain)
    {
        var result = new ValidationResult { IsValid = true };
 
        // Estrarre tutti i nomi dall'End-Entity
        var names = ExtractAllNames(endEntity);
 
        // Verificare ogni CA nella chain
        foreach (var ca in chain)
        {
            var constraints = ExtractNameConstraints(ca);
            if (constraints == null) continue;
 
            foreach (var name in names)
            {
                // Verificare Permitted Subtrees
                if (constraints.Permitted.Any())
                {
                    bool isPermitted = constraints.Permitted.Any(p => MatchesConstraint(name, p));
                    if (!isPermitted)
                    {
                        result.IsValid = false;
                        result.Errors.Add($"Nome '{name.Value}' non in permitted subtrees di {ca.Subject}");
                    }
                }
 
                // Verificare Excluded Subtrees
                bool isExcluded = constraints.Excluded.Any(e => MatchesConstraint(name, e));
                if (isExcluded)
                {
                    result.IsValid = false;
                    result.Errors.Add($"Nome '{name.Value}' in excluded subtrees di {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" corrisponde a "www.example.com" e "example.com"
        if (constraint.StartsWith("."))
        {
            return dnsName.EndsWith(constraint, StringComparison.OrdinalIgnoreCase) ||
                   dnsName.Equals(constraint.TrimStart('.'), StringComparison.OrdinalIgnoreCase);
        }
        // Constraint "example.com" corrisponde anche a tutti i sottodomini
        return dnsName.Equals(constraint, StringComparison.OrdinalIgnoreCase) ||
               dnsName.EndsWith("." + constraint, StringComparison.OrdinalIgnoreCase);
    }
 
    private bool MatchesEmailConstraint(string email, string constraint)
    {
        // Constraint "@example.com" corrisponde a tutte le e-mail di quel dominio
        if (constraint.StartsWith("@"))
        {
            return email.EndsWith(constraint, StringComparison.OrdinalIgnoreCase);
        }
        // Constraint "example.com" corrisponde anche a tutti i sottodomini
        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;
    }
}

Tipi di Name Constraint

Tipo Tag ASN.1 Esempio Descrizione
dNSName 2 example.com Dominio DNS
rfc822Name 1 @example.com Dominio e-mail
iPAddress 7 192.168.0.0/255.255.0.0 Range IP
directoryName 4 O=Example Prefisso DN
uniformResourceIdentifier 6 https://example.com Prefisso URI

Scenari tipici di constraint

Scenario Permitted Excluded
CA interna .internal.local (vuoto)
CA di reparto .dept.example.com .admin.dept.example.com
CA partner .partner.com .example.com
CA IoT 10.0.0.0/8 (vuoto)

Verifica Name Constraint con X509Chain

// .NET Framework esegue automaticamente la verifica dei Name Constraints
var chain = new X509Chain();
chain.ChainPolicy.ExtraStore.Add(intermediateCert);
 
bool isValid = chain.Build(endEntityCert);
 
// Riconoscere errori Name Constraint
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("Violazione Name Constraint:");
    foreach (var error in ncErrors)
    {
        Console.WriteLine($"  {error.StatusInformation}");
    }
}

Scenari correlati

Relazione Scenario Descrizione
Prerequisito 5.4 Validazione Policy Verificare policies
Correlato 1.2 Intermediate-CA CA con constraints
Correlato 1.3 Gerarchia CA Design constraints

« ← 5.4 Validazione Policy | ↑ Panoramica validazione | → Tutti gli scenari »


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