64ac736ec5
Former-commit-id: f3cc9b82f3e5bd8f0fd3ebc098f789556b44e9cd
962 lines
32 KiB
C#
962 lines
32 KiB
C#
//
|
|
// System.Security.Cryptography.X509Certificates.X509ChainImplMono
|
|
//
|
|
// Author:
|
|
// Sebastien Pouliot <sebastien@ximian.com>
|
|
//
|
|
// (C) 2003 Motus Technologies Inc. (http://www.motus.com)
|
|
// Copyright (C) 2004-2006 Novell Inc. (http://www.novell.com)
|
|
// Copyright (C) 2011 Xamarin Inc. (http://www.xamarin.com)
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining
|
|
// a copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
|
// permit persons to whom the Software is furnished to do so, subject to
|
|
// the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be
|
|
// included in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
//
|
|
|
|
#if SECURITY_DEP
|
|
|
|
#if MONO_SECURITY_ALIAS
|
|
extern alias MonoSecurity;
|
|
using MX = MonoSecurity::Mono.Security.X509;
|
|
#else
|
|
using MX = Mono.Security.X509;
|
|
#endif
|
|
|
|
using System.Collections;
|
|
using System.Text;
|
|
|
|
namespace System.Security.Cryptography.X509Certificates {
|
|
|
|
internal class X509ChainImplMono : X509ChainImpl
|
|
{
|
|
private StoreLocation location;
|
|
private X509ChainElementCollection elements;
|
|
private X509ChainPolicy policy;
|
|
private X509ChainStatus[] status;
|
|
|
|
static X509ChainStatus[] Empty = new X509ChainStatus [0];
|
|
|
|
// RFC3280 variables
|
|
private int max_path_length;
|
|
private X500DistinguishedName working_issuer_name;
|
|
// private string working_public_key_algorithm;
|
|
private AsymmetricAlgorithm working_public_key;
|
|
|
|
// other flags
|
|
private X509ChainElement bce_restriction;
|
|
|
|
// constructors
|
|
|
|
public X509ChainImplMono ()
|
|
: this (false)
|
|
{
|
|
}
|
|
|
|
public X509ChainImplMono (bool useMachineContext)
|
|
{
|
|
location = useMachineContext ? StoreLocation.LocalMachine : StoreLocation.CurrentUser;
|
|
elements = new X509ChainElementCollection ();
|
|
policy = new X509ChainPolicy ();
|
|
}
|
|
|
|
[MonoTODO ("Mono's X509Chain is fully managed. All handles are invalid.")]
|
|
public X509ChainImplMono (IntPtr chainContext)
|
|
{
|
|
// CryptoAPI compatibility (unmanaged handle)
|
|
throw new NotSupportedException ();
|
|
}
|
|
|
|
public override bool IsValid {
|
|
get { return true; }
|
|
}
|
|
|
|
public override IntPtr Handle {
|
|
get { return IntPtr.Zero; }
|
|
}
|
|
|
|
// properties
|
|
|
|
public override X509ChainElementCollection ChainElements {
|
|
get { return elements; }
|
|
}
|
|
|
|
public override X509ChainPolicy ChainPolicy {
|
|
get { return policy; }
|
|
set { policy = value; }
|
|
}
|
|
|
|
public override X509ChainStatus[] ChainStatus {
|
|
get {
|
|
if (status == null)
|
|
return Empty;
|
|
return status;
|
|
}
|
|
}
|
|
|
|
// methods
|
|
|
|
public override void AddStatus (X509ChainStatusFlags error)
|
|
{
|
|
}
|
|
|
|
[MonoTODO ("Not totally RFC3280 compliant, but neither is MS implementation...")]
|
|
public override bool Build (X509Certificate2 certificate)
|
|
{
|
|
if (certificate == null)
|
|
throw new ArgumentException ("certificate");
|
|
|
|
Reset ();
|
|
X509ChainStatusFlags flag;
|
|
try {
|
|
flag = BuildChainFrom (certificate);
|
|
ValidateChain (flag);
|
|
}
|
|
catch (CryptographicException ce) {
|
|
throw new ArgumentException ("certificate", ce);
|
|
}
|
|
|
|
X509ChainStatusFlags total = X509ChainStatusFlags.NoError;
|
|
ArrayList list = new ArrayList ();
|
|
// build "global" ChainStatus from the ChainStatus of every ChainElements
|
|
foreach (X509ChainElement ce in elements) {
|
|
foreach (X509ChainStatus cs in ce.ChainElementStatus) {
|
|
// we MUST avoid duplicates in the "global" list
|
|
if ((total & cs.Status) != cs.Status) {
|
|
list.Add (cs);
|
|
total |= cs.Status;
|
|
}
|
|
}
|
|
}
|
|
// and if required add some
|
|
if (flag != X509ChainStatusFlags.NoError) {
|
|
list.Insert (0, new X509ChainStatus (flag));
|
|
}
|
|
status = (X509ChainStatus[]) list.ToArray (typeof (X509ChainStatus));
|
|
|
|
// (fast path) this ignore everything we have checked
|
|
if ((status.Length == 0) || (ChainPolicy.VerificationFlags == X509VerificationFlags.AllFlags))
|
|
return true;
|
|
|
|
bool result = true;
|
|
// now check if exclude some verification for the "end result" (boolean)
|
|
foreach (X509ChainStatus cs in status) {
|
|
switch (cs.Status) {
|
|
case X509ChainStatusFlags.UntrustedRoot:
|
|
case X509ChainStatusFlags.PartialChain:
|
|
result &= ((ChainPolicy.VerificationFlags & X509VerificationFlags.AllowUnknownCertificateAuthority) != 0);
|
|
break;
|
|
case X509ChainStatusFlags.NotTimeValid:
|
|
result &= ((ChainPolicy.VerificationFlags & X509VerificationFlags.IgnoreNotTimeValid) != 0);
|
|
break;
|
|
// FIXME - from here we needs new test cases for all cases
|
|
case X509ChainStatusFlags.NotTimeNested:
|
|
result &= ((ChainPolicy.VerificationFlags & X509VerificationFlags.IgnoreNotTimeNested) != 0);
|
|
break;
|
|
case X509ChainStatusFlags.InvalidBasicConstraints:
|
|
result &= ((ChainPolicy.VerificationFlags & X509VerificationFlags.IgnoreInvalidBasicConstraints) != 0);
|
|
break;
|
|
case X509ChainStatusFlags.InvalidPolicyConstraints:
|
|
case X509ChainStatusFlags.NoIssuanceChainPolicy:
|
|
result &= ((ChainPolicy.VerificationFlags & X509VerificationFlags.IgnoreInvalidPolicy) != 0);
|
|
break;
|
|
case X509ChainStatusFlags.InvalidNameConstraints:
|
|
case X509ChainStatusFlags.HasNotSupportedNameConstraint:
|
|
case X509ChainStatusFlags.HasNotPermittedNameConstraint:
|
|
case X509ChainStatusFlags.HasExcludedNameConstraint:
|
|
result &= ((ChainPolicy.VerificationFlags & X509VerificationFlags.IgnoreInvalidName) != 0);
|
|
break;
|
|
case X509ChainStatusFlags.InvalidExtension:
|
|
// not sure ?!?
|
|
result &= ((ChainPolicy.VerificationFlags & X509VerificationFlags.IgnoreWrongUsage) != 0);
|
|
break;
|
|
//
|
|
// ((ChainPolicy.VerificationFlags & X509VerificationFlags.IgnoreRootRevocationUnknown) != 0)
|
|
// ((ChainPolicy.VerificationFlags & X509VerificationFlags.IgnoreEndRevocationUnknown) != 0)
|
|
case X509ChainStatusFlags.CtlNotTimeValid:
|
|
result &= ((ChainPolicy.VerificationFlags & X509VerificationFlags.IgnoreCtlNotTimeValid) != 0);
|
|
break;
|
|
case X509ChainStatusFlags.CtlNotSignatureValid:
|
|
// ?
|
|
break;
|
|
// ((ChainPolicy.VerificationFlags & X509VerificationFlags.IgnoreCtlSignerRevocationUnknown) != 0);
|
|
case X509ChainStatusFlags.CtlNotValidForUsage:
|
|
// FIXME - does IgnoreWrongUsage apply to CTL (it doesn't have Ctl in it's name like the others)
|
|
result &= ((ChainPolicy.VerificationFlags & X509VerificationFlags.IgnoreWrongUsage) != 0);
|
|
break;
|
|
default:
|
|
result = false;
|
|
break;
|
|
}
|
|
// once we have one failure there's no need to check further
|
|
if (!result)
|
|
return false;
|
|
}
|
|
|
|
// every "problem" was excluded
|
|
return true;
|
|
}
|
|
|
|
public override void Reset ()
|
|
{
|
|
// note: this call doesn't Reset the X509ChainPolicy
|
|
if ((status != null) && (status.Length != 0))
|
|
status = null;
|
|
if (elements.Count > 0)
|
|
elements.Clear ();
|
|
if (user_root_store != null) {
|
|
user_root_store.Close ();
|
|
user_root_store = null;
|
|
}
|
|
if (root_store != null) {
|
|
root_store.Close ();
|
|
root_store = null;
|
|
}
|
|
if (user_ca_store != null) {
|
|
user_ca_store.Close ();
|
|
user_ca_store = null;
|
|
}
|
|
if (ca_store != null) {
|
|
ca_store.Close ();
|
|
ca_store = null;
|
|
}
|
|
roots = null;
|
|
cas = null;
|
|
collection = null;
|
|
bce_restriction = null;
|
|
working_public_key = null;
|
|
}
|
|
|
|
// private stuff
|
|
|
|
private X509Certificate2Collection roots;
|
|
private X509Certificate2Collection cas;
|
|
private X509Store root_store;
|
|
private X509Store ca_store;
|
|
private X509Store user_root_store;
|
|
private X509Store user_ca_store;
|
|
|
|
private X509Certificate2Collection Roots {
|
|
get {
|
|
if (roots == null) {
|
|
X509Certificate2Collection c = new X509Certificate2Collection ();
|
|
X509Store store = LMRootStore;
|
|
if (location == StoreLocation.CurrentUser)
|
|
c.AddRange (UserRootStore.Certificates);
|
|
c.AddRange (store.Certificates);
|
|
roots = c;
|
|
}
|
|
return roots;
|
|
}
|
|
}
|
|
|
|
private X509Certificate2Collection CertificateAuthorities {
|
|
get {
|
|
if (cas == null) {
|
|
X509Certificate2Collection c = new X509Certificate2Collection ();
|
|
X509Store store = LMCAStore;
|
|
if (location == StoreLocation.CurrentUser)
|
|
c.AddRange (UserCAStore.Certificates);
|
|
c.AddRange (store.Certificates);
|
|
cas = c;
|
|
}
|
|
return cas;
|
|
}
|
|
}
|
|
|
|
private X509Store LMRootStore {
|
|
get {
|
|
if (root_store == null) {
|
|
root_store = new X509Store (StoreName.Root, StoreLocation.LocalMachine);
|
|
try {
|
|
root_store.Open (OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
|
|
} catch {
|
|
}
|
|
}
|
|
return root_store;
|
|
}
|
|
}
|
|
|
|
private X509Store UserRootStore {
|
|
get {
|
|
if (user_root_store == null) {
|
|
user_root_store = new X509Store (StoreName.Root, StoreLocation.CurrentUser);
|
|
try {
|
|
user_root_store.Open (OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
|
|
} catch {
|
|
}
|
|
}
|
|
return user_root_store;
|
|
}
|
|
}
|
|
|
|
private X509Store LMCAStore {
|
|
get {
|
|
if (ca_store == null) {
|
|
ca_store = new X509Store (StoreName.CertificateAuthority, StoreLocation.LocalMachine);
|
|
try {
|
|
ca_store.Open (OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
|
|
} catch {
|
|
}
|
|
}
|
|
return ca_store;
|
|
}
|
|
}
|
|
|
|
private X509Store UserCAStore {
|
|
get {
|
|
if (user_ca_store == null) {
|
|
user_ca_store = new X509Store (StoreName.CertificateAuthority, StoreLocation.CurrentUser);
|
|
try {
|
|
user_ca_store.Open (OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
|
|
} catch {
|
|
}
|
|
}
|
|
return user_ca_store;
|
|
}
|
|
}
|
|
// *** certificate chain/path building stuff ***
|
|
|
|
private X509Certificate2Collection collection;
|
|
|
|
// we search local user (default) or machine certificate store
|
|
// and in the extra certificate supplied in ChainPolicy.ExtraStore
|
|
private X509Certificate2Collection CertificateCollection {
|
|
get {
|
|
if (collection == null) {
|
|
collection = new X509Certificate2Collection (ChainPolicy.ExtraStore);
|
|
collection.AddRange (Roots);
|
|
collection.AddRange (CertificateAuthorities);
|
|
}
|
|
return collection;
|
|
}
|
|
}
|
|
|
|
// This is a non-recursive chain/path building algorithm.
|
|
//
|
|
// At this stage we only checks for PartialChain, Cyclic and UntrustedRoot errors are they
|
|
// affect the path building (other errors are verification errors).
|
|
//
|
|
// Note that the order match the one we need to match MS and not the one defined in RFC3280,
|
|
// we also include the trusted root certificate (trust anchor in RFC3280) in the list.
|
|
// (this isn't an issue, just keep that in mind if you look at the source and the RFC)
|
|
private X509ChainStatusFlags BuildChainFrom (X509Certificate2 certificate)
|
|
{
|
|
elements.Add (certificate);
|
|
|
|
while (!IsChainComplete (certificate)) {
|
|
certificate = FindParent (certificate);
|
|
|
|
if (certificate == null)
|
|
return X509ChainStatusFlags.PartialChain;
|
|
|
|
if (elements.Contains (certificate))
|
|
return X509ChainStatusFlags.Cyclic;
|
|
|
|
elements.Add (certificate);
|
|
}
|
|
|
|
// roots may be supplied (e.g. in the ExtraStore) so we need to confirm their
|
|
// trustiness (what a cute word) in the trusted root collection
|
|
if (!Roots.Contains (certificate))
|
|
elements [elements.Count - 1].StatusFlags |= X509ChainStatusFlags.UntrustedRoot;
|
|
|
|
return X509ChainStatusFlags.NoError;
|
|
}
|
|
|
|
|
|
private X509Certificate2 SelectBestFromCollection (X509Certificate2 child, X509Certificate2Collection c)
|
|
{
|
|
switch (c.Count) {
|
|
case 0:
|
|
return null;
|
|
case 1:
|
|
return c [0];
|
|
default:
|
|
// multiple candidate, keep only the ones that are still valid
|
|
X509Certificate2Collection time_valid = c.Find (X509FindType.FindByTimeValid, ChainPolicy.VerificationTime, false);
|
|
switch (time_valid.Count) {
|
|
case 0:
|
|
// that's too restrictive, let's revert and try another thing...
|
|
time_valid = c;
|
|
break;
|
|
case 1:
|
|
return time_valid [0];
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// again multiple candidates, let's find the AKI that match the SKI (if we have one)
|
|
string aki = GetAuthorityKeyIdentifier (child);
|
|
if (String.IsNullOrEmpty (aki)) {
|
|
return time_valid [0]; // FIXME: out of luck, you get the first one
|
|
}
|
|
foreach (X509Certificate2 parent in time_valid) {
|
|
string ski = GetSubjectKeyIdentifier (parent);
|
|
// if both id are available then they must match
|
|
if (aki == ski)
|
|
return parent;
|
|
}
|
|
return time_valid [0]; // FIXME: out of luck, you get the first one
|
|
}
|
|
}
|
|
|
|
private X509Certificate2 FindParent (X509Certificate2 certificate)
|
|
{
|
|
X509Certificate2Collection subset = CertificateCollection.Find (X509FindType.FindBySubjectDistinguishedName, certificate.Issuer, false);
|
|
string aki = GetAuthorityKeyIdentifier (certificate);
|
|
if ((aki != null) && (aki.Length > 0)) {
|
|
subset.AddRange (CertificateCollection.Find (X509FindType.FindBySubjectKeyIdentifier, aki, false));
|
|
}
|
|
X509Certificate2 parent = SelectBestFromCollection (certificate, subset);
|
|
// if parent==certificate we're looping but it's not (probably) a bug and not a true cyclic (over n certs)
|
|
return certificate.Equals (parent) ? null : parent;
|
|
}
|
|
|
|
private bool IsChainComplete (X509Certificate2 certificate)
|
|
{
|
|
// the chain is complete if we have a self-signed certificate
|
|
if (!IsSelfIssued (certificate))
|
|
return false;
|
|
|
|
// we're very limited to what we can do without certificate extensions
|
|
if (certificate.Version < 3)
|
|
return true;
|
|
|
|
// check that Authority Key Identifier == Subject Key Identifier
|
|
// e.g. it will be different if a self-signed certificate is part (not the end) of the chain
|
|
string ski = GetSubjectKeyIdentifier (certificate);
|
|
if (String.IsNullOrEmpty (ski))
|
|
return true;
|
|
string aki = GetAuthorityKeyIdentifier (certificate);
|
|
if (String.IsNullOrEmpty (aki))
|
|
return true;
|
|
// if both id are available then they must match
|
|
return (aki == ski);
|
|
}
|
|
|
|
// check for "self-issued" certificate - without verifying the signature
|
|
// note that self-issued doesn't always mean it's a root certificate!
|
|
private bool IsSelfIssued (X509Certificate2 certificate)
|
|
{
|
|
return (certificate.Issuer == certificate.Subject);
|
|
}
|
|
|
|
|
|
// *** certificate chain/path validation stuff ***
|
|
|
|
// Currently a subset of RFC3280 (hopefully a full implementation someday)
|
|
private void ValidateChain (X509ChainStatusFlags flag)
|
|
{
|
|
// 'n' should be the root certificate...
|
|
int n = elements.Count - 1;
|
|
X509Certificate2 certificate = elements [n].Certificate;
|
|
|
|
// ... and, if so, must be treated outside the chain...
|
|
if (((flag & X509ChainStatusFlags.PartialChain) == 0)) {
|
|
Process (n);
|
|
// deal with the case where the chain == the root certificate
|
|
// (which isn't for RFC3280) part of the chain
|
|
if (n == 0) {
|
|
elements [0].UncompressFlags ();
|
|
return;
|
|
}
|
|
// skip the root certificate when processing the chain (in 6.1.3)
|
|
n--;
|
|
}
|
|
// ... unless the chain is a partial one (then we start with that one)
|
|
|
|
// 6.1.1 - Inputs
|
|
// 6.1.1.a - a prospective certificate path of length n (i.e. elements)
|
|
// 6.1.1.b - the current date/time (i.e. ChainPolicy.VerificationTime)
|
|
// 6.1.1.c - user-initial-policy-set (i.e. ChainPolicy.CertificatePolicy)
|
|
// 6.1.1.d - the trust anchor information (i.e. certificate, unless it's a partial chain)
|
|
// 6.1.1.e - initial-policy-mapping-inhibit (NOT SUPPORTED BY THE API)
|
|
// 6.1.1.f - initial-explicit-policy (NOT SUPPORTED BY THE API)
|
|
// 6.1.1.g - initial-any-policy-inhibit (NOT SUPPORTED BY THE API)
|
|
|
|
// 6.1.2 - Initialization (incomplete)
|
|
// 6.1.2.a-f - policy stuff, some TODO, some not supported
|
|
// 6.1.2.g - working public key algorithm
|
|
// working_public_key_algorithm = certificate.PublicKey.Oid.Value;
|
|
// 6.1.2.h-i - our key contains both the "working public key" and "working public key parameters" data
|
|
working_public_key = certificate.PublicKey.Key;
|
|
// 6.1.2.j - working issuer name
|
|
working_issuer_name = certificate.IssuerName;
|
|
// 6.1.2.k - this integer is initialized to n, is decremented for each non-self-issued, certificate and
|
|
// may be reduced to the value in the path length constraint field
|
|
max_path_length = n;
|
|
|
|
// 6.1.3 - Basic Certificate Processing
|
|
// note: loop looks reversed (the list is) but we process this part just like RFC3280 does
|
|
for (int i = n; i > 0; i--) {
|
|
Process (i);
|
|
// 6.1.4 - preparation for certificate i+1 (for not with i+1, or i-1 in our loop)
|
|
PrepareForNextCertificate (i);
|
|
}
|
|
Process (0);
|
|
|
|
// 6.1.3.a.3 - revocation checks
|
|
CheckRevocationOnChain (flag);
|
|
|
|
// 6.1.5 - Wrap-up procedure
|
|
WrapUp ();
|
|
}
|
|
|
|
private void Process (int n)
|
|
{
|
|
X509ChainElement element = elements [n];
|
|
X509Certificate2 certificate = element.Certificate;
|
|
var monoCertificate = X509Helper2.GetMonoCertificate (certificate);
|
|
|
|
// pre-step: DSA certificates may inherit the parameters of their CA
|
|
if ((n != elements.Count - 1) && (monoCertificate.KeyAlgorithm == "1.2.840.10040.4.1")) {
|
|
if (monoCertificate.KeyAlgorithmParameters == null) {
|
|
var parent = X509Helper2.GetMonoCertificate (elements [n+1].Certificate);
|
|
monoCertificate.KeyAlgorithmParameters = parent.KeyAlgorithmParameters;
|
|
}
|
|
}
|
|
|
|
bool root = (working_public_key == null);
|
|
// 6.1.3.a.1 - check signature (with special case to deal with root certificates)
|
|
if (!IsSignedWith (certificate, root ? certificate.PublicKey.Key : working_public_key)) {
|
|
// another special case where only an end-entity is available and can't be verified.
|
|
// In this case we do not report an invalid signature (since this is unknown)
|
|
if (root || (n != elements.Count - 1) || IsSelfIssued (certificate)) {
|
|
element.StatusFlags |= X509ChainStatusFlags.NotSignatureValid;
|
|
}
|
|
}
|
|
|
|
// 6.1.3.a.2 - check validity period
|
|
if ((ChainPolicy.VerificationTime < certificate.NotBefore) ||
|
|
(ChainPolicy.VerificationTime > certificate.NotAfter)) {
|
|
element.StatusFlags |= X509ChainStatusFlags.NotTimeValid;
|
|
}
|
|
// TODO - for X509ChainStatusFlags.NotTimeNested (needs global structure)
|
|
|
|
// note: most of them don't apply to the root certificate
|
|
if (root) {
|
|
return;
|
|
}
|
|
|
|
// 6.1.3.a.3 - revocation check (we're doing at the last stage)
|
|
// note: you revoke a trusted root by removing it from your trusted store (i.e. no CRL can do this job)
|
|
|
|
// 6.1.3.a.4 - check certificate issuer name
|
|
if (!X500DistinguishedName.AreEqual (certificate.IssuerName, working_issuer_name)) {
|
|
// NOTE: this is not the "right" error flag, but it's the closest one defined
|
|
element.StatusFlags |= X509ChainStatusFlags.InvalidNameConstraints;
|
|
}
|
|
|
|
if (!IsSelfIssued (certificate) && (n != 0)) {
|
|
// TODO 6.1.3.b - subject name in the permitted_subtrees ...
|
|
// TODO 6.1.3.c - subject name not within excluded_subtrees...
|
|
|
|
// TODO - check for X509ChainStatusFlags.InvalidNameConstraint
|
|
// TODO - check for X509ChainStatusFlags.HasNotSupportedNameConstraint
|
|
// TODO - check for X509ChainStatusFlags.HasNotPermittedNameConstraint
|
|
// TODO - check for X509ChainStatusFlags.HasExcludedNameConstraint
|
|
}
|
|
|
|
// TODO 6.1.3.d - check if certificate policies extension is present
|
|
//if (false) {
|
|
// TODO - for X509ChainStatusFlags.InvalidPolicyConstraints
|
|
// using X509ChainPolicy.ApplicationPolicy and X509ChainPolicy.CertificatePolicy
|
|
|
|
// TODO - check for X509ChainStatusFlags.NoIssuanceChainPolicy
|
|
|
|
//} else {
|
|
// TODO 6.1.3.e - set valid_policy_tree to NULL
|
|
//}
|
|
|
|
// TODO 6.1.3.f - verify explict_policy > 0 if valid_policy_tree != NULL
|
|
}
|
|
|
|
// CTL == Certificate Trust List / NOT SUPPORTED
|
|
// TODO - check for X509ChainStatusFlags.CtlNotTimeValid
|
|
// TODO - check for X509ChainStatusFlags.CtlNotSignatureValid
|
|
// TODO - check for X509ChainStatusFlags.CtlNotValidForUsage
|
|
|
|
private void PrepareForNextCertificate (int n)
|
|
{
|
|
X509ChainElement element = elements [n];
|
|
X509Certificate2 certificate = element.Certificate;
|
|
|
|
// TODO 6.1.4.a-b
|
|
|
|
// 6.1.4.c
|
|
working_issuer_name = certificate.SubjectName;
|
|
// 6.1.4.d-e - our key includes both the public key and it's parameters
|
|
working_public_key = certificate.PublicKey.Key;
|
|
// 6.1.4.f
|
|
// working_public_key_algorithm = certificate.PublicKey.Oid.Value;
|
|
|
|
// TODO 6.1.4.g-j
|
|
|
|
// 6.1.4.k - Verify that the certificate is a CA certificate
|
|
X509BasicConstraintsExtension bce = (certificate.Extensions["2.5.29.19"] as X509BasicConstraintsExtension);
|
|
if (bce != null) {
|
|
if (!bce.CertificateAuthority) {
|
|
element.StatusFlags |= X509ChainStatusFlags.InvalidBasicConstraints;
|
|
}
|
|
} else if (certificate.Version >= 3) {
|
|
// recent (v3+) CA certificates must include BCE
|
|
element.StatusFlags |= X509ChainStatusFlags.InvalidBasicConstraints;
|
|
}
|
|
|
|
// 6.1.4.l - if the certificate isn't self-issued...
|
|
if (!IsSelfIssued (certificate)) {
|
|
// ... verify that max_path_length > 0
|
|
if (max_path_length > 0) {
|
|
max_path_length--;
|
|
} else {
|
|
// to match MS the reported status must be against the certificate
|
|
// with the BCE and not where the path is too long. It also means
|
|
// that this condition has to be reported only once
|
|
if (bce_restriction != null) {
|
|
bce_restriction.StatusFlags |= X509ChainStatusFlags.InvalidBasicConstraints;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 6.1.4.m - if pathLengthConstraint is present...
|
|
if ((bce != null) && (bce.HasPathLengthConstraint)) {
|
|
// ... and is less that max_path_length, set max_path_length to it's value
|
|
if (bce.PathLengthConstraint < max_path_length) {
|
|
max_path_length = bce.PathLengthConstraint;
|
|
bce_restriction = element;
|
|
}
|
|
}
|
|
|
|
// 6.1.4.n - if key usage extension is present...
|
|
X509KeyUsageExtension kue = (certificate.Extensions["2.5.29.15"] as X509KeyUsageExtension);
|
|
if (kue != null) {
|
|
// ... verify keyCertSign is set
|
|
X509KeyUsageFlags success = X509KeyUsageFlags.KeyCertSign;
|
|
if ((kue.KeyUsages & success) != success)
|
|
element.StatusFlags |= X509ChainStatusFlags.NotValidForUsage;
|
|
}
|
|
|
|
// 6.1.4.o - recognize and process other critical extension present in the certificate
|
|
ProcessCertificateExtensions (element);
|
|
}
|
|
|
|
private void WrapUp ()
|
|
{
|
|
X509ChainElement element = elements [0];
|
|
X509Certificate2 certificate = element.Certificate;
|
|
|
|
// 6.1.5.a - TODO if certificate n (our 0) wasn't self issued and explicit_policy != 0
|
|
if (IsSelfIssued (certificate)) {
|
|
// TODO... decrement explicit_policy by 1
|
|
}
|
|
|
|
// 6.1.5.b - TODO
|
|
|
|
// 6.1.5.c,d,e - not required by the X509Chain implementation
|
|
|
|
// 6.1.5.f - recognize and process other critical extension present in the certificate
|
|
ProcessCertificateExtensions (element);
|
|
|
|
// 6.1.5.g - TODO
|
|
|
|
// uncompressed the flags into several elements
|
|
for (int i = elements.Count - 1; i >= 0; i--) {
|
|
elements [i].UncompressFlags ();
|
|
}
|
|
}
|
|
|
|
private void ProcessCertificateExtensions (X509ChainElement element)
|
|
{
|
|
foreach (X509Extension ext in element.Certificate.Extensions) {
|
|
if (ext.Critical) {
|
|
switch (ext.Oid.Value) {
|
|
case "2.5.29.15": // X509KeyUsageExtension
|
|
case "2.5.29.19": // X509BasicConstraintsExtension
|
|
// we processed this extension
|
|
break;
|
|
default:
|
|
// note: Under Windows XP MS implementation seems to ignore
|
|
// certificate with unknown critical extensions.
|
|
element.StatusFlags |= X509ChainStatusFlags.InvalidExtension;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsSignedWith (X509Certificate2 signed, AsymmetricAlgorithm pubkey)
|
|
{
|
|
if (pubkey == null)
|
|
return false;
|
|
// Sadly X509Certificate2 doesn't expose the signature nor the tbs (to be signed) structure
|
|
var mx = X509Helper2.GetMonoCertificate (signed);
|
|
return (mx.VerifySignature (pubkey));
|
|
}
|
|
|
|
private string GetSubjectKeyIdentifier (X509Certificate2 certificate)
|
|
{
|
|
X509SubjectKeyIdentifierExtension ski = (certificate.Extensions["2.5.29.14"] as X509SubjectKeyIdentifierExtension);
|
|
return (ski == null) ? String.Empty : ski.SubjectKeyIdentifier;
|
|
}
|
|
|
|
// System.dll v2 doesn't have a class to deal with the AuthorityKeyIdentifier extension
|
|
static string GetAuthorityKeyIdentifier (X509Certificate2 certificate)
|
|
{
|
|
var monoCertificate = X509Helper2.GetMonoCertificate (certificate);
|
|
return GetAuthorityKeyIdentifier (monoCertificate.Extensions ["2.5.29.35"]);
|
|
}
|
|
|
|
// but anyway System.dll v2 doesn't expose CRL in any way so...
|
|
static string GetAuthorityKeyIdentifier (MX.X509Crl crl)
|
|
{
|
|
return GetAuthorityKeyIdentifier (crl.Extensions ["2.5.29.35"]);
|
|
}
|
|
|
|
static string GetAuthorityKeyIdentifier (MX.X509Extension ext)
|
|
{
|
|
if (ext == null)
|
|
return String.Empty;
|
|
var aki = new MX.Extensions.AuthorityKeyIdentifierExtension (ext);
|
|
byte[] id = aki.Identifier;
|
|
if (id == null)
|
|
return String.Empty;
|
|
StringBuilder sb = new StringBuilder ();
|
|
foreach (byte b in id)
|
|
sb.Append (b.ToString ("X02"));
|
|
return sb.ToString ();
|
|
}
|
|
|
|
// we check the revocation only once we have built the complete chain
|
|
private void CheckRevocationOnChain (X509ChainStatusFlags flag)
|
|
{
|
|
bool partial = ((flag & X509ChainStatusFlags.PartialChain) != 0);
|
|
bool online;
|
|
|
|
switch (ChainPolicy.RevocationMode) {
|
|
case X509RevocationMode.Online:
|
|
// default
|
|
online = true;
|
|
break;
|
|
case X509RevocationMode.Offline:
|
|
online = false;
|
|
break;
|
|
case X509RevocationMode.NoCheck:
|
|
return;
|
|
default:
|
|
throw new InvalidOperationException (Locale.GetText ("Invalid revocation mode."));
|
|
}
|
|
|
|
bool unknown = partial;
|
|
// from the root down to the end-entity
|
|
for (int i = elements.Count - 1; i >= 0; i--) {
|
|
bool check = true;
|
|
|
|
switch (ChainPolicy.RevocationFlag) {
|
|
case X509RevocationFlag.EndCertificateOnly:
|
|
check = (i == 0);
|
|
break;
|
|
case X509RevocationFlag.EntireChain:
|
|
check = true;
|
|
break;
|
|
case X509RevocationFlag.ExcludeRoot:
|
|
// default
|
|
check = (i != (elements.Count - 1));
|
|
// anyway, who's gonna sign that the root is invalid ?
|
|
break;
|
|
}
|
|
|
|
X509ChainElement element = elements [i];
|
|
|
|
// we can't assume the revocation status if the certificate is bad (e.g. invalid signature)
|
|
if (!unknown)
|
|
unknown |= ((element.StatusFlags & X509ChainStatusFlags.NotSignatureValid) != 0);
|
|
|
|
if (unknown) {
|
|
// we can skip the revocation checks as we can't be sure of them anyway
|
|
element.StatusFlags |= X509ChainStatusFlags.RevocationStatusUnknown;
|
|
element.StatusFlags |= X509ChainStatusFlags.OfflineRevocation;
|
|
} else if (check && !partial && !IsSelfIssued (element.Certificate)) {
|
|
// check for revocation (except for the trusted root and self-issued certs)
|
|
element.StatusFlags |= CheckRevocation (element.Certificate, i+1, online);
|
|
// if revoked, then all others following in the chain are unknown...
|
|
unknown |= ((element.StatusFlags & X509ChainStatusFlags.Revoked) != 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// This isn't how RFC3280 (section 6.3) deals with CRL, but then we don't (yet) support DP, deltas...
|
|
private X509ChainStatusFlags CheckRevocation (X509Certificate2 certificate, int ca, bool online)
|
|
{
|
|
X509ChainStatusFlags result = X509ChainStatusFlags.RevocationStatusUnknown;
|
|
X509ChainElement element = elements [ca];
|
|
X509Certificate2 ca_cert = element.Certificate;
|
|
|
|
// find the CRL from the "right" CA
|
|
while (IsSelfIssued (ca_cert) && (ca < elements.Count - 1)) {
|
|
// try with this self-issued
|
|
result = CheckRevocation (certificate, ca_cert, online);
|
|
if (result != X509ChainStatusFlags.RevocationStatusUnknown)
|
|
break;
|
|
ca++;
|
|
element = elements [ca];
|
|
ca_cert = element.Certificate;
|
|
}
|
|
if (result == X509ChainStatusFlags.RevocationStatusUnknown)
|
|
result = CheckRevocation (certificate, ca_cert, online);
|
|
return result;
|
|
}
|
|
|
|
private X509ChainStatusFlags CheckRevocation (X509Certificate2 certificate, X509Certificate2 ca_cert, bool online)
|
|
{
|
|
// change this if/when we support OCSP
|
|
X509KeyUsageExtension kue = (ca_cert.Extensions["2.5.29.15"] as X509KeyUsageExtension);
|
|
if (kue != null) {
|
|
// ... verify CrlSign is set
|
|
X509KeyUsageFlags success = X509KeyUsageFlags.CrlSign;
|
|
if ((kue.KeyUsages & success) != success) {
|
|
// FIXME - we should try to find an alternative CA that has the CrlSign bit
|
|
return X509ChainStatusFlags.RevocationStatusUnknown;
|
|
}
|
|
}
|
|
|
|
MX.X509Crl crl = FindCrl (ca_cert);
|
|
|
|
if ((crl == null) && online) {
|
|
// FIXME - download and install new CRL
|
|
// then you get a second chance
|
|
// crl = FindCrl (ca_cert, ref valid, ref out_of_date);
|
|
|
|
// We need to get the subjectAltName and an URI from there (or use OCSP)
|
|
// X509KeyUsageExtension subjectAltName = (ca_cert.Extensions["2.5.29.17"] as X509KeyUsageExtension);
|
|
}
|
|
|
|
if (crl != null) {
|
|
// validate the digital signature on the CRL using the CA public key
|
|
// note #1: we can't use X509Crl.VerifySignature(X509Certificate) because it duplicates
|
|
// checks and we loose the "why" of the failure
|
|
// note #2: we do this before other tests as an invalid signature could be a hacked CRL
|
|
// (so anything within can't be trusted)
|
|
if (!crl.VerifySignature (ca_cert.PublicKey.Key)) {
|
|
return X509ChainStatusFlags.RevocationStatusUnknown;
|
|
}
|
|
|
|
var monoCertificate = X509Helper2.GetMonoCertificate (certificate);
|
|
MX.X509Crl.X509CrlEntry entry = crl.GetCrlEntry (monoCertificate);
|
|
if (entry != null) {
|
|
// We have an entry for this CRL that includes an unknown CRITICAL extension
|
|
// See [X.509 7.3] NOTE 4
|
|
if (!ProcessCrlEntryExtensions (entry))
|
|
return X509ChainStatusFlags.Revoked;
|
|
|
|
// FIXME - a little more is involved
|
|
if (entry.RevocationDate <= ChainPolicy.VerificationTime)
|
|
return X509ChainStatusFlags.Revoked;
|
|
}
|
|
|
|
// are we overdue for a CRL update ? if so we can't be sure of any certificate status
|
|
if (crl.NextUpdate < ChainPolicy.VerificationTime)
|
|
return X509ChainStatusFlags.RevocationStatusUnknown | X509ChainStatusFlags.OfflineRevocation;
|
|
|
|
// we have a CRL that includes an unknown CRITICAL extension
|
|
// we put this check at the end so we do not "hide" any Revoked flags
|
|
if (!ProcessCrlExtensions (crl)) {
|
|
return X509ChainStatusFlags.RevocationStatusUnknown;
|
|
}
|
|
} else {
|
|
return X509ChainStatusFlags.RevocationStatusUnknown;
|
|
}
|
|
|
|
return X509ChainStatusFlags.NoError;
|
|
}
|
|
|
|
static MX.X509Crl CheckCrls (string subject, string ski, MX.X509Store store)
|
|
{
|
|
if (store == null)
|
|
return null;
|
|
|
|
var crls = store.Crls;
|
|
foreach (MX.X509Crl crl in crls) {
|
|
if (crl.IssuerName == subject && (ski.Length == 0 || ski == GetAuthorityKeyIdentifier (crl)))
|
|
return crl;
|
|
}
|
|
return null; // No CRL found
|
|
}
|
|
|
|
private MX.X509Crl FindCrl (X509Certificate2 caCertificate)
|
|
{
|
|
string subject = caCertificate.SubjectName.Decode (X500DistinguishedNameFlags.None);
|
|
string ski = GetSubjectKeyIdentifier (caCertificate);
|
|
|
|
// consider that the LocalMachine directories could not exists... and cannot be created by the user
|
|
MX.X509Crl result = CheckCrls (subject, ski, LMCAStore.Store);
|
|
if (result != null)
|
|
return result;
|
|
if (location == StoreLocation.CurrentUser) {
|
|
result = CheckCrls (subject, ski, UserCAStore.Store);
|
|
if (result != null)
|
|
return result;
|
|
}
|
|
|
|
// consider that the LocalMachine directories could not exists... and cannot be created by the user
|
|
result = CheckCrls (subject, ski, LMRootStore.Store);
|
|
if (result != null)
|
|
return result;
|
|
if (location == StoreLocation.CurrentUser) {
|
|
result = CheckCrls (subject, ski, UserRootStore.Store);
|
|
if (result != null)
|
|
return result;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private bool ProcessCrlExtensions (MX.X509Crl crl)
|
|
{
|
|
foreach (MX.X509Extension ext in crl.Extensions) {
|
|
if (ext.Critical) {
|
|
switch (ext.Oid) {
|
|
case "2.5.29.20": // cRLNumber
|
|
case "2.5.29.35": // authorityKeyIdentifier
|
|
// we processed/know about this extension
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private bool ProcessCrlEntryExtensions (MX.X509Crl.X509CrlEntry entry)
|
|
{
|
|
foreach (MX.X509Extension ext in entry.Extensions) {
|
|
if (ext.Critical) {
|
|
switch (ext.Oid) {
|
|
case "2.5.29.21": // cRLReason
|
|
// we processed/know about this extension
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
#endif
|