diff --git a/Melanzana.CodeSign/Blobs/CmsWrapperBlob.cs b/Melanzana.CodeSign/Blobs/CmsWrapperBlob.cs index c30793b..eda3acd 100644 --- a/Melanzana.CodeSign/Blobs/CmsWrapperBlob.cs +++ b/Melanzana.CodeSign/Blobs/CmsWrapperBlob.cs @@ -11,10 +11,67 @@ namespace Melanzana.CodeSign.Blobs { public class CmsWrapperBlob { + /// + /// The key usage extension defines the purpose (e.g., encipherment, + /// signature, certificate signing) of the key contained in the + /// certificate. + /// + /// + public const string KeyUsage = "2.5.29.15"; + + /// + /// The extended key usage field indicates one or more purposes for which the certified + /// public key may be used, in addition to or in place of the basic + /// purposes indicated in the key usage extension field. + /// + /// + public const string ExtendedKeyUsage = "2.5.29.37"; + + /// + /// An extended key usage value indicating the certificate may be used for + /// signing of downloadable executable code. + /// + /// + public const string CodeSigning = "1.3.6.1.5.5.7.3.3"; + + /// + /// The OID of a certificate extension which indicates the certificate is inteded for iPhone Software Submission Signing. + /// + /// + public const string SoftwareSubmissionSigning = "1.2.840.113635.100.6.1.4"; + + /// + /// The OID of a certificate extension which indicates the certificate is intended for iPhone Software Development Signing. + /// + /// + public const string SoftwareDevelopmentSigning = "1.2.840.113635.100.6.1.2"; + + /// + /// The basic constraints extension identifies whether the subject of the + /// certificate is a CA and the maximum depth of valid certification + /// paths that include this certificate. + /// + /// + public const string BasicConstraints = "2.5.29.19"; + + /// + /// The certificate policies extension contains a sequence of one or more + /// policy information terms. + /// + /// + public const string CertificatePolicies = "2.5.29.32"; + private readonly static string rootCertificatePath = "Melanzana.CodeSign.Certificates.RootCertificate.cer"; private readonly static string g1IntermediateCertificatePath = "Melanzana.CodeSign.Certificates.IntermediateG1Certificate.cer"; private readonly static string g3IntermediateCertificatePath = "Melanzana.CodeSign.Certificates.IntermediateG3Certificate.cer"; + private readonly static X509Certificate2 RootCertificate = GetManifestCertificate(rootCertificatePath); + private readonly static X509Certificate2[] IntermediateCertificates = new X509Certificate2[] + { + GetManifestCertificate(g1IntermediateCertificatePath), + GetManifestCertificate(g3IntermediateCertificatePath), + }; + private static X509Certificate2 GetManifestCertificate(string name) { var memoryStream = new MemoryStream(); @@ -54,24 +111,22 @@ public static byte[] Create( var certificatesList = new X509Certificate2Collection(); // Try to build full chain - var chain = new X509Chain(); - var chainPolicy = new X509ChainPolicy { TrustMode = X509ChainTrustMode.CustomRootTrust }; - chainPolicy.CustomTrustStore.Add(GetManifestCertificate(rootCertificatePath)); - chainPolicy.CustomTrustStore.Add(GetManifestCertificate(g1IntermediateCertificatePath)); - chainPolicy.CustomTrustStore.Add(GetManifestCertificate(g3IntermediateCertificatePath)); - chain.ChainPolicy = chainPolicy; - if (chain.Build(developerCertificate)) + + if (GetCustomRootTrustChain(developerCertificate, out var customCertificates)) + { + certificatesList.AddRange(customCertificates); + } + else if (GetDefaultChain(developerCertificate, out var defaultCertificates)) { - certificatesList.AddRange(chain.ChainElements.Select(e => e.Certificate).ToArray()); + certificatesList.AddRange(defaultCertificates); + } + else if(GetManualChain(developerCertificate, out var manualCertificates)) + { + certificatesList.AddRange(manualCertificates); } else { - // Retry with default policy and system certificate store - chain.ChainPolicy = new X509ChainPolicy(); - if (chain.Build(developerCertificate)) - { - certificatesList.AddRange(chain.ChainElements.Select(e => e.Certificate).ToArray()); - } + throw new Exception("Could not build the certificate chain for the developer certificate."); } var cmsSigner = privateKey == null ? @@ -117,5 +172,96 @@ public static byte[] Create( return blobBuffer; } + + private static bool GetCustomRootTrustChain(X509Certificate2 developerCertificate, out X509Certificate2[] certificates) + { + var chain = new X509Chain(); + var chainPolicy = new X509ChainPolicy { TrustMode = X509ChainTrustMode.CustomRootTrust }; + chainPolicy.CustomTrustStore.Add(RootCertificate); + chainPolicy.CustomTrustStore.AddRange(IntermediateCertificates); + chain.ChainPolicy = chainPolicy; + if (chain.Build(developerCertificate)) + { + certificates = chain.ChainElements.Select(e => e.Certificate).ToArray(); + return true; + } + else + { + certificates = Array.Empty(); + return false; + } + } + + private static bool GetDefaultChain(X509Certificate2 developerCertificate, out X509Certificate2[] certificates) + { + var chain = new X509Chain(); + if (chain.Build(developerCertificate)) + { + certificates = chain.ChainElements.Select(e => e.Certificate).ToArray(); + return true; + } + else + { + certificates = Array.Empty(); + return false; + } + } + + private static bool GetManualChain(X509Certificate2 developerCertificate, out X509Certificate2[] certificates) + { + certificates = Array.Empty(); + + // The certificate must be issued by any of the intermediate certificates + // TODO: we should also check on Authority Key Identifier + var intermediate = IntermediateCertificates.SingleOrDefault(c => developerCertificate.IssuerName.Name == c.SubjectName.Name); + if (intermediate == null) + { + return false; + } + + // The certificate must be a signing certificate + var keyUsageExtension = (X509KeyUsageExtension?)developerCertificate.Extensions[KeyUsage]; + if (keyUsageExtension == null || keyUsageExtension.KeyUsages != X509KeyUsageFlags.DigitalSignature) + { + return false; + } + + // The certificate must be a code signing certificate + var extendedKeyUsage = (X509EnhancedKeyUsageExtension?)developerCertificate.Extensions[ExtendedKeyUsage]; + if (extendedKeyUsage == null || extendedKeyUsage.EnhancedKeyUsages.Count != 1 || extendedKeyUsage.EnhancedKeyUsages[0].Value != CodeSigning) + { + return false; + } + + // The certificate must be either a software submission or a software development certificate + bool softwareSubmissionSigning = developerCertificate.Extensions[SoftwareSubmissionSigning] != null; + bool softwareDevelopmentSigning = developerCertificate.Extensions[SoftwareDevelopmentSigning] != null; + + if (!softwareSubmissionSigning && !softwareDevelopmentSigning) + { + return false; + } + + + // The certificate cannot be a self-signed certificate or a root CA + var basicConstraints = developerCertificate.Extensions[BasicConstraints] as X509BasicConstraintsExtension; + if (basicConstraints != null && basicConstraints.CertificateAuthority) + { + return false; + } + + // The certificate should adhere to the Apple certificate policy (but .NET does not contain classes for validating + // certificate policies) + var policiesExtension = developerCertificate.Extensions[CertificatePolicies]; + + certificates = new X509Certificate2[] + { + RootCertificate, + intermediate, + developerCertificate, + }; + + return true; + } } }