dodo  0.0.1
A C++ library to create containerized Linux services
x509cert.cpp
Go to the documentation of this file.
1 /*
2  * This file is part of the dodo library (https://github.com/jmspit/dodo).
3  * Copyright (c) 2019 Jan-Marten Spit.
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, version 3.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 /**
19  * @file x509cert.cpp
20  * Implements the dodo::network::SSLSocket class.
21  */
22 
23 #include <fstream>
24 #include <iostream>
25 #include <openssl/ssl.h>
26 #include <regex>
27 
28 #include "common/exception.hpp"
29 #include "common/util.hpp"
30 #include "network/address.hpp"
31 #include "network/tlscontext.hpp"
32 #include "network/x509cert.hpp"
33 
34 
35 namespace dodo::network {
36 
37  //====================================================================================================================
38  // X509Common
39  //====================================================================================================================
40 
41  X509Common::X509Type X509Common::detectX509Type( const std::string file, std::string &tag ) {
43  std::ifstream pem( file );
44  std::string head = "";
45  std::getline( pem, head );
46  std::regex re("-----BEGIN (.*)-----");
47  std::smatch match;
48  tag = "";
49  if (std::regex_search( head, match, re ) && match.size() > 1 ) {
50  tag = match.str(1);
51  }
52  if ( tag == "PRIVATE KEY" ) result = X509Common::X509Type::PrivateKey; else
53  if ( tag == "ENCRYPTED PRIVATE KEY" ) result = X509Common::X509Type::PrivateKey; else
54  if ( tag == "PUBLIC KEY" ) result = X509Common::X509Type::PublicKey; else
55  if ( tag == "CERTIFICATE REQUEST" ) result = X509Common::X509Type::CertificateSigningRequest; else
56  if ( tag == "CERTIFICATE" ) result = X509Common::X509Type::Certificate;
57 
58  return result;
59  }
60 
62  Identity identity;
63  //std::cout << "! " << src << endl;
64  std::vector<std::string> items = common::escapedSplit( src, {'\\'}, ',' );
65  for ( auto item : items ) {
66  std::vector<std::string> kvpair = common::split( item, '=' );
67  //std::cout << "!! " << kvpair[0] << endl;
68  if ( kvpair.size() == 2 ) {
69  if ( kvpair[0] == "C" ) identity.countryCode = kvpair[1];
70  else if ( kvpair[0] == "ST" ) identity.state = kvpair[1];
71  else if ( kvpair[0] == "L" ) identity.locality = kvpair[1];
72  else if ( kvpair[0] == "O" ) identity.organization = kvpair[1];
73  else if ( kvpair[0] == "OU" ) identity.organizationUnit = kvpair[1];
74  else if ( kvpair[0] == "CN" ) identity.commonName = kvpair[1];
75  else if ( kvpair[0] == "emailAddress" ) identity.email = kvpair[1];
76  else if ( kvpair[0] == "businessCategory" ) identity.businessCategory = kvpair[1];
77  else if ( kvpair[0] == "jurisdictionC" ) identity.jurisdictionC = kvpair[1];
78  else if ( kvpair[0] == "jurisdictionST" ) identity.jurisdictionST = kvpair[1];
79  else if ( kvpair[0] == "serialNumber" ) identity.serialNumber = kvpair[1];
80  else if ( kvpair[0] == "street" ) identity.street = kvpair[1];
81  else if ( kvpair[0] == "postalCode" ) identity.postalCode = kvpair[1];
82  else identity.other[ kvpair[0] ] = kvpair[1];
83  }
84  }
85  return identity;
86  }
87 
88  //====================================================================================================================
89  // X509CertificateSigningRequest
90  //====================================================================================================================
91 
92  X509_REQ* X509CertificateSigningRequest::loadPEM( const std::string file ) {
93  BIO* pembio = BIO_new( BIO_s_file() );
94  try {
95  if ( pembio == nullptr ) throw_Exception( "BIO_new( BIO_s_file() ) failed " +
96  common::getSSLErrors(';') );
97  int rc = BIO_read_filename( pembio, file.c_str() );
98  if ( !rc ) throw_Exception( "BIO_read_filename failed " +
99  common::getSSLErrors(';') );
100  X509_REQ* temp = PEM_read_bio_X509_REQ( pembio, nullptr, nullptr, nullptr );
101  if ( temp == nullptr ) throw_Exception( "PEM_read_bio_X509_AUX failed " +
102  common::getSSLErrors(';') );
103  BIO_free( pembio );
104  return temp;
105  }
106  catch ( common::Exception &e ) {
107  BIO_free( pembio );
108  throw;
109  }
110  }
111 
113  X509_NAME* name = X509_REQ_get_subject_name( cert );
114  BIO* output_bio = BIO_new( BIO_s_mem() );
115  X509_NAME_print_ex( output_bio, name, 0, XN_FLAG_RFC2253 );
116  std::string tmp = dodo::common::bio2String( output_bio );
117  BIO_free( output_bio );
118  return parseIdentity( tmp );
119  }
120 
121  std::string X509CertificateSigningRequest::getFingerPrint( const X509_REQ *cert, const std::string hashname ) {
122  std::stringstream ss;
123  ss << std::hex << std::setfill('0');
124  unsigned int hash_size;
125  unsigned char hash[EVP_MAX_MD_SIZE];
126  const EVP_MD * digest = EVP_get_digestbyname( hashname.c_str() );
127  if ( digest == nullptr ) throw_Exception( "EVP_get_digestbyname failed " +
128  common::getSSLErrors(';') );
129  int rc = X509_REQ_digest( cert, digest, hash, &hash_size);
130  if ( !rc ) throw_Exception( "X509_digest failed " +
131  common::getSSLErrors(';') );
132  for( unsigned int pos = 0; pos < hash_size; pos++ ) {
133  if ( pos ) ss << ":";
134  ss << std::setw(2) << (unsigned int)hash[pos];
135  }
136  return ss.str();
137  }
138 
139  //====================================================================================================================
140  // X509Certificate
141  //====================================================================================================================
142 
143  X509* X509Certificate::loadPEM( const std::string file ) {
144  BIO* pembio = BIO_new( BIO_s_file() );
145  try {
146  if ( pembio == nullptr ) throw_Exception( "BIO_new( BIO_s_file() ) failed " +
147  common::getSSLErrors(';') );
148  int rc = BIO_read_filename( pembio, file.c_str() );
149  if ( !rc ) throw_Exception( "BIO_read_filename failed " +
150  common::getSSLErrors(';') );
151  X509* temp = PEM_read_bio_X509_AUX( pembio, nullptr, nullptr, nullptr );
152  if ( temp == nullptr ) throw_Exception( "PEM_read_bio_X509_AUX failed " +
153  common::getSSLErrors(';') );
154  BIO_free( pembio );
155  return temp;
156  }
157  catch ( common::Exception &e ) {
158  BIO_free( pembio );
159  throw;
160  }
161  }
162 
164  X509_NAME* name = X509_get_issuer_name( cert );
165  BIO* output_bio = BIO_new( BIO_s_mem() );
166  X509_NAME_print_ex( output_bio, name, 0, XN_FLAG_RFC2253 );
167  std::string tmp = dodo::common::bio2String( output_bio );
168  BIO_free( output_bio );
169  return parseIdentity(tmp);
170  }
171 
172  std::string X509Certificate::getSerial( const X509 *cert ) {
173  const ASN1_INTEGER* val = X509_get0_serialNumber( cert );
174  BIGNUM *bnser = ASN1_INTEGER_to_BN( val, nullptr );
175  char *serialChar = BN_bn2hex(bnser);
176  std::string tmp = serialChar;
177  ::free( serialChar );
178  BN_free(bnser);
179  return tmp;
180  }
181 
183  X509_NAME* name = X509_get_subject_name( cert );
184  BIO* output_bio = BIO_new( BIO_s_mem() );
185  X509_NAME_print_ex( output_bio, name, 0, XN_FLAG_RFC2253 );
186  std::string tmp = dodo::common::bio2String( output_bio );
187  BIO_free( output_bio );
188  //std::cout << tmp << std::endl;
189  return parseIdentity( tmp );
190  }
191 
192  std::list<X509Certificate::SAN> X509Certificate::getSubjectAltNames( const X509* cert ) {
193  std::list<X509Certificate::SAN> result;
194  GENERAL_NAMES* subjectaltnames = (GENERAL_NAMES*)X509_get_ext_d2i( cert, NID_subject_alt_name, NULL, NULL);
195  if ( !subjectaltnames ) return result;
196  int altnamecount = sk_GENERAL_NAME_num(subjectaltnames);
197  for ( int i = 0; i < altnamecount; i++ ) {
198  GENERAL_NAME* generalname = sk_GENERAL_NAME_value( subjectaltnames, i) ;
199  if ( generalname ) {
200  if ( generalname->type == GEN_URI ||
201  generalname->type == GEN_DNS ||
202  generalname->type == GEN_EMAIL ) {
203  std::string san = std::string(reinterpret_cast<char*>(generalname->d.ia5->data));
204  result.push_back( { static_cast<X509Common::SANType>(generalname->type), san } );
205  } else if ( generalname->type == GEN_IPADD) {
206  unsigned char *data = generalname->d.ip->data;
207  if ( generalname->d.ip->length == 4 ) {
208  std::stringstream ip;
209  ip << (int)data[0] << '.' << (int)data[1] << '.' << (int)data[2] << '.' << (int)data[3];
210  result.push_back( { static_cast<X509Common::SANType>(generalname->type), ip.str() } );
211  } else {
212  const unsigned char *ipdata = ASN1_STRING_get0_data( generalname->d.iPAddress );
213  int datalen = ASN1_STRING_length( generalname->d.ia5 );
214  const unsigned char *p = ipdata;
215  std::stringstream ip;
216  for ( int j = 0; j < datalen/2 ; j++ ) {
217  if ( j > 0 ) ip << ":";
218  ip << std::hex << (int)(p[0] << 8 | p[1]);
219  p+=2;
220  }
221  result.push_back( { static_cast<X509Common::SANType>(generalname->type), ip.str() } );
222  }
223  }
224  }
225  }
226  if ( subjectaltnames ) sk_GENERAL_NAME_pop_free( subjectaltnames, GENERAL_NAME_free );
227  return result;
228  }
229 
230  std::string X509Certificate::getFingerPrint( const X509 *cert, const std::string hashname ) {
231  std::stringstream ss;
232  ss << std::hex << std::setfill('0') << std::uppercase;
233  unsigned int hash_size;
234  unsigned char hash[EVP_MAX_MD_SIZE];
235  const EVP_MD * digest = EVP_get_digestbyname( hashname.c_str() );
236  if ( digest == nullptr ) throw_Exception( "EVP_get_digestbyname failed " +
237  common::getSSLErrors(';') );
238  int rc = X509_digest( cert, digest, hash, &hash_size);
239  if ( !rc ) throw_Exception( "X509_digest failed " +
240  common::getSSLErrors(';') );
241  for( unsigned int pos = 0; pos < hash_size; pos++ ) {
242  if ( pos ) ss << ":";
243  ss << std::setw(2) << (unsigned int)hash[pos];
244  }
245  return ss.str();
246  }
247 
248  bool X509Certificate::verifyName( const std::string &peer, const std::string &san, bool allowwildcard ) {
249  if ( peer.length() < san.length() ) return false;
250  auto itr_peer = peer.rbegin();
251  auto itr_san = san.rbegin();
252  while ( itr_peer != peer.rend() && itr_san != san.rend() ) {
253  if ( *itr_peer != *itr_san ) {
254  if ( allowwildcard && *itr_san == '*' ) {
255  // verify there are no dots to the left
256  while ( itr_peer != peer.rend() ) {
257  if ( *itr_peer == '.' ) return false;
258  itr_peer++;
259  }
260  return true;
261  } else return false;
262  }
263  itr_peer++;
264  itr_san++;
265  }
266  return peer.length() == san.length();
267  }
268 
269  bool X509Certificate::verifyIP( const std::string &peer, const std::string &san ) {
270  network::Address addr_peer = peer;
271  network::Address addr_san = san;
272  if ( addr_peer.isValid() && addr_san.isValid() && addr_peer == addr_san )
273  return true;
274  else
275  return false;
276  }
277 
278  bool X509Certificate::verifySAN( const X509 *cert, const SAN &san, bool wildcards ) {
279  bool verified = false;
280  auto cert_sans = getSubjectAltNames( cert );
281  X509Common::Identity subject = getSubject( cert );
282  cert_sans.push_front( { san.san_type, subject.commonName } );
283  switch ( san.san_type ) {
284  case SANType::stDNS:
285  case SANType::stURI:
286  case SANType::stEMAIL:
287  verified = std::any_of( cert_sans.cbegin(),
288  cert_sans.cend(),
289  [san,wildcards](const SAN &s) -> bool { return s.san_type == san.san_type &&
290  verifyName( san.san_name, s.san_name, wildcards ); } );
291 
292  break;
293  case SANType::stIP:
294  verified = std::any_of( cert_sans.cbegin(),
295  cert_sans.cend(),
296  [san](const SAN &s) -> bool { return s.san_type == san.san_type &&
297  verifyIP( san.san_name, s.san_name ); } );
298  break;
299  }
300  return verified;
301  }
302 
303 }
dodo::common::split
std::vector< std::string > split(const std::string &src, char delimiter=' ')
Split a string into substrings.
Definition: util.hpp:136
dodo::network::X509Common::Identity::state
std::string state
The State or Province name.
Definition: x509cert.hpp:99
dodo::network::X509Common::SAN
Subject AltName record.
Definition: x509cert.hpp:70
tlscontext.hpp
dodo::network::X509Common::Identity
Attributes that together constitute a X509 identity.
Definition: x509cert.hpp:86
dodo::network::X509Common::Identity::serialNumber
std::string serialNumber
A cert serial number.
Definition: x509cert.hpp:144
dodo::network::X509Common::detectX509Type
static X509Type detectX509Type(const std::string file, std::string &tag)
Detects a X509 document type from a PEM file.
Definition: x509cert.cpp:41
dodo::network::Address::isValid
bool isValid() const
True if this Address is valid.
Definition: address.hpp:134
dodo::common::escapedSplit
std::vector< std::string > escapedSplit(const std::string &src, std::set< char > escape, char delimiter=' ')
Split a string into substrings by delimiter - unless the delimiter is escaped.
Definition: util.hpp:157
dodo::network::X509Certificate::free
static void free(X509 *cert)
Free / clean an X509 object.
Definition: x509cert.hpp:285
dodo::network::X509Common::X509Type::Certificate
@ Certificate
Certificate PEM document.
dodo::network::X509Certificate::getIssuer
static X509Common::Identity getIssuer(const X509 *cert)
Get the certificate issuer.
Definition: x509cert.cpp:163
dodo::network::X509CertificateSigningRequest::loadPEM
static X509_REQ * loadPEM(const std::string file)
Load a certificate signing request (CSR) from a PEM file.
Definition: x509cert.cpp:92
dodo::network::X509Certificate::loadPEM
static X509 * loadPEM(const std::string file)
Load a public key certificate (aka 'certificate') from a PEM file.
Definition: x509cert.cpp:143
dodo::network::X509Common::X509Type::Unknown
@ Unknown
Unknown PEM document.
dodo::network::X509Certificate::verifySAN
static bool verifySAN(const X509 *cert, const SAN &san, bool wildcards=false)
Verify a peer name against this certificate's CN and SubjectAltnames.
Definition: x509cert.cpp:278
dodo::network::X509Common::Identity::organizationUnit
std::string organizationUnit
The organizational unit name.
Definition: x509cert.hpp:114
dodo::network::Address
Generic network Address, supporting ipv4 and ipv6 transparently.
Definition: address.hpp:90
dodo::network::X509Common::SANType::stDNS
@ stDNS
A DNS name such as myhost.mydomain.org.
dodo::network::X509Common::Identity::street
std::string street
The street address.
Definition: x509cert.hpp:149
dodo::network::X509Common::Identity::businessCategory
std::string businessCategory
The businessCategory.
Definition: x509cert.hpp:129
dodo::network::X509Certificate::getFingerPrint
static std::string getFingerPrint(const X509 *cert, const std::string hashname="shake256")
Get the certificate fingerprint (a hash on the public key modulus) in string format,...
Definition: x509cert.cpp:230
dodo::network::X509Common::SANType::stIP
@ stIP
An IPv4 or IPv6 address.
dodo::network::X509Common::parseIdentity
static Identity parseIdentity(const std::string src)
Parse a subject or issuer string into an Identity.
Definition: x509cert.cpp:61
dodo::network::X509Common::Identity::countryCode
std::string countryCode
A two-character country code, for example NL for The Netherlands.
Definition: x509cert.hpp:94
dodo::network::X509Common::X509Type::PublicKey
@ PublicKey
Public key PEM document.
dodo::network::X509Common::Identity::jurisdictionC
std::string jurisdictionC
The jurisdiction country code.
Definition: x509cert.hpp:134
dodo::network::X509Common::Identity::postalCode
std::string postalCode
The postal code.
Definition: x509cert.hpp:154
dodo::network::X509Certificate::getSubject
static X509Common::Identity getSubject(const X509 *cert)
Get the certificate subject identity.
Definition: x509cert.cpp:182
dodo::network::X509Common::Identity::jurisdictionST
std::string jurisdictionST
The jurisdiction state.
Definition: x509cert.hpp:139
dodo::network::X509Certificate::verifyIP
static bool verifyIP(const std::string &peer, const std::string &san)
Verify a peer IP matches a SAN of type stIP.
Definition: x509cert.cpp:269
dodo::network::X509Common::SANType
SANType
The SubjectAltName type.
Definition: x509cert.hpp:45
address.hpp
dodo::network::X509Common::SAN::san_type
X509Common::SANType san_type
The type.
Definition: x509cert.hpp:74
throw_Exception
#define throw_Exception(what)
Throws an Exception, passes FILE and LINE to constructor.
Definition: exception.hpp:174
dodo::network::X509Common::SANType::stURI
@ stURI
An URI.
dodo::network::X509Common::Identity::organization
std::string organization
The organization name.
Definition: x509cert.hpp:109
dodo::network::X509Common::Identity::other
std::map< std::string, std::string > other
Other key-value pairs in the identity.
Definition: x509cert.hpp:159
dodo::network
Interface for network communication.
Definition: address.hpp:37
dodo::common::bio2String
std::string bio2String(BIO *bio)
Convert the data contents of an OpenSSL BIO to a std::string.
Definition: util.cpp:132
dodo::network::X509Common::SANType::stEMAIL
@ stEMAIL
An email address.
dodo::common::getSSLErrors
std::string getSSLErrors(char terminator)
Get all OpenSSL errors as a single string, and clear their error state.
Definition: util.cpp:154
dodo::network::X509Certificate::getSubjectAltNames
static std::list< X509Common::SAN > getSubjectAltNames(const X509 *cert)
Get the SAN (subject alternate name) list for the certificate, which may be empty.
Definition: x509cert.cpp:192
dodo::network::X509Common::Identity::locality
std::string locality
The locality name (city, town).
Definition: x509cert.hpp:104
exception.hpp
dodo::network::X509CertificateSigningRequest::getFingerPrint
static std::string getFingerPrint(const X509_REQ *cert, const std::string hashname="shake256")
Get the certificate fingerprint (a hash on the public key modulus) in string format,...
Definition: x509cert.cpp:121
dodo::network::X509Certificate::getSerial
static std::string getSerial(const X509 *cert)
Get the certificate serial number as concatenated hex bytes.
Definition: x509cert.cpp:172
dodo::network::X509Common::X509Type::CertificateSigningRequest
@ CertificateSigningRequest
CSR PEM document.
dodo::network::X509CertificateSigningRequest::getSubject
static X509Common::Identity getSubject(const X509_REQ *cert)
Get the CSR subject identity.
Definition: x509cert.cpp:112
x509cert.hpp
dodo::network::X509Common::Identity::commonName
std::string commonName
The common name.
Definition: x509cert.hpp:119
dodo::network::X509Common::Identity::email
std::string email
The email address.
Definition: x509cert.hpp:124
dodo::network::X509Common::X509Type::PrivateKey
@ PrivateKey
Private key PEM document (possibly encrypted).
dodo::network::X509Common::X509Type
X509Type
Enumeration of X509 document types.
Definition: x509cert.hpp:165
util.hpp
dodo::network::X509Certificate::verifyName
static bool verifyName(const std::string &peer, const std::string &san, bool wildcards=false)
Verify a peer name matches a SAN.
Definition: x509cert.cpp:248
dodo::common::Exception
An Exception is thrown in exceptional circumstances, and its occurrence should generally imply that t...
Definition: exception.hpp:83