NuGet / Insights

Gather insights about public NuGet.org package data
Apache License 2.0
26 stars 7 forks source link

Add flag indicating whether or not a certificate is an extended validation (EV) certificate #89

Closed dtivel closed 1 year ago

dtivel commented 1 year ago

See https://en.wikipedia.org/wiki/Extended_Validation_Certificate.

I don't think it's important to know if a certificate is domain validation (DV) or organization validation (OV).

joelverhagen commented 1 year ago

@dtivel, do you have positive and a negative (with and without) .cer files that I can see to understand this?

This is the part of the code that maps the X502Certificate2 to a POCO, which is later mapped to CSV and Kusto: https://github.com/NuGet/Insights/blob/2e8f2926b669daf819328394a8cbfe48409b992a/src/Worker.Logic/CatalogScan/Drivers/PackageCertificateToCsv/CertificateRecord.cs#L35

I wonder if it's as simple as updating the code path there to check some more parts of the X502Certificate2 instance.

dtivel commented 1 year ago
// This is an Extended Validation (EV) code signing certificate.
// EV code signing certificates are identified by the presence of a well-known certificate policy:
//
//      extended-validation-codesigning (2.23.140.1.3)
//
// This policy is defined by the CA/Browser Forum at https://cabforum.org/object-registry/.
NiCertificates
| where FingerprintSHA256Hex == 'FB32E016FD317DB68C0B2B5B6E33231EE932B4B21E27F32B51654A483A10ADFB'

// This is not an EV code signing certificate.
NiCertificates
| where FingerprintSHA256Hex == '5A2901D6ADA3D18260B9C6DFE2133C95D74B9EEF6AE0E5DC334C8454D1477DF4'

Here's some sample C# code. (No promises on it compiling.)

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0.

private static bool IsExtendedValidationCodeSigningCertificate(X509Certificate2 certificate)
{
    var isEvCodeSigning = false;
    X509Extension? certificatePolicy = certificate.Extensions[Oids.CertificatePolicies.Value!];

    if (certificatePolicy is not null)
    {
        AsnReader reader = new(certificatePolicy.RawData, AsnEncodingRules.DER);

        IReadOnlyList<PolicyInformation> policyInformations = ReadPolicies(reader.ReadSequence());

        isEvCodeSigning = policyInformations.Any(
            policy => policy.PolicyIdentifier.Value == Oids.ExtendedValidationCodeSigning.Value);
    }

    return isEvCodeSigning;
}

private static IReadOnlyList<PolicyInformation> ReadPolicies(AsnReader reader)
{
    List<PolicyInformation> policies = new();

    while (reader.HasData)
    {
        PolicyInformation policy = PolicyInformation.Read(reader);

        policies.Add(policy);
    }

    return policies;
}

/*
    From RFC 5280 (https://tools.ietf.org/html/rfc5280#appendix-A.2):

        PolicyInformation ::= SEQUENCE {
            policyIdentifier   CertPolicyId,
            policyQualifiers   SEQUENCE SIZE (1..MAX) OF
                                    PolicyQualifierInfo OPTIONAL }

        CertPolicyId ::= OBJECT IDENTIFIER
*/
internal sealed class PolicyInformation
{
    internal Oid PolicyIdentifier { get; }
    internal IReadOnlyList<PolicyQualifierInfo> PolicyQualifiers { get; }

    private PolicyInformation(Oid policyIdentifier, IReadOnlyList<PolicyQualifierInfo> policyQualifiers)
    {
        PolicyIdentifier = policyIdentifier;
        PolicyQualifiers = policyQualifiers;
    }

    internal static PolicyInformation Read(AsnReader reader)
    {
        AsnReader policyInfoReader = reader.ReadSequence();
        string policyIdentifier = policyInfoReader.ReadObjectIdentifier();
        bool isAnyPolicy = policyIdentifier == Oids.AnyPolicy.Value;
        IReadOnlyList<PolicyQualifierInfo>? policyQualifiers = null;

        if (policyInfoReader.HasData)
        {
            policyQualifiers = ReadPolicyQualifiers(policyInfoReader, isAnyPolicy);
        }

        return new PolicyInformation(new Oid(policyIdentifier), policyQualifiers ?? Array.Empty<PolicyQualifierInfo>());
    }

    private static IReadOnlyList<PolicyQualifierInfo> ReadPolicyQualifiers(
        AsnReader reader,
        bool isAnyPolicy)
    {
        AsnReader policyQualifiersReader = reader.ReadSequence();
        List<PolicyQualifierInfo> policyQualifiers = new();

        while (policyQualifiersReader.HasData)
        {
            PolicyQualifierInfo policyQualifier = PolicyQualifierInfo.Read(policyQualifiersReader);

            if (isAnyPolicy)
            {
                if (policyQualifier.PolicyQualifierId.Value != Oids.IdQtCps.Value &&
                    policyQualifier.PolicyQualifierId.Value != Oids.IdQtUnotice.Value)
                {
                    throw new Exception("InvalidAsn1");
                }
            }

            policyQualifiers.Add(policyQualifier);
        }

        if (policyQualifiers.Count == 0)
        {
            throw new Exception("InvalidAsn1");
        }

        return policyQualifiers;
    }
}

/*
    From RFC 5280 (https://tools.ietf.org/html/rfc5280#appendix-A.2):

        PolicyQualifierInfo ::= SEQUENCE {
            policyQualifierId  PolicyQualifierId,
            qualifier          ANY DEFINED BY policyQualifierId }

        -- policyQualifierIds for Internet policy qualifiers

        id-qt          OBJECT IDENTIFIER ::=  { id-pkix 2 }
        id-qt-cps      OBJECT IDENTIFIER ::=  { id-qt 1 }
        id-qt-unotice  OBJECT IDENTIFIER ::=  { id-qt 2 }

        PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
*/
internal sealed class PolicyQualifierInfo
{
    internal Oid PolicyQualifierId { get; }
    internal ReadOnlyMemory<byte> Qualifier { get; }

    private PolicyQualifierInfo(Oid policyQualifierId, ReadOnlyMemory<byte> qualifier)
    {
        PolicyQualifierId = policyQualifierId;
        Qualifier = qualifier;
    }

    internal static PolicyQualifierInfo Read(AsnReader reader)
    {
        AsnReader policyQualifierReader = reader.ReadSequence();
        string policyQualifierId = policyQualifierReader.ReadObjectIdentifier();
        ReadOnlyMemory<byte> qualifier = null;

        if (policyQualifierReader.HasData)
        {
            qualifier = policyQualifierReader.ReadEncodedValue();

            policyQualifierReader.ThrowIfNotEmpty();
        }

        return new PolicyQualifierInfo(new Oid(policyQualifierId), qualifier);
    }
}

internal static class Oids
{
    internal static readonly Oid AnyPolicy = new(DottedDecimals.AnyPolicy);
    internal static readonly Oid CertificatePolicies = new(DottedDecimals.CertificatePolicies);
    internal static readonly Oid ExtendedValidationCodeSigning = new(DottedDecimals.ExtendedValidationCodeSigning);
    internal static readonly Oid IdQtCps = new(DottedDecimals.IdQtCps);
    internal static readonly Oid IdQtUnotice = new(DottedDecimals.IdQtUnotice);

    private class DottedDecimals
    {
        // RFC 5280 "anyPolicy" https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.4
        internal const string AnyPolicy = "2.5.29.32.0";

        // RFC 5280 "id-ce-certificatePolicies" https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.4
        internal const string CertificatePolicies = "2.5.29.32";

        // CA/B Forum "extended-validation-codesigning" https://cabforum.org/object-registry/
        internal const string ExtendedValidationCodeSigning = "2.23.140.1.3";

        // RFC 5280 "id-qt-cps" https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.4
        internal const string IdQtCps = "1.3.6.1.5.5.7.2.1";

        // RFC 5280 "id-qt-unotice" https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.4
        internal const string IdQtUnotice = "1.3.6.1.5.5.7.2.2";
    }
}
joelverhagen commented 1 year ago

Done with f869511119c335eb7f02d4265e24205e91f3b589