blob: 9e44c4b8d5750e7b6a856f0e55f245affb8282a5 [file] [log] [blame]
// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/base/x509_openssl_util.h"
#include "base/logging.h"
#include "base/string_number_conversions.h"
#include "base/string_piece.h"
#include "base/string_util.h"
#include "base/time.h"
namespace net {
namespace x509_openssl_util {
namespace {
// Helper for ParseDate. |*field| must contain at least |field_len| characters.
// |*field| will be advanced by |field_len| on exit. |*ok| is set to false if
// there is an error in parsing the number, but left untouched otherwise.
// Returns the parsed integer.
int ParseIntAndAdvance(const char** field, size_t field_len, bool* ok) {
int result = 0;
*ok &= base::StringToInt(*field, *field + field_len, &result);
*field += field_len;
return result;
}
} // namespace
bool ParsePrincipalKeyAndValueByIndex(X509_NAME* name,
int index,
std::string* key,
std::string* value) {
X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, index);
if (!entry)
return false;
if (key) {
ASN1_OBJECT* object = X509_NAME_ENTRY_get_object(entry);
key->assign(OBJ_nid2sn(OBJ_obj2nid(object)));
}
ASN1_STRING* data = X509_NAME_ENTRY_get_data(entry);
if (!data)
return false;
unsigned char* buf = NULL;
int len = ASN1_STRING_to_UTF8(&buf, data);
if (len <= 0)
return false;
value->assign(reinterpret_cast<const char*>(buf), len);
OPENSSL_free(buf);
return true;
}
bool ParsePrincipalValueByIndex(X509_NAME* name,
int index,
std::string* value) {
return ParsePrincipalKeyAndValueByIndex(name, index, NULL, value);
}
bool ParsePrincipalValueByNID(X509_NAME* name, int nid, std::string* value) {
int index = X509_NAME_get_index_by_NID(name, nid, -1);
if (index < 0)
return false;
return ParsePrincipalValueByIndex(name, index, value);
}
bool ParseDate(ASN1_TIME* x509_time, base::Time* time) {
if (!x509_time ||
(x509_time->type != V_ASN1_UTCTIME &&
x509_time->type != V_ASN1_GENERALIZEDTIME))
return false;
base::StringPiece str_date(reinterpret_cast<const char*>(x509_time->data),
x509_time->length);
// UTCTime: YYMMDDHHMMSSZ
// GeneralizedTime: YYYYMMDDHHMMSSZ
size_t year_length = x509_time->type == V_ASN1_UTCTIME ? 2 : 4;
if (str_date.length() < 11 + year_length)
return false;
const char* field = str_date.data();
bool valid = true;
base::Time::Exploded exploded = {0};
exploded.year = ParseIntAndAdvance(&field, year_length, &valid);
exploded.month = ParseIntAndAdvance(&field, 2, &valid);
exploded.day_of_month = ParseIntAndAdvance(&field, 2, &valid);
exploded.hour = ParseIntAndAdvance(&field, 2, &valid);
exploded.minute = ParseIntAndAdvance(&field, 2, &valid);
exploded.second = ParseIntAndAdvance(&field, 2, &valid);
if (valid && year_length == 2)
exploded.year += exploded.year < 50 ? 2000 : 1900;
valid &= exploded.HasValidValues();
if (!valid) {
NOTREACHED() << "can't parse x509 date " << str_date;
return false;
}
*time = base::Time::FromUTCExploded(exploded);
return true;
}
// TODO(joth): Investigate if we can upstream this into the OpenSSL library,
// to avoid duplicating this logic across projects.
bool VerifyHostname(const std::string& hostname,
const std::vector<std::string>& cert_names) {
DCHECK(!hostname.empty());
// Simple host name validation. A valid domain name must only contain
// alpha, digits, hyphens, and dots. An IP address may have digits and dots,
// and also square braces and colons for IPv6 addresses.
std::string reference_name;
reference_name.reserve(hostname.length());
bool found_alpha = false;
bool found_ip6_chars = false;
bool found_hyphen = false;
int dot_count = 0;
size_t first_dot_index = std::string::npos;
for (std::string::const_iterator it = hostname.begin();
it != hostname.end(); ++it) {
char c = *it;
if (IsAsciiAlpha(c)) {
found_alpha = true;
c = base::ToLowerASCII(c);
} else if (c == '.') {
++dot_count;
if (first_dot_index == std::string::npos)
first_dot_index = reference_name.length();
} else if (c == ':') {
found_ip6_chars = true;
} else if (c == '-') {
found_hyphen = true;
} else if (!IsAsciiDigit(c)) {
LOG(WARNING) << "Invalid char " << c << " in hostname " << hostname;
return false;
}
reference_name.push_back(c);
}
DCHECK(!reference_name.empty());
// TODO(joth): Add IP address support. See http://crbug.com/62973
if (found_ip6_chars || !found_alpha) {
NOTIMPLEMENTED() << hostname;
return false;
}
// |wildcard_domain| is the remainder of |host| after the leading host
// component is stripped off, but includes the leading dot e.g.
// "www.f.com" -> ".f.com".
// If there is no meaningful domain part to |host| (e.g. it is an IP address
// or contains no dots) then |wildcard_domain| will be empty.
// We required at least 3 components (i.e. 2 dots) as a basic protection
// against too-broad wild-carding.
base::StringPiece wildcard_domain;
if (found_alpha && !found_ip6_chars && dot_count >= 2) {
DCHECK(first_dot_index != std::string::npos);
wildcard_domain = reference_name;
wildcard_domain.remove_prefix(first_dot_index);
DCHECK(wildcard_domain.starts_with("."));
}
for (std::vector<std::string>::const_iterator it = cert_names.begin();
it != cert_names.end(); ++it) {
// Catch badly corrupt cert names up front.
if (it->empty() || it->find('\0') != std::string::npos) {
LOG(WARNING) << "Bad name in cert: " << *it;
continue;
}
const std::string cert_name_string(StringToLowerASCII(*it));
base::StringPiece cert_match(cert_name_string);
// Remove trailing dot, if any.
if (cert_match.ends_with("."))
cert_match.remove_suffix(1);
// The hostname must be at least as long as the cert name it is matching,
// as we require the wildcard (if present) to match at least one character.
if (cert_match.length() > reference_name.length())
continue;
if (cert_match == reference_name)
return true;
// Next see if this cert name starts with a wildcard, so long as the
// hostname we're matching against has a valid 'domain' part to match.
// Note the "-10" version of draft-saintandre-tls-server-id-check allows
// the wildcard to appear anywhere in the leftmost label, rather than
// requiring it to be the only character. See also http://crbug.com/60719
if (wildcard_domain.empty() || !cert_match.starts_with("*"))
continue;
// Erase the * but not the . from the domain, as we need to include the dot
// in the comparison.
cert_match.remove_prefix(1);
// Do character by character comparison on the remainder to see
// if we have a wildcard match. This intentionally does no special handling
// for any other wildcard characters in |domain|; alternatively it could
// detect these and skip those candidate cert names.
if (cert_match == wildcard_domain)
return true;
}
DVLOG(1) << "Could not find any match for " << hostname
<< " (canonicalized as " << reference_name
<< ") in cert names " << JoinString(cert_names, '|');
return false;
}
} // namespace x509_openssl_util
} // namespace net