/*
* Microsoft Copyright, 2017
* Author: pengland
*/
namespace DICETest
{
using System;
using System.Linq;
using System.IO;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Nist;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.X509.Extension;
///
/// DICE Certificate and CSR validity checker. This is not an exhaustive validation library for X509 certificates, but
/// checks the mandatory fields created by DICE implementations, and smoke-tests the certificate chains that result.
///
class CertChecker
{
static readonly string SigAlgName = "SHA-256withECDSA";
private static string DICEExtensionOid = "2.23.133.5.4.1";
private static int DICEExtensionVersionNumber = 1;
private static int MinimumCertLifetimeInYears = 10;
private static int serialNumMinBytes = 8;
SubjectPublicKeyInfo PubKeyInfoFromDICEExtension=null;
string[] PEMCertFile;
///
/// First cert is always the Alias (leaf) cert
///
X509Certificate[] Certs;
int NumCerts;
internal CertChecker()
{
}
///
/// Specify the PEM file names for the certificates to be checked. The first cert should be the Alias Cert, and the
/// subsequent certs should be in order "up the chain" to a self-signed cert (vendor/root or DeviceID)
///
///
///
internal bool SetCerts(string[] pemFileNames)
{
PEMCertFile = pemFileNames;
NumCerts = PEMCertFile.Length;
Certs = new X509Certificate[NumCerts];
for(int j=0;j
/// This is the main certificate and certificate chain validation function.
///
/// Checks a DICE certificate chain. The chain must always contain the Alias Cert and the DeiceID cert
/// as the first two elements. Zero or any number of vendor intermediate certs may follow. THe last cert
/// must always be self signed (either the vendor root-CA cert, or the DeviceID cert).
///
/// All tests passed
internal bool CheckChain()
{
bool ok = true;
// check subject/issuer linkage (and print subjects)
ok &= CheckSubjectIssuerLinkage();
// check the the signatures good includign the self-signed root-cert
ok &= CheckSigningLinkage();
// check basic constraints for all certs
ok &= CheckCaAndPathLengthConstraint();
// Check the optional Authority Key Identifier linkage
ok &= CheckAuthKeyIdentifierLinkage();
// next check that the chain is well-formed using the .NET built-in validation routines
// (i.e. not the bouncy-castle validator.)
ok &= ChainChecker.CheckChain(Certs);
// Now check the cert fields are all good for all of the certificates in the chain
for (int j = 0; j < NumCerts; j++)
{
string tp = "";
switch(j)
{
case 0: tp = "Alias Cert"; break;
case 1: tp = "DeviceID Cert"; break;
default:
if (j == NumCerts - 1)
{
tp = "Vendor Root CA";
} else
{
tp = "Vendor Intermediate CA";
}
break;
}
Notify($"Checking {tp}: {Certs[j].SubjectDN.ToString()}");
bool certOk = CheckCertFields(Certs[j], j);
if(certOk)
{
NotifySuccess("OK");
}
else
{
Error("Certificate has errors");
}
ok &= certOk;
}
return ok;
}
///
/// Does basic validity checks for a CSR (is it properly self-signed)
///
///
/// CSR is properly signed
internal static bool CheckCSR(string csrPEM)
{
Pkcs10CertificationRequest csr = null;
try
{
using (var reader = File.OpenText(csrPEM))
{
csr = (Pkcs10CertificationRequest)new Org.BouncyCastle.OpenSsl.PemReader(reader).ReadObject();
}
}
catch (Exception e)
{
Program.Print($"Failed to parse the csr: {csrPEM}", NotifyType.Error);
Program.Print($"Error is : {e.ToString()}", NotifyType.Error);
return false;
}
bool sigOk = csr.Verify();
if(!sigOk)
{
Error("CSR signature did not verify");
return false;
}
return true;
}
///
/// A PoP cert is a "fake" DeviceID cert containing a challenge subject common name. To check this there must
/// be two certs in the chain: the PoP cert and the vendor root cert.
///
///
/// PoP cert is properly signed, and the subject common name is correct.
internal bool CheckPopCert(string cn)
{
X509Name name=null;
try
{
name = new X509Name(cn);
}
catch(Exception e)
{
Error($"Name {cn} is not a valid X509 Name (e.g. CN=XXXQQQ) {e.ToString()}");
return false;
}
Notify("Checking PoP Cert");
bool ok = CheckSigningLinkage();
if(!ok)
{
return false;
}
var certSubject = Certs[0].SubjectDN;
if(certSubject.ToString()!= name.ToString())
{
Error($"Cert subject is incorrect. Should be {name.ToString()} but is {certSubject.ToString()}");
return false;
}
return true;
}
///
/// Checks that the issuer of all certs in a chain matches the subject of the parent.
///
/// The subject/issuer correspondence is correct
internal bool CheckSubjectIssuerLinkage()
{
bool ok = true;
Notify($"Checking Subject/Issuer Linkage");
foreach (var c in Certs)
{
Notify($" {c.SubjectDN.ToString()}");
}
if(Certs[NumCerts-1].SubjectDN.ToString() != Certs[NumCerts - 1].IssuerDN.ToString())
{
Error($"Root cert subject and issuer are different Subject:{Certs[NumCerts - 1].SubjectDN.ToString()} Issuer:{Certs[NumCerts - 1].IssuerDN.ToString()}");
ok = false;
}
// alias to root
for(int j=0;j
/// Checks that the root cert is self signed, and that all other certs are propely signed by the issuer.
///
/// Chain signing linkage is OK
internal bool CheckSigningLinkage()
{
Notify($"Checking signature chains");
bool ok = true;
// alias to cert-before-root should be signed by parent
for (int j = 0; j < NumCerts - 1; j++)
{
var target = Certs[j];
var signer = Certs[j + 1];
var signerPubKey = signer.GetPublicKey();
try
{
target.Verify(signerPubKey);
}
catch(Exception e)
{
Error($" Cert {target.SubjectDN.ToString()} is not properly signed by {signer.SubjectDN.ToString()}. Error is {e.ToString()}");
ok = false;
}
}
// The root should be self-signed
var root = Certs[NumCerts - 1];
var rootPubKey = root.GetPublicKey();
try
{
root.Verify(rootPubKey);
}
catch (Exception)
{
Error($"Root cert is not properly self-signed.");
ok = false;
}
if(ok)
{
NotifySuccess("OK");
}
return ok;
}
///
/// Authority Key Identifier is an optional field. If present, it should be the key identifier of the parent.
/// Absence of the field is a warning. If present, bad linkage is an error.
///
///
internal bool CheckAuthKeyIdentifierLinkage()
{
bool ok = true;
// alias to root
for (int j = 0; j < NumCerts - 1; j++)
{
var signer = Certs[j + 1];
var target = Certs[j];
var akiData = target.GetExtensionValue(X509Extensions.AuthorityKeyIdentifier);
if(akiData ==null)
{
Warning($"Certificate does not contain an Authority Key Identifier Extension: {target.SubjectDN.ToString()}");
continue;
}
if (akiData != null)
{
var aki = new AuthorityKeyIdentifierStructure(akiData);
var signerKeyId = new AuthorityKeyIdentifier(SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(signer.GetPublicKey()));
if(!signerKeyId.Equals(aki))
{
Error($"Authority Key Identifier does not match signer for certificate with subject: {target.SubjectDN.ToString()}");
ok = false;
}
}
}
return ok;
}
///
/// Check that the CA fields and path length constraint allows a valid chain to be built
///
/// Basic constraints are OK
internal bool CheckCaAndPathLengthConstraint()
{
bool ok = true;
// alias to root
for (int j = 0; j < NumCerts - 1; j++)
{
var c = Certs[j];
int basicConstraint = c.GetBasicConstraints();
if(j==0)
{
if(basicConstraint!=-1)
{
Error($"Alias Cert has basic constraint");
ok = false;
}
}
else
{
if (basicConstraint < j-1)
{
Error($"Root or intermediate cert BasicConstraint is incorrect for {c.SubjectDN.ToString()}. Require >{j-1}, but it is {basicConstraint}");
ok = false;
}
}
}
return ok;
}
///
/// Checks the fields in the certificate are OK
///
/// The certifcate to be checked
/// How far up the chain the cert is (alias=zero)
/// The cert fields are good
bool CheckCertFields(X509Certificate c, int offsetFromAlias)
{
bool ok = true;
// processing depends on the type of cert
bool isAlias = offsetFromAlias == 0;
bool isCa = offsetFromAlias != 0;
bool isAliasOrDevId = offsetFromAlias <= 1;
ok&=CheckAlgs(c);
ok&=CheckKeyUsage(c, isCa);
ok&=CheckExtendedKeyUsage(c, isCa) ;
ok&=CheckValidityDates(c);
if (isAlias)
{
ok&= CheckDICEExtension(c) ;
}
if (isAliasOrDevId)
{
ok&= CheckSerialNumber(c);
}
return ok;
}
///
/// Checks the signature algorithm (only P256SHA256, at this time)
///
/// Certificate to be checked
/// Algorithm set is OK
bool CheckAlgs(X509Certificate c)
{
if (!CheckExpected("Signature algorithm", SigAlgName, c.SigAlgName)) return false;
return true;
}
///
/// Parses and checks contents of the DICE extension
///
/// Certificate to validate
/// Extension is well formed
bool CheckDICEExtension(X509Certificate c)
{
var criticalOids = c.GetCriticalExtensionOids();
if (criticalOids.Contains(DICEExtensionOid))
{
Error("DICE extension is marked critical and should be non-critical");
return false;
}
var nonCriticalOids = c.GetNonCriticalExtensionOids();
if(!nonCriticalOids.Contains(DICEExtensionOid))
{
Error("DICE extension not found");
return false;
}
var diceExtension = c.GetExtensionValue(new DerObjectIdentifier(DICEExtensionOid));
try
{
DerOctetString envelope = (DerOctetString) DerOctetString.FromByteArray(diceExtension.GetEncoded());
DerSequence seq = (DerSequence) DerSequence.FromByteArray(envelope.GetOctets());
// first field is version number
var versionNumber = (DerInteger)seq[0];
if (versionNumber.PositiveValue.IntValue != 1)
{
Error($"DICE Extension has Wrong version number. Expecing {DICEExtensionVersionNumber}, cert contains {versionNumber.ToString()}");
return false;
}
// second field is DeviceID
var devIdPubKey = SubjectPublicKeyInfo.GetInstance(seq[1]);
// will check it's good later
PubKeyInfoFromDICEExtension = devIdPubKey;
// third field contains {hashOid, hashVal}
var hashEnvelope = (DerSequence)seq[2];
var hashAlg = (DerObjectIdentifier)hashEnvelope[0];
if (hashAlg.Id != NistObjectIdentifiers.IdSha256.ToString())
{
Error("DICE Extension hash alg is wrong. ");
return false;
}
var hashVal = (DerOctetString)hashEnvelope[1];
if (hashVal.GetOctets().Length != 32)
{
Error("DICE Extension hash value length is wrong. ");
return false;
}
}
catch (Exception e)
{
Error($"Failed to parse the DICE extension. Parsing exception was {e.ToString()}");
return false ;
}
return true;
}
///
/// Sanity checks the Key Usage certificate extension
///
/// Certificate to validate
/// Whether the cert is a CA cert (intermediate or root)
/// Key Usage is OK
bool CheckKeyUsage(X509Certificate c, bool isCA)
{
int requiredFlags = (isCA? KeyUsage.KeyCertSign : 0);
// Currently none of the reference implemtations use these fields.
int badFlags = KeyUsage.DataEncipherment | KeyUsage.CrlSign | KeyUsage.DecipherOnly | KeyUsage.EncipherOnly |
KeyUsage.KeyAgreement | KeyUsage.KeyEncipherment | KeyUsage.NonRepudiation;
bool flagsOk = true;
try
{
var keyUsage= c.GetKeyUsage();
if(keyUsage==null)
{
Error($"KeyUsage is missing.");
return false;
}
if (keyUsage.Length>9)
{
Error($"Unsupported KeyUsage. This usually means that DecipherOnly is asserted, which is an error");
return false;
}
for (int j = 0; j < 9; j++)
{
if (j >= keyUsage.Length) break;
int flag = 1 << (7-j);
if ((requiredFlags & flag) != 0)
{
if (!keyUsage[j])
{
Error($"Required key usage NOT asserted: {KeyUsageFlagToString(flag)}");
flagsOk = false;
}
}
if ((badFlags & flag) != 0)
{
if (keyUsage[j])
{
Error($"Bad key usage asserted: {KeyUsageFlagToString(flag)}");
flagsOk = false;
}
}
}
}
catch (Exception e)
{
Error("Failed to parse the flags extension:" + e.ToString());
flagsOk = false;
}
return flagsOk;
}
///
/// Checks the extended key usage for CA (including DeviceID) and alias certs
///
/// The certififcate to check
/// Whether the cert is a root or intermediate CA cert
/// Extended usage is OK
bool CheckExtendedKeyUsage(X509Certificate c, bool isCa)
{
var extendedKeyUsage = c.GetExtendedKeyUsage();
var setUsage = "";
if (isCa)
{
if (extendedKeyUsage!= null)
{
foreach (var s in extendedKeyUsage) setUsage += s.ToString() + " ";
Error("One of more ExtendedKeyUsage asserted on non-alias certificate:" + setUsage);
return false;
}
return true;
}
// else is the alias
foreach (var s in extendedKeyUsage) setUsage += s.ToString() + " ";
{
if (extendedKeyUsage.Count != 1)
{
Error("Too many ExtendedKeyUsage asserted for non-alias certificate:" + setUsage);
return false;
}
if (extendedKeyUsage[0].ToString() != KeyPurposeID.IdKPClientAuth.ToString())
{
Error("Extended Key Usage ClientAuth not asserted");
return false;
}
}
return true;
}
///
/// Basic validity period checks
///
/// Certificate to validate
/// Validity period is OK
bool CheckValidityDates(X509Certificate c)
{
TimeSpan MinimumCertLifetime = new TimeSpan(MinimumCertLifetimeInYears * 365, 0, 0, 0, 0);
if(!c.IsValidNow)
{
Warning("Certificate is not valid now");
return true;
}
TimeSpan certLifetime = c.NotAfter - DateTime.Now;
if (certLifetime
/// Device and Alias cert serial numbers must be "long enough" integers
///
///
///
bool CheckSerialNumber(X509Certificate c)
{
BigInteger sn = c.SerialNumber;
var snBytes = sn.ToByteArray();
int snNumBytes = snBytes.Length;
if(snNumBytes x == 0);
if (numZeroBytes>=3)
{
Warning($"Serial number has {numZeroBytes} zeros. Is it random?");
return false;
}
return true;
}
///
/// Helper checker and notification function.
///
/// Error message if expected!=value
///
/// Actual data
/// Expected==actual
bool CheckExpected(string category, string expected, string actual)
{
if (expected == actual) return true;
Error($"Bad field: {category}. Expected={expected}, Actual={actual}");
return false;
}
///
/// ASN.1 Key Usage value-to-string translator
///
///
///
static string KeyUsageFlagToString(int flag)
{
switch (flag)
{
case (1 << 7): return "DigitalSignature";
case (1 << 6): return "NonRepudiation";
case (1 << 5): return "KeyEncipherment";
case (1 << 4): return "DataEncipherment";
case (1 << 3): return "KeyAgreement";
case (1 << 2): return "KeyCertSign";
case (1 << 1): return "CrlSign";
case (1 << 0): return "EncipherOnly";
case (1 << 15): return "DecipherOnly";
default: return $"Undefined flag at bit position {flag}";
}
}
///
/// Print/Log an error
///
/// String to print
static void Error(string error)
{
Program.Print(error, NotifyType.Error);
return;
}
///
/// Print/log a warning
///
/// Warning to print
static void Warning(string warning)
{
Program.Print(warning, NotifyType.Warning);
return;
}
///
/// Print/log a notification message
///
/// Message to print
static void Notify(string message)
{
Program.Print(message, NotifyType.Notify);
return;
}
///
/// Print/log a success notification
///
/// Message to print
static void NotifySuccess(string message)
{
Program.Print(message, NotifyType.Success);
return;
}
}
}