Skip to content

Commit

Permalink
PROTON-1088 - Added two functions pn_ssl_get_cert_fingerprint() and p…
Browse files Browse the repository at this point in the history
…n_ssl_get_remote_subject_subfield() to obtain certificate fingerprint and certificate subject subfields respectively
  • Loading branch information
ganeshmurthy committed Jan 18, 2016
1 parent ba108cf commit 463cfd7
Show file tree
Hide file tree
Showing 12 changed files with 389 additions and 1 deletion.
7 changes: 7 additions & 0 deletions proton-c/bindings/python/cproton.i
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,12 @@ bool pn_ssl_get_cipher_name(pn_ssl_t *ssl, char *OUTPUT, size_t MAX_OUTPUT_SIZE)
bool pn_ssl_get_protocol_name(pn_ssl_t *ssl, char *OUTPUT, size_t MAX_OUTPUT_SIZE);
%ignore pn_ssl_get_protocol_name;

char* pn_ssl_get_remote_subject_subfield(pn_ssl_t *ssl, pn_ssl_cert_subject_subfield field);
%ignore pn_ssl_get_remote_subject_subfield;

int pn_ssl_get_cert_fingerprint(pn_ssl_t *ssl, char *OUTPUT, size_t MAX_OUTPUT_SIZE, pn_ssl_hash_alg hash_alg);
%ignore pn_ssl_get_cert_fingerprint;

%rename(pn_ssl_get_peer_hostname) wrap_pn_ssl_get_peer_hostname;
%inline %{
int wrap_pn_ssl_get_peer_hostname(pn_ssl_t *ssl, char *VTEXT_OUT, size_t *VTEXT_SIZE) {
Expand All @@ -243,6 +249,7 @@ bool pn_ssl_get_protocol_name(pn_ssl_t *ssl, char *OUTPUT, size_t MAX_OUTPUT_SIZ
%}
%ignore pn_ssl_get_peer_hostname;


%immutable PN_PYREF;
%inline %{
extern const pn_class_t *PN_PYREF;
Expand Down
67 changes: 67 additions & 0 deletions proton-c/bindings/python/proton/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3627,6 +3627,73 @@ def protocol_name(self):
return name
return None

SHA1 = PN_SSL_SHA1
SHA256 = PN_SSL_SHA256
SHA512 = PN_SSL_SHA512
MD5 = PN_SSL_MD5

CERT_COUNTRY_NAME = PN_SSL_CERT_SUBJECT_COUNTRY_NAME
CERT_STATE_OR_PROVINCE = PN_SSL_CERT_SUBJECT_STATE_OR_PROVINCE
CERT_CITY_OR_LOCALITY = PN_SSL_CERT_SUBJECT_CITY_OR_LOCALITY
CERT_ORGANIZATION_NAME = PN_SSL_CERT_SUBJECT_ORGANIZATION_NAME
CERT_ORGANIZATION_UNIT = PN_SSL_CERT_SUBJECT_ORGANIZATION_UNIT
CERT_COMMON_NAME = PN_SSL_CERT_SUBJECT_COMMON_NAME

def get_cert_subject_subfield(self, subfield_name):
subfield_value = pn_ssl_get_remote_subject_subfield(self._ssl, subfield_name)
return subfield_value

def get_cert_subject(self):
subject = pn_ssl_get_remote_subject(self._ssl)
return subject

def _get_cert_subject_unknown_subfield(self):
# Pass in an unhandled enum
return self.get_cert_subject_subfield(10)

# Convenience functions for obtaining the subfields of the subject field.
def get_cert_common_name(self):
return self.get_cert_subject_subfield(SSL.CERT_COMMON_NAME)

def get_cert_organization(self):
return self.get_cert_subject_subfield(SSL.CERT_ORGANIZATION_NAME)

def get_cert_organization_unit(self):
return self.get_cert_subject_subfield(SSL.CERT_ORGANIZATION_UNIT)

def get_cert_locality_or_city(self):
return self.get_cert_subject_subfield(SSL.CERT_CITY_OR_LOCALITY)

def get_cert_country(self):
return self.get_cert_subject_subfield(SSL.CERT_COUNTRY_NAME)

def get_cert_state_or_province(self):
return self.get_cert_subject_subfield(SSL.CERT_STATE_OR_PROVINCE)

def get_cert_fingerprint(self, fingerprint_length, digest_name):
rc, fingerprint_str = pn_ssl_get_cert_fingerprint(self._ssl, fingerprint_length, digest_name)
if rc == PN_OK:
return fingerprint_str
return None

# Convenience functions for obtaining fingerprint for specific hashing algorithms
def _get_cert_fingerprint_unknown_hash_alg(self):
return self.get_cert_fingerprint(41, 10)

def get_cert_fingerprint_sha1(self):
return self.get_cert_fingerprint(41, SSL.SHA1)

def get_cert_fingerprint_sha256(self):
# sha256 produces a fingerprint that is 64 characters long
return self.get_cert_fingerprint(65, SSL.SHA256)

def get_cert_fingerprint_sha512(self):
# sha512 produces a fingerprint that is 128 characters long
return self.get_cert_fingerprint(129, SSL.SHA512)

def get_cert_fingerprint_md5(self):
return self.get_cert_fingerprint(33, SSL.MD5)

@property
def remote_subject(self):
return pn_ssl_get_remote_subject( self._ssl )
Expand Down
55 changes: 55 additions & 0 deletions proton-c/include/proton/ssl.h
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,61 @@ PN_EXTERN int pn_ssl_get_peer_hostname( pn_ssl_t *ssl, char *hostname, size_t *b
*/
PN_EXTERN const char* pn_ssl_get_remote_subject(pn_ssl_t *ssl);

/**
* Enumeration identifying the sub fields of the subject field in the ssl certificate.
*/
typedef enum {
PN_SSL_CERT_SUBJECT_COUNTRY_NAME,
PN_SSL_CERT_SUBJECT_STATE_OR_PROVINCE,
PN_SSL_CERT_SUBJECT_CITY_OR_LOCALITY,
PN_SSL_CERT_SUBJECT_ORGANIZATION_NAME,
PN_SSL_CERT_SUBJECT_ORGANIZATION_UNIT,
PN_SSL_CERT_SUBJECT_COMMON_NAME
} pn_ssl_cert_subject_subfield;


/**
* Enumeration identifying hashing algorithm.
*/
typedef enum {
PN_SSL_SHA1, /* Produces hash that is 20 bytes long */
PN_SSL_SHA256, /* Produces hash that is 32 bytes long */
PN_SSL_SHA512, /* Produces hash that is 64 bytes long */
PN_SSL_MD5 /* Produces hash that is 16 bytes long */
} pn_ssl_hash_alg;

/**
* Get the fingerprint of the certificate. The certificate fingerprint (as displayed in the Fingerprints section when
* looking at a certificate with say the Firefox browser) is the hexadecimal hash of the entire certificate.
* The fingerprint is not part of the certificate, rather it is computed from the certificate and can be used to uniquely identify a certificate.
* @param[in] ssl the ssl client/server to query
* @param[in] fingerprint char pointer. The certificate fingerprint (in hex format) will be populated in this array.
* If sha1 is the digest name, the fingerprint is 41 characters long (40 + 1 '\0' character), 65 characters long for
* sha256 and 129 characters long for sha512 and 33 characters for md5.
* @param[in] fingerprint_length - Must be at >= 33 for md5, >= 41 for sha1, >= 65 for sha256 and >=129 for sha512.
* @param[in] the hash algorithm to use. Must be of type pn_ssl_hash_alg (currently supports sha1, sha256, sha512 and md5)
* @return error code - Returns 0 on success. Return a value less than zero if there were any errors. Upon execution of this function,
* char *fingerprint will contain the appropriate null terminated hex fingerprint
*/
PN_EXTERN int pn_ssl_get_cert_fingerprint(pn_ssl_t *ssl0,
char *fingerprint,
size_t fingerprint_length,
pn_ssl_hash_alg hash_alg);

/**
* Returns a char pointer that contains the value of the sub field of the subject field in the ssl certificate. The subject field usually contains the following sub fields -
* C = ISO3166 two character country code
* ST = state or province
* L = Locality; generally means city
* O = Organization - Company Name
* OU = Organization Unit - division or unit
* CN = CommonName
* @param[in] ssl the ssl client/server to query
* @param[in] The enumeration pn_ssl_cert_subject_subfield representing the required sub field.
* @return A null terminated string which contains the requested sub field value which is valid until the ssl object is destroyed.
*/
PN_EXTERN const char* pn_ssl_get_remote_subject_subfield(pn_ssl_t *ssl0, pn_ssl_cert_subject_subfield field);

/** @} */

#ifdef __cplusplus
Expand Down
122 changes: 122 additions & 0 deletions proton-c/src/ssl/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,128 @@ const char* pn_ssl_get_remote_subject(pn_ssl_t *ssl0)
return ssl->subject;
}

int pn_ssl_get_cert_fingerprint(pn_ssl_t *ssl0, char *fingerprint, size_t fingerprint_length, pn_ssl_hash_alg hash_alg)
{
const char *digest_name = NULL;
size_t min_required_length;

// old versions of python expect fingerprint to contain a valid string on
// return from this function
fingerprint[0] = 0;

// Assign the correct digest_name value based on the enum values.
switch (hash_alg) {
case PN_SSL_SHA1 :
min_required_length = 41; // 40 hex characters + 1 '\0' character
digest_name = "sha1";
break;
case PN_SSL_SHA256 :
min_required_length = 65; // 64 hex characters + 1 '\0' character
digest_name = "sha256";
break;
case PN_SSL_SHA512 :
min_required_length = 129; // 128 hex characters + 1 '\0' character
digest_name = "sha512";
break;
case PN_SSL_MD5 :
min_required_length = 33; // 32 hex characters + 1 '\0' character
digest_name = "md5";
break;
default:
ssl_log_error("Unknown or unhandled hash algorithm %i \n", hash_alg);
return PN_ERR;

}

if(fingerprint_length < min_required_length) {
ssl_log_error("Insufficient fingerprint_length %i. fingerprint_length must be %i or above for %s digest\n",
fingerprint_length, min_required_length, digest_name);
return PN_ERR;
}

const EVP_MD *digest = EVP_get_digestbyname(digest_name);

pni_ssl_t *ssl = get_ssl_internal(ssl0);

X509 *cert = SSL_get_peer_certificate(ssl->ssl);

if(cert) {
unsigned int len;

unsigned char bytes[64]; // sha512 uses 64 octets, we will use that as the maximum.

if (X509_digest(cert, digest, bytes, &len) != 1) {
ssl_log_error("Failed to extract X509 digest\n");
return PN_ERR;
}

char *cursor = fingerprint;

for (size_t i=0; i<len ; i++) {
cursor += snprintf((char *)cursor, fingerprint_length, "%02x", bytes[i]);
fingerprint_length = fingerprint_length - 2;
}

return PN_OK;
}
else {
ssl_log_error("No certificate is available yet \n");
return PN_ERR;
}

return 0;
}


const char* pn_ssl_get_remote_subject_subfield(pn_ssl_t *ssl0, pn_ssl_cert_subject_subfield field)
{
int openssl_field = 0;

// Assign openssl internal representations of field values to openssl_field
switch (field) {
case PN_SSL_CERT_SUBJECT_COUNTRY_NAME :
openssl_field = NID_countryName;
break;
case PN_SSL_CERT_SUBJECT_STATE_OR_PROVINCE :
openssl_field = NID_stateOrProvinceName;
break;
case PN_SSL_CERT_SUBJECT_CITY_OR_LOCALITY :
openssl_field = NID_localityName;
break;
case PN_SSL_CERT_SUBJECT_ORGANIZATION_NAME :
openssl_field = NID_organizationName;
break;
case PN_SSL_CERT_SUBJECT_ORGANIZATION_UNIT :
openssl_field = NID_organizationalUnitName;
break;
case PN_SSL_CERT_SUBJECT_COMMON_NAME :
openssl_field = NID_commonName;
break;
default:
ssl_log_error("Unknown or unhandled certificate subject subfield %i \n", field);
return NULL;
}

pni_ssl_t *ssl = get_ssl_internal(ssl0);
X509 *cert = SSL_get_peer_certificate(ssl->ssl);

X509_NAME *subject_name = X509_get_subject_name(cert);

// TODO (gmurthy) - A server side cert subject field can have more than one common name like this - Subject: CN=www.domain1.com, CN=www.domain2.com, see https://bugzilla.mozilla.org/show_bug.cgi?id=380656
// For now, we will only return the first common name if there is more than one common name in the cert
int index = X509_NAME_get_index_by_NID(subject_name, openssl_field, -1);

if (index > -1) {
X509_NAME_ENTRY *ne = X509_NAME_get_entry(subject_name, index);
if(ne) {
ASN1_STRING *name_asn1 = X509_NAME_ENTRY_get_data(ne);
return (char *) name_asn1->data;
}
}

return NULL;
}

static ssize_t process_input_done(pn_transport_t *transport, unsigned int layer, const char *input_data, size_t len)
{
return PN_EOS;
Expand Down
12 changes: 12 additions & 0 deletions proton-j/src/main/resources/cssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@
PN_SSL_ANONYMOUS_PEER=2
PN_SSL_VERIFY_PEER_NAME=3

PN_SSL_SHA1=0
PN_SSL_SHA256=1
PN_SSL_SHA512=2
PN_SSL_MD5=3

PN_SSL_CERT_SUBJECT_COUNTRY_NAME=0
PN_SSL_CERT_SUBJECT_STATE_OR_PROVINCE=1
PN_SSL_CERT_SUBJECT_CITY_OR_LOCALITY=2
PN_SSL_CERT_SUBJECT_ORGANIZATION_NAME=3
PN_SSL_CERT_SUBJECT_ORGANIZATION_UNIT=4
PN_SSL_CERT_SUBJECT_COMMON_NAME=5

PN_SSL_MODE_J2P = {
SslDomain.Mode.CLIENT: PN_SSL_MODE_CLIENT,
SslDomain.Mode.SERVER: PN_SSL_MODE_SERVER
Expand Down
60 changes: 59 additions & 1 deletion tests/python/proton_tests/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import random
import string
import subprocess

import sys
from proton import *
from .common import Skipped, pump

Expand Down Expand Up @@ -185,6 +185,63 @@ def test_server_authentication(self):
server.connection.close()
self._pump( client, server )

def test_certificate_fingerprint_and_subfields(self):
if "java" in sys.platform:
raise Skipped("Not yet implemented in Proton-J")

self.server_domain.set_credentials(self._testpath("server-certificate.pem"),
self._testpath("server-private-key.pem"),
"server-password")
self.server_domain.set_trusted_ca_db(self._testpath("ca-certificate.pem"))
self.server_domain.set_peer_authentication( SSLDomain.VERIFY_PEER,
self._testpath("ca-certificate.pem") )
server = SslTest.SslTestConnection( self.server_domain, mode=Transport.SERVER )

# give the client a certificate, but let's not require server authentication
self.client_domain.set_credentials(self._testpath("client-certificate1.pem"),
self._testpath("client-private-key1.pem"),
"client-password")
self.client_domain.set_peer_authentication( SSLDomain.ANONYMOUS_PEER )
client = SslTest.SslTestConnection( self.client_domain )

client.connection.open()
server.connection.open()
self._pump( client, server )

# Test the subject subfields
self.assertEqual("Client", server.ssl.get_cert_organization())
self.assertEqual("Dev", server.ssl.get_cert_organization_unit())
self.assertEqual("ST", server.ssl.get_cert_state_or_province())
self.assertEqual("US", server.ssl.get_cert_country())
self.assertEqual("City", server.ssl.get_cert_locality_or_city())
self.assertEqual("O=Server,CN=A1.Good.Server.domain.com", client.ssl.get_cert_subject())
self.assertEqual("O=Client,CN=127.0.0.1,C=US,ST=ST,L=City,OU=Dev", server.ssl.get_cert_subject())

self.assertEqual("03c97341abafe5861d6969c66a190d2846d905d6", server.ssl.get_cert_fingerprint_sha1())
self.assertEqual("ad86cc76278e69aef3a5c0dda13fc49831622f92d1364ce1ed361ff842b0143a", server.ssl.get_cert_fingerprint_sha256())
self.assertEqual("9fb3340ecee4471534d60be025358cae33ef2cc9442ca8bb7703a68db68d9ffb7963678292996011fa55a9a2524b84a40a11a2778f25797e78e23cf05623218d",
server.ssl.get_cert_fingerprint_sha512())
self.assertEqual("0d4faa84a0bb6846eaec6b7493916b30", server.ssl.get_cert_fingerprint_md5())

# Test the various fingerprint algorithms
self.assertEqual("f390ddb4dc8a90bcf3528774b622a7f87f7322b5", client.ssl.get_cert_fingerprint_sha1())
self.assertEqual("c116e902345c99bc01dda14b7a5f1b8ae6a451eddb23e5336c996ba4d12bc122", client.ssl.get_cert_fingerprint_sha256())
self.assertEqual("8941c8ce00824ab7196bb1952787c90ef7f9bd837cbb0bb4823f57fc89e80033c75adc98b78801928d0035bcd6db6ddc9ab6da026c6548a66ede5c4f43f7e166",
client.ssl.get_cert_fingerprint_sha512())
self.assertEqual("d008bf05afbc983a3f98ae56e3eba643", client.ssl.get_cert_fingerprint_md5())

self.assertEqual(None, client.ssl.get_cert_fingerprint(21, SSL.SHA1)) # Should be at least 41
self.assertEqual(None, client.ssl.get_cert_fingerprint(50, SSL.SHA256)) # Should be at least 65
self.assertEqual(None, client.ssl.get_cert_fingerprint(128, SSL.SHA512)) # Should be at least 129
self.assertEqual(None, client.ssl.get_cert_fingerprint(10, SSL.MD5)) # Should be at least 33
self.assertEqual(None, client.ssl._get_cert_subject_unknown_subfield())

self.assertNotEqual(None, client.ssl.get_cert_fingerprint(50, SSL.SHA1)) # Should be at least 41
self.assertNotEqual(None, client.ssl.get_cert_fingerprint(70, SSL.SHA256)) # Should be at least 65
self.assertNotEqual(None, client.ssl.get_cert_fingerprint(130, SSL.SHA512)) # Should be at least 129
self.assertNotEqual(None, client.ssl.get_cert_fingerprint(35, SSL.MD5)) # Should be at least 33
self.assertEqual(None, client.ssl._get_cert_fingerprint_unknown_hash_alg())

def test_client_authentication(self):
""" Force the client to authenticate.
"""
Expand All @@ -208,6 +265,7 @@ def test_client_authentication(self):
client.connection.open()
server.connection.open()
self._pump( client, server )

assert client.ssl.protocol_name() is not None
client.connection.close()
server.connection.close()
Expand Down

0 comments on commit 463cfd7

Please sign in to comment.