710 lines
16 KiB
C
710 lines
16 KiB
C
/*
|
|
* Copyright (C) 2010 Martin Willi
|
|
* Copyright (C) 2010 revosec AG
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation; either version 2 of the License, or (at your
|
|
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* for more details.
|
|
*/
|
|
|
|
#include "constraints_validator.h"
|
|
|
|
#include <utils/debug.h>
|
|
#include <asn1/asn1.h>
|
|
#include <collections/linked_list.h>
|
|
#include <credentials/certificates/x509.h>
|
|
|
|
typedef struct private_constraints_validator_t private_constraints_validator_t;
|
|
|
|
/**
|
|
* Private data of an constraints_validator_t object.
|
|
*/
|
|
struct private_constraints_validator_t {
|
|
|
|
/**
|
|
* Public constraints_validator_t interface.
|
|
*/
|
|
constraints_validator_t public;
|
|
};
|
|
|
|
/**
|
|
* Check pathlen constraint of issuer certificate
|
|
*/
|
|
static bool check_pathlen(x509_t *issuer, int pathlen)
|
|
{
|
|
u_int pathlen_constraint;
|
|
|
|
pathlen_constraint = issuer->get_constraint(issuer, X509_PATH_LEN);
|
|
if (pathlen_constraint != X509_NO_CONSTRAINT &&
|
|
pathlen > pathlen_constraint)
|
|
{
|
|
DBG1(DBG_CFG, "path length of %d violates constraint of %d",
|
|
pathlen, pathlen_constraint);
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Check if a FQDN constraint matches
|
|
*/
|
|
static bool fqdn_matches(identification_t *constraint, identification_t *id)
|
|
{
|
|
chunk_t c, i, diff;
|
|
|
|
c = constraint->get_encoding(constraint);
|
|
i = id->get_encoding(id);
|
|
|
|
if (!c.len || i.len < c.len)
|
|
{
|
|
return FALSE;
|
|
}
|
|
diff = chunk_create(i.ptr, i.len - c.len);
|
|
if (!chunk_equals(c, chunk_skip(i, diff.len)))
|
|
{
|
|
return FALSE;
|
|
}
|
|
if (!diff.len)
|
|
{
|
|
return TRUE;
|
|
}
|
|
if (c.ptr[0] == '.' || diff.ptr[diff.len - 1] == '.')
|
|
{
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* Check if a RFC822 constraint matches
|
|
*/
|
|
static bool email_matches(identification_t *constraint, identification_t *id)
|
|
{
|
|
chunk_t c, i, diff;
|
|
|
|
c = constraint->get_encoding(constraint);
|
|
i = id->get_encoding(id);
|
|
|
|
if (!c.len || i.len < c.len)
|
|
{
|
|
return FALSE;
|
|
}
|
|
if (memchr(c.ptr, '@', c.len))
|
|
{ /* constraint is a full email address */
|
|
return chunk_equals(c, i);
|
|
}
|
|
diff = chunk_create(i.ptr, i.len - c.len);
|
|
if (!diff.len || !chunk_equals(c, chunk_skip(i, diff.len)))
|
|
{
|
|
return FALSE;
|
|
}
|
|
if (c.ptr[0] == '.')
|
|
{ /* constraint is domain, suffix match */
|
|
return TRUE;
|
|
}
|
|
if (diff.ptr[diff.len - 1] == '@')
|
|
{ /* constraint is host specific, only username can be appended */
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* Check if a DN constraint matches (RDN prefix match)
|
|
*/
|
|
static bool dn_matches(identification_t *constraint, identification_t *id)
|
|
{
|
|
enumerator_t *ec, *ei;
|
|
id_part_t pc, pi;
|
|
chunk_t cc, ci;
|
|
bool match = TRUE;
|
|
|
|
ec = constraint->create_part_enumerator(constraint);
|
|
ei = id->create_part_enumerator(id);
|
|
while (ec->enumerate(ec, &pc, &cc))
|
|
{
|
|
if (!ei->enumerate(ei, &pi, &ci) ||
|
|
pi != pc || !chunk_equals(cc, ci))
|
|
{
|
|
match = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
ec->destroy(ec);
|
|
ei->destroy(ei);
|
|
|
|
return match;
|
|
}
|
|
|
|
/**
|
|
* Check if a certificate matches to a NameConstraint
|
|
*/
|
|
static bool name_constraint_matches(identification_t *constraint,
|
|
certificate_t *cert, bool permitted)
|
|
{
|
|
x509_t *x509 = (x509_t*)cert;
|
|
enumerator_t *enumerator;
|
|
identification_t *id;
|
|
id_type_t type;
|
|
bool matches = permitted;
|
|
|
|
type = constraint->get_type(constraint);
|
|
if (type == ID_DER_ASN1_DN)
|
|
{
|
|
matches = dn_matches(constraint, cert->get_subject(cert));
|
|
if (matches != permitted)
|
|
{
|
|
return matches;
|
|
}
|
|
}
|
|
|
|
enumerator = x509->create_subjectAltName_enumerator(x509);
|
|
while (enumerator->enumerate(enumerator, &id))
|
|
{
|
|
if (id->get_type(id) == type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case ID_FQDN:
|
|
matches = fqdn_matches(constraint, id);
|
|
break;
|
|
case ID_RFC822_ADDR:
|
|
matches = email_matches(constraint, id);
|
|
break;
|
|
case ID_DER_ASN1_DN:
|
|
matches = dn_matches(constraint, id);
|
|
break;
|
|
default:
|
|
DBG1(DBG_CFG, "%N NameConstraint matching not implemented",
|
|
id_type_names, type);
|
|
matches = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
if (matches != permitted)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
return matches;
|
|
}
|
|
|
|
/**
|
|
* Check if a permitted or excluded NameConstraint has been inherited to sub-CA
|
|
*/
|
|
static bool name_constraint_inherited(identification_t *constraint,
|
|
x509_t *x509, bool permitted)
|
|
{
|
|
enumerator_t *enumerator;
|
|
identification_t *id, *a, *b;
|
|
bool inherited = FALSE;
|
|
id_type_t type;
|
|
|
|
if (!(x509->get_flags(x509) & X509_CA))
|
|
{ /* not a sub-CA, not required */
|
|
return TRUE;
|
|
}
|
|
|
|
type = constraint->get_type(constraint);
|
|
enumerator = x509->create_name_constraint_enumerator(x509, permitted);
|
|
while (enumerator->enumerate(enumerator, &id))
|
|
{
|
|
if (id->get_type(id) == type)
|
|
{
|
|
if (permitted)
|
|
{ /* permitted constraint can be narrowed */
|
|
a = constraint;
|
|
b = id;
|
|
}
|
|
else
|
|
{ /* excluded constraint can be widened */
|
|
a = id;
|
|
b = constraint;
|
|
}
|
|
switch (type)
|
|
{
|
|
case ID_FQDN:
|
|
inherited = fqdn_matches(a, b);
|
|
break;
|
|
case ID_RFC822_ADDR:
|
|
inherited = email_matches(a, b);
|
|
break;
|
|
case ID_DER_ASN1_DN:
|
|
inherited = dn_matches(a, b);
|
|
break;
|
|
default:
|
|
DBG1(DBG_CFG, "%N NameConstraint matching not implemented",
|
|
id_type_names, type);
|
|
inherited = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
if (inherited)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
return inherited;
|
|
}
|
|
|
|
/**
|
|
* Check name constraints
|
|
*/
|
|
static bool check_name_constraints(certificate_t *subject, x509_t *issuer)
|
|
{
|
|
enumerator_t *enumerator;
|
|
identification_t *constraint;
|
|
|
|
enumerator = issuer->create_name_constraint_enumerator(issuer, TRUE);
|
|
while (enumerator->enumerate(enumerator, &constraint))
|
|
{
|
|
if (!name_constraint_matches(constraint, subject, TRUE))
|
|
{
|
|
DBG1(DBG_CFG, "certificate '%Y' does not match permitted name "
|
|
"constraint '%Y'", subject->get_subject(subject), constraint);
|
|
enumerator->destroy(enumerator);
|
|
return FALSE;
|
|
}
|
|
if (!name_constraint_inherited(constraint, (x509_t*)subject, TRUE))
|
|
{
|
|
DBG1(DBG_CFG, "intermediate CA '%Y' does not inherit permitted name "
|
|
"constraint '%Y'", subject->get_subject(subject), constraint);
|
|
enumerator->destroy(enumerator);
|
|
return FALSE;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
enumerator = issuer->create_name_constraint_enumerator(issuer, FALSE);
|
|
while (enumerator->enumerate(enumerator, &constraint))
|
|
{
|
|
if (name_constraint_matches(constraint, subject, FALSE))
|
|
{
|
|
DBG1(DBG_CFG, "certificate '%Y' matches excluded name "
|
|
"constraint '%Y'", subject->get_subject(subject), constraint);
|
|
enumerator->destroy(enumerator);
|
|
return FALSE;
|
|
}
|
|
if (!name_constraint_inherited(constraint, (x509_t*)subject, FALSE))
|
|
{
|
|
DBG1(DBG_CFG, "intermediate CA '%Y' does not inherit excluded name "
|
|
"constraint '%Y'", subject->get_subject(subject), constraint);
|
|
enumerator->destroy(enumerator);
|
|
return FALSE;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Special OID for anyPolicy
|
|
*/
|
|
static chunk_t any_policy = chunk_from_chars(0x55,0x1d,0x20,0x00);
|
|
|
|
/**
|
|
* Check if an issuer certificate has a given policy OID
|
|
*/
|
|
static bool has_policy(x509_t *issuer, chunk_t oid)
|
|
{
|
|
x509_policy_mapping_t *mapping;
|
|
x509_cert_policy_t *policy;
|
|
enumerator_t *enumerator;
|
|
|
|
enumerator = issuer->create_cert_policy_enumerator(issuer);
|
|
while (enumerator->enumerate(enumerator, &policy))
|
|
{
|
|
if (chunk_equals(oid, policy->oid) ||
|
|
chunk_equals(any_policy, policy->oid))
|
|
{
|
|
enumerator->destroy(enumerator);
|
|
return TRUE;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
/* fall back to a mapped policy */
|
|
enumerator = issuer->create_policy_mapping_enumerator(issuer);
|
|
while (enumerator->enumerate(enumerator, &mapping))
|
|
{
|
|
if (chunk_equals(mapping->subject, oid))
|
|
{
|
|
enumerator->destroy(enumerator);
|
|
return TRUE;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* Check certificatePolicies.
|
|
*/
|
|
static bool check_policy(x509_t *subject, x509_t *issuer)
|
|
{
|
|
certificate_t *cert = (certificate_t*)subject;
|
|
x509_policy_mapping_t *mapping;
|
|
x509_cert_policy_t *policy;
|
|
enumerator_t *enumerator;
|
|
char *oid;
|
|
|
|
/* verify if policyMappings in subject are valid */
|
|
enumerator = subject->create_policy_mapping_enumerator(subject);
|
|
while (enumerator->enumerate(enumerator, &mapping))
|
|
{
|
|
if (!has_policy(issuer, mapping->issuer))
|
|
{
|
|
oid = asn1_oid_to_string(mapping->issuer);
|
|
DBG1(DBG_CFG, "certificate '%Y' maps policy from %s, but issuer "
|
|
"misses it", cert->get_subject(cert), oid);
|
|
free(oid);
|
|
enumerator->destroy(enumerator);
|
|
return FALSE;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
enumerator = subject->create_cert_policy_enumerator(subject);
|
|
while (enumerator->enumerate(enumerator, &policy))
|
|
{
|
|
if (!has_policy(issuer, policy->oid))
|
|
{
|
|
oid = asn1_oid_to_string(policy->oid);
|
|
DBG1(DBG_CFG, "policy %s missing in issuing certificate '%Y'",
|
|
oid, cert->get_issuer(cert));
|
|
free(oid);
|
|
enumerator->destroy(enumerator);
|
|
return FALSE;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Check if a given policy is valid under a trustchain
|
|
*/
|
|
static bool is_policy_valid(linked_list_t *chain, chunk_t oid)
|
|
{
|
|
x509_policy_mapping_t *mapping;
|
|
x509_cert_policy_t *policy;
|
|
x509_t *issuer;
|
|
enumerator_t *issuers, *policies, *mappings;
|
|
bool found = TRUE;
|
|
|
|
issuers = chain->create_enumerator(chain);
|
|
while (issuers->enumerate(issuers, &issuer))
|
|
{
|
|
int maxmap = 8;
|
|
|
|
while (found)
|
|
{
|
|
found = FALSE;
|
|
|
|
policies = issuer->create_cert_policy_enumerator(issuer);
|
|
while (policies->enumerate(policies, &policy))
|
|
{
|
|
if (chunk_equals(oid, policy->oid) ||
|
|
chunk_equals(any_policy, policy->oid))
|
|
{
|
|
found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
policies->destroy(policies);
|
|
if (found)
|
|
{
|
|
break;
|
|
}
|
|
/* fall back to a mapped policy */
|
|
mappings = issuer->create_policy_mapping_enumerator(issuer);
|
|
while (mappings->enumerate(mappings, &mapping))
|
|
{
|
|
if (chunk_equals(mapping->subject, oid))
|
|
{
|
|
oid = mapping->issuer;
|
|
found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
mappings->destroy(mappings);
|
|
if (--maxmap == 0)
|
|
{
|
|
found = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
issuers->destroy(issuers);
|
|
|
|
return found;
|
|
}
|
|
|
|
/**
|
|
* Check len certificates in trustchain for inherited policies
|
|
*/
|
|
static bool has_policy_chain(linked_list_t *chain, x509_t *subject, int len)
|
|
{
|
|
enumerator_t *enumerator;
|
|
x509_t *issuer;
|
|
bool valid = TRUE;
|
|
|
|
enumerator = chain->create_enumerator(chain);
|
|
while (len-- > 0 && enumerator->enumerate(enumerator, &issuer))
|
|
{
|
|
if (!check_policy(subject, issuer))
|
|
{
|
|
valid = FALSE;
|
|
break;
|
|
}
|
|
subject = issuer;
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
return valid;
|
|
}
|
|
|
|
/**
|
|
* Check len certificates in trustchain to have no policyMappings
|
|
*/
|
|
static bool has_no_policy_mapping(linked_list_t *chain, int len)
|
|
{
|
|
enumerator_t *enumerator, *mappings;
|
|
x509_policy_mapping_t *mapping;
|
|
certificate_t *cert;
|
|
x509_t *x509;
|
|
bool valid = TRUE;
|
|
|
|
enumerator = chain->create_enumerator(chain);
|
|
while (len-- > 0 && enumerator->enumerate(enumerator, &x509))
|
|
{
|
|
mappings = x509->create_policy_mapping_enumerator(x509);
|
|
valid = !mappings->enumerate(mappings, &mapping);
|
|
mappings->destroy(mappings);
|
|
if (!valid)
|
|
{
|
|
cert = (certificate_t*)x509;
|
|
DBG1(DBG_CFG, "found policyMapping in certificate '%Y', but "
|
|
"inhibitPolicyMapping in effect", cert->get_subject(cert));
|
|
break;
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
return valid;
|
|
}
|
|
|
|
/**
|
|
* Check len certificates in trustchain to have no anyPolicies
|
|
*/
|
|
static bool has_no_any_policy(linked_list_t *chain, int len)
|
|
{
|
|
enumerator_t *enumerator, *policies;
|
|
x509_cert_policy_t *policy;
|
|
certificate_t *cert;
|
|
x509_t *x509;
|
|
bool valid = TRUE;
|
|
|
|
enumerator = chain->create_enumerator(chain);
|
|
while (len-- > 0 && enumerator->enumerate(enumerator, &x509))
|
|
{
|
|
policies = x509->create_cert_policy_enumerator(x509);
|
|
while (policies->enumerate(policies, &policy))
|
|
{
|
|
if (chunk_equals(policy->oid, any_policy))
|
|
{
|
|
cert = (certificate_t*)x509;
|
|
DBG1(DBG_CFG, "found anyPolicy in certificate '%Y', but "
|
|
"inhibitAnyPolicy in effect", cert->get_subject(cert));
|
|
valid = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
policies->destroy(policies);
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
return valid;
|
|
}
|
|
|
|
/**
|
|
* Check requireExplicitPolicy and inhibitPolicyMapping constraints
|
|
*/
|
|
static bool check_policy_constraints(x509_t *issuer, u_int pathlen,
|
|
auth_cfg_t *auth)
|
|
{
|
|
certificate_t *subject;
|
|
bool valid = TRUE;
|
|
|
|
subject = auth->get(auth, AUTH_RULE_SUBJECT_CERT);
|
|
if (subject)
|
|
{
|
|
if (subject->get_type(subject) == CERT_X509)
|
|
{
|
|
x509_cert_policy_t *policy;
|
|
enumerator_t *enumerator;
|
|
linked_list_t *chain;
|
|
certificate_t *cert;
|
|
auth_rule_t rule;
|
|
x509_t *x509;
|
|
int len = 0;
|
|
u_int expl, inh;
|
|
char *oid;
|
|
|
|
/* prepare trustchain to validate */
|
|
chain = linked_list_create();
|
|
enumerator = auth->create_enumerator(auth);
|
|
while (enumerator->enumerate(enumerator, &rule, &cert))
|
|
{
|
|
if (rule == AUTH_RULE_IM_CERT &&
|
|
cert->get_type(cert) == CERT_X509)
|
|
{
|
|
chain->insert_last(chain, cert);
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
chain->insert_last(chain, issuer);
|
|
|
|
/* search for requireExplicitPolicy constraints */
|
|
enumerator = chain->create_enumerator(chain);
|
|
while (enumerator->enumerate(enumerator, &x509))
|
|
{
|
|
expl = x509->get_constraint(x509, X509_REQUIRE_EXPLICIT_POLICY);
|
|
if (expl != X509_NO_CONSTRAINT)
|
|
{
|
|
if (!has_policy_chain(chain, (x509_t*)subject, len - expl))
|
|
{
|
|
valid = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
len++;
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
/* search for inhibitPolicyMapping/inhibitAnyPolicy constraints */
|
|
len = 0;
|
|
chain->insert_first(chain, subject);
|
|
enumerator = chain->create_enumerator(chain);
|
|
while (enumerator->enumerate(enumerator, &x509))
|
|
{
|
|
inh = x509->get_constraint(x509, X509_INHIBIT_POLICY_MAPPING);
|
|
if (inh != X509_NO_CONSTRAINT)
|
|
{
|
|
if (!has_no_policy_mapping(chain, len - inh))
|
|
{
|
|
valid = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
inh = x509->get_constraint(x509, X509_INHIBIT_ANY_POLICY);
|
|
if (inh != X509_NO_CONSTRAINT)
|
|
{
|
|
if (!has_no_any_policy(chain, len - inh))
|
|
{
|
|
valid = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
len++;
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
|
|
if (valid)
|
|
{
|
|
x509 = (x509_t*)subject;
|
|
|
|
enumerator = x509->create_cert_policy_enumerator(x509);
|
|
while (enumerator->enumerate(enumerator, &policy))
|
|
{
|
|
oid = asn1_oid_to_string(policy->oid);
|
|
if (oid)
|
|
{
|
|
if (is_policy_valid(chain, policy->oid))
|
|
{
|
|
auth->add(auth, AUTH_RULE_CERT_POLICY, oid);
|
|
}
|
|
else
|
|
{
|
|
DBG1(DBG_CFG, "certificate policy %s for '%Y' "
|
|
"not allowed by trustchain, ignored",
|
|
oid, subject->get_subject(subject));
|
|
free(oid);
|
|
}
|
|
}
|
|
}
|
|
enumerator->destroy(enumerator);
|
|
}
|
|
chain->destroy(chain);
|
|
}
|
|
}
|
|
return valid;
|
|
}
|
|
|
|
METHOD(cert_validator_t, validate, bool,
|
|
private_constraints_validator_t *this, certificate_t *subject,
|
|
certificate_t *issuer, bool online, u_int pathlen, bool anchor,
|
|
auth_cfg_t *auth)
|
|
{
|
|
if (issuer->get_type(issuer) == CERT_X509 &&
|
|
subject->get_type(subject) == CERT_X509)
|
|
{
|
|
if (!check_pathlen((x509_t*)issuer, pathlen))
|
|
{
|
|
lib->credmgr->call_hook(lib->credmgr, CRED_HOOK_EXCEEDED_PATH_LEN,
|
|
subject);
|
|
return FALSE;
|
|
}
|
|
if (!check_name_constraints(subject, (x509_t*)issuer))
|
|
{
|
|
lib->credmgr->call_hook(lib->credmgr, CRED_HOOK_POLICY_VIOLATION,
|
|
subject);
|
|
return FALSE;
|
|
}
|
|
if (anchor)
|
|
{
|
|
if (!check_policy_constraints((x509_t*)issuer, pathlen, auth))
|
|
{
|
|
lib->credmgr->call_hook(lib->credmgr,
|
|
CRED_HOOK_POLICY_VIOLATION, issuer);
|
|
return FALSE;
|
|
}
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
METHOD(constraints_validator_t, destroy, void,
|
|
private_constraints_validator_t *this)
|
|
{
|
|
free(this);
|
|
}
|
|
|
|
/**
|
|
* See header
|
|
*/
|
|
constraints_validator_t *constraints_validator_create()
|
|
{
|
|
private_constraints_validator_t *this;
|
|
|
|
INIT(this,
|
|
.public = {
|
|
.validator.validate = _validate,
|
|
.destroy = _destroy,
|
|
},
|
|
);
|
|
|
|
return &this->public;
|
|
}
|