From 6a4f65e4a0b34889b7057eb6b7b7bfa7d08ee10a Mon Sep 17 00:00:00 2001 From: Matt Jones Date: Sun, 28 Sep 2014 18:17:56 -0800 Subject: [PATCH 1/4] Implement functions to get X509 certificate validity fields. Implemented as PKI.get.notBefore() and PKI.get.notAfter(), with associated C-level functions that read the ASN.1 binary date representation and convert it to time_t to pass forward to R. --- NAMESPACE | 4 +- R/x509.R | 16 ++++++++ src/asn1.c | 108 +++++++++++++++++++++++++++++++++++++++++++++++++ src/asn1.h | 4 ++ src/pki-x509.c | 30 ++++++++++++++ src/pki.h | 1 + 6 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/asn1.h diff --git a/NAMESPACE b/NAMESPACE index f03dd7d..dab53d2 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,3 +1,3 @@ -useDynLib(PKI, PKI_load_DER_X509, PKI_verify_cert, PKI_cert_public_key, PKI_encrypt, PKI_decrypt, PKI_load_private_RSA, PKI_load_public_RSA, PKI_RSAkeygen, PKI_sign_RSA, PKI_verify_RSA, PKI_digest, PKI_extract_key, PKI_raw2hex, encode_ASN1, decode_ASN1, PKI_asBIGNUMint, PKI_sym_cipher, PKI_get_subject) -export(PKI.load.cert, PKI.verifyCA, PKI.verify, PKI.sign, PKI.pubkey, PKI.encrypt, PKI.decrypt, PKI.genRSAkey, PKI.digest, PKI.save.key, PKI.load.key, raw2hex, ASN1.encode, ASN1.decode, ASN1.item, ASN1.type, as.BIGNUMint, PKI.mkRSApubkey, PKI.load.OpenSSH.pubkey, PKI.sign.tar, PKI.verify.tar, PKI.get.subject) +useDynLib(PKI, PKI_load_DER_X509, PKI_verify_cert, PKI_cert_public_key, PKI_encrypt, PKI_decrypt, PKI_load_private_RSA, PKI_load_public_RSA, PKI_RSAkeygen, PKI_sign_RSA, PKI_verify_RSA, PKI_digest, PKI_extract_key, PKI_raw2hex, encode_ASN1, decode_ASN1, PKI_asBIGNUMint, PKI_sym_cipher, PKI_get_subject, PKI_get_notBefore, PKI_get_notAfter) +export(PKI.load.cert, PKI.verifyCA, PKI.verify, PKI.sign, PKI.pubkey, PKI.encrypt, PKI.decrypt, PKI.genRSAkey, PKI.digest, PKI.save.key, PKI.load.key, raw2hex, ASN1.encode, ASN1.decode, ASN1.item, ASN1.type, as.BIGNUMint, PKI.mkRSApubkey, PKI.load.OpenSSH.pubkey, PKI.sign.tar, PKI.verify.tar, PKI.get.subject, PKI.get.notBefore, PKI.get.notAfter) import(base64enc) diff --git a/R/x509.R b/R/x509.R index 1c4286b..15ef7a7 100644 --- a/R/x509.R +++ b/R/x509.R @@ -25,3 +25,19 @@ PKI.verifyCA <- function(certificate, ca) .Call(PKI_verify_cert, ca, certificate PKI.pubkey <- function(certificate) .Call(PKI_cert_public_key, certificate) PKI.get.subject <- function(certificate) .Call(PKI_get_subject, certificate) + +PKI.get.notBefore <- function(certificate) { + time_real <- .Call(PKI_get_notBefore, certificate) + + # To do the conversion in R, we would use + # t <- strptime(aTime, format="%y%m%d%H%M%SZ", tz="GMT") + # But that hardcodes the ASN1_TIME format, which won't always be correct + + return(as.POSIXct(time_real, tz="GMT", origin="1970-01-01")) +} + +PKI.get.notAfter <- function(certificate) { + time_real <- .Call(PKI_get_notAfter, certificate) + return(as.POSIXct(time_real, tz="GMT", origin="1970-01-01")) +} + diff --git a/src/asn1.c b/src/asn1.c index 8c665df..1b71253 100644 --- a/src/asn1.c +++ b/src/asn1.c @@ -1,6 +1,7 @@ #include #include +#include "asn1.h" static SEXP decode_ASN1_bytes(unsigned char *d, unsigned int l, unsigned int *ptr) { unsigned int i = 0; @@ -269,3 +270,110 @@ SEXP PKI_asBIGNUMint(SEXP sWhat, SEXP sScalar) { /* unreachable */ return R_NilValue; } + +/* +* Parse an ASN1_TIME value and return a time_t structure. According to RFC 5280 (http://www.rfc-editor.org/rfc/rfc5280.txt) +* conforming implementations "MUST always encode certificate validity dates through the year 2049 as UTCTime; +* certificate validity dates in 2050 or later MUST be encoded as GeneralizedTime." See section 4.1 of RFC 5280 for the +* definition of UTCTime and Generalized time, but the crux is that UTCTime uses a two digit year, whereas GeneralizedTime +* uses a 4 digit year. +* +* This algorithm is based on parsing the character +* string in ASN1_TIME and extracting the date and time components based on length and results in the string. It +* is derived from the publicly posted algorithm here: http://marc.info/?l=openssl-users&m=106781789300592&w=2 +* I use timegm() rather than mktime() to do the conversion to time_t in order to not assume a local time zone during +* the conversion, but its not clear if timegm() is portable to Windows. Need to look into that, +* possibly proving an implementation for Windows as outlined here: +* http://trac.rtmpd.com/browser/trunk/sources/common/src/platform/windows/timegm.cpp +*/ +time_t getTimeFromASN1(const ASN1_TIME * aTime) { + + time_t lResult = 0; + + char lBuffer[24]; + char * pBuffer = lBuffer; + + size_t lTimeLength = aTime->length; + + char * pString = (char *)aTime->data; + + if (aTime->type == V_ASN1_UTCTIME) { + if ((lTimeLength < 11) || (lTimeLength > 17)) { + return 0; + } + + memcpy(pBuffer, pString, 10); + pBuffer += 10; + pString += 10; + } else { + if (lTimeLength < 13) { + return 0; + } + + memcpy(pBuffer, pString, 12); + pBuffer += 12; + pString += 12; + } + + if ((*pString == 'Z') || (*pString == '-') || (*pString == '+')) { + *(pBuffer++) = '0'; + *(pBuffer++) = '0'; + } else { + *(pBuffer++) = *(pString++); + *(pBuffer++) = *(pString++); + // Skip any fractional seconds... + if (*pString == '.') { + pString++; + while ((*pString >= '0') && (*pString <= '9')) { + pString++; + } + } + } + + *(pBuffer++) = 'Z'; + *(pBuffer++) = '\0'; + + time_t lSecondsFromUCT; + if (*pString == 'Z') { + lSecondsFromUCT = 0; + } else { + if ((*pString != '+') && (pString[5] != '-')) { + return 0; + } + + lSecondsFromUCT = ((pString[1]-'0') * 10 + (pString[2]-'0')) * 60; + lSecondsFromUCT += (pString[3]-'0') * 10 + (pString[4]-'0'); + if (*pString == '-') { + lSecondsFromUCT = -lSecondsFromUCT; + } + } + + struct tm lTime; + lTime.tm_sec = ((lBuffer[10] - '0') * 10) + (lBuffer[11] - '0'); + lTime.tm_min = ((lBuffer[8] - '0') * 10) + (lBuffer[9] - '0'); + lTime.tm_hour = ((lBuffer[6] - '0') * 10) + (lBuffer[7] - '0'); + lTime.tm_mday = ((lBuffer[4] - '0') * 10) + (lBuffer[5] - '0'); + lTime.tm_mon = (((lBuffer[2] - '0') * 10) + (lBuffer[3] - '0')) - 1; + lTime.tm_year = ((lBuffer[0] - '0') * 10) + (lBuffer[1] - '0'); + if (lTime.tm_year < 50) { + lTime.tm_year += 100; + // RFC 2459 + } + lTime.tm_wday = 0; + lTime.tm_yday = 0; + lTime.tm_isdst = 0; + + // No DST adjustment requested + lResult = timegm(&lTime); + if ((time_t)-1 != lResult) { + if (0 != lTime.tm_isdst) { + lResult -= 3600; + // mktime may adjust for DST (OS dependent) + } + lResult += lSecondsFromUCT; + } else { + lResult = 0; + } + + return lResult; +} diff --git a/src/asn1.h b/src/asn1.h new file mode 100644 index 0000000..f24c9c4 --- /dev/null +++ b/src/asn1.h @@ -0,0 +1,4 @@ +#include + +/* Method prototypes */ +time_t getTimeFromASN1(const ASN1_TIME * aTime); \ No newline at end of file diff --git a/src/pki-x509.c b/src/pki-x509.c index 0b4f004..67b830c 100644 --- a/src/pki-x509.c +++ b/src/pki-x509.c @@ -611,3 +611,33 @@ SEXP PKI_get_subject(SEXP sCert) { BIO_free(mem); return res; } + +// Return the notBefore date of an X509 Certificate by wrapping the OpenSSL X509_get_notBefore() function. +SEXP PKI_get_notBefore(SEXP sCert) { + X509 *cert; + PKI_init(); + cert = retrieve_cert(sCert, ""); + ASN1_TIME *timeASN1; + timeASN1 = X509_get_notBefore(cert); + time_t result_time; + result_time = getTimeFromASN1(timeASN1); + SEXP *timeSEXP; + timeSEXP = PROTECT(ScalarReal(result_time)); + UNPROTECT(1); + return timeSEXP; +} + +// Return the notAfter date of an X509 Certificate by wrapping the OpenSSL X509_get_notAfter() function. +SEXP PKI_get_notAfter(SEXP sCert) { + X509 *cert; + PKI_init(); + cert = retrieve_cert(sCert, ""); + ASN1_TIME *timeASN1; + timeASN1 = X509_get_notAfter(cert); + time_t result_time; + result_time = getTimeFromASN1(timeASN1); + SEXP *timeSEXP; + timeSEXP = PROTECT(ScalarReal(result_time)); + UNPROTECT(1); + return timeSEXP; +} \ No newline at end of file diff --git a/src/pki.h b/src/pki.h index 260fd68..8d8fa9a 100644 --- a/src/pki.h +++ b/src/pki.h @@ -15,6 +15,7 @@ #include #include #include +#include "asn1.h" #if __APPLE__ #if defined MAC_OS_X_VERSION_10_7 && MAC_OS_X_VERSION_MIN_REQUIRED >= 1070 From 33d60119f3b4b349a188a80bcf1aa14939c70675 Mon Sep 17 00:00:00 2001 From: Matt Jones Date: Sun, 28 Sep 2014 22:11:09 -0800 Subject: [PATCH 2/4] Fixed spelling error. --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 88e8fa2..56d36dc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -5,7 +5,7 @@ Author: Simon Urbanek Maintainer: Simon Urbanek Depends: R (>= 2.9.0), base64enc Enhances: gmp -Description: This package provides PKI functions such as verifyig certificates, RSA encription and signing which can be used to build PKI infrastructure and perform cryptographic tasks. +Description: This package provides PKI functions such as verifying certificates, RSA encription and signing which can be used to build PKI infrastructure and perform cryptographic tasks. License: GPL-2 | GPL-3 | file LICENSE URL: http://www.rforge.net/PKI SystemRequirements: OpenSSL library From 5ef92f91584fe11d9e78e77488f5e4586ab6b28b Mon Sep 17 00:00:00 2001 From: Matt Jones Date: Sun, 28 Sep 2014 22:12:30 -0800 Subject: [PATCH 3/4] Documentation completed, and checks pass. Added documentation for new methods PKI.get.notBefore() and PKI.get.notAfter(), and ensured R CMD check passes. --- man/X509.Rd | 19 +++++++++++++++++++ src/asn1.h | 2 +- src/pki-x509.c | 10 +++++----- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/man/X509.Rd b/man/X509.Rd index cc1475b..10abe68 100644 --- a/man/X509.Rd +++ b/man/X509.Rd @@ -4,6 +4,8 @@ \alias{PKI.verifyCA} \alias{PKI.pubkey} \alias{PKI.get.subject} +\alias{PKI.get.notBefore} +\alias{PKI.get.notAfter} \title{ Public Key Instraftructure (X509) functions } @@ -17,12 +19,19 @@ \code{PKI.pubkey} extracts public key from a certificate. \code{PKI.get.subject} extracts the subject name from the certificate. + + \code{PKI.get.notBefore} extracts the notBefore field from the certificate. + + \code{PKI.get.notAfter} extracts the notAfter field from the certificate. + } \usage{ PKI.load.cert(what, format = c("PEM", "DER"), file) PKI.verifyCA(certificate, ca) PKI.pubkey(certificate) PKI.get.subject(certificate) +PKI.get.notBefore(certificate) +PKI.get.notAfter(certificate) } \arguments{ \item{what}{string, raw vector or connection to load the certificate @@ -49,6 +58,14 @@ PKI.get.subject(certificate) one-line RFC2253 format but in UTF8 encoding instead of MBS escapes. NOTE: this is experimantal, we may choose to parse the contents and return it in native R form as a named vector instead. + + \code{PKI.get.notBefore}: POSIXct value containing the datetime 'notBefore' + field fromt the certificate. This represents the earliest time at which the + certificate is valid. + + \code{PKI.get.notAfter}: POSIXct value containing the datetime 'notAfter' + field fromt the certificate. This represents the latest time at which the + certificate is valid. } %\references{ %} @@ -65,5 +82,7 @@ PKI.get.subject(certificate) PKI.verifyCA(my.cert, ca) PKI.pubkey(my.cert) PKI.get.subject(my.cert) + PKI.get.notBefore(my.cert) + PKI.get.notAfter(my.cert) } \keyword{manip} diff --git a/src/asn1.h b/src/asn1.h index f24c9c4..277a623 100644 --- a/src/asn1.h +++ b/src/asn1.h @@ -1,4 +1,4 @@ #include /* Method prototypes */ -time_t getTimeFromASN1(const ASN1_TIME * aTime); \ No newline at end of file +time_t getTimeFromASN1(const ASN1_TIME * aTime); diff --git a/src/pki-x509.c b/src/pki-x509.c index 67b830c..9d809f2 100644 --- a/src/pki-x509.c +++ b/src/pki-x509.c @@ -612,7 +612,7 @@ SEXP PKI_get_subject(SEXP sCert) { return res; } -// Return the notBefore date of an X509 Certificate by wrapping the OpenSSL X509_get_notBefore() function. +/* Return the notBefore date of an X509 Certificate by wrapping the OpenSSL X509_get_notBefore() function. */ SEXP PKI_get_notBefore(SEXP sCert) { X509 *cert; PKI_init(); @@ -621,13 +621,13 @@ SEXP PKI_get_notBefore(SEXP sCert) { timeASN1 = X509_get_notBefore(cert); time_t result_time; result_time = getTimeFromASN1(timeASN1); - SEXP *timeSEXP; + SEXP timeSEXP; timeSEXP = PROTECT(ScalarReal(result_time)); UNPROTECT(1); return timeSEXP; } -// Return the notAfter date of an X509 Certificate by wrapping the OpenSSL X509_get_notAfter() function. +/* Return the notAfter date of an X509 Certificate by wrapping the OpenSSL X509_get_notAfter() function. */ SEXP PKI_get_notAfter(SEXP sCert) { X509 *cert; PKI_init(); @@ -636,8 +636,8 @@ SEXP PKI_get_notAfter(SEXP sCert) { timeASN1 = X509_get_notAfter(cert); time_t result_time; result_time = getTimeFromASN1(timeASN1); - SEXP *timeSEXP; + SEXP timeSEXP; timeSEXP = PROTECT(ScalarReal(result_time)); UNPROTECT(1); return timeSEXP; -} \ No newline at end of file +} From 2c3de0836894e55175a59a39098c66123b2700ac Mon Sep 17 00:00:00 2001 From: Matt Jones Date: Thu, 9 Oct 2014 16:34:56 -0800 Subject: [PATCH 4/4] Conform to RFC 2253 / RFC 4514 for subject names. Previously, we used the openssl defaults when printing subject names, but upon further examination, the RFC 2253/RFC 4514 format is in much more common usage, and are the standards used by LDAP and other DN systems. This is by far the better default without exposing the myriad options for how to display subject names. --- src/pki-x509.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pki-x509.c b/src/pki-x509.c index 9d809f2..7e87102 100644 --- a/src/pki-x509.c +++ b/src/pki-x509.c @@ -596,7 +596,7 @@ SEXP PKI_get_subject(SEXP sCert) { char *txt = 0; PKI_init(); cert = retrieve_cert(sCert, ""); - if (X509_NAME_print_ex(mem, X509_get_subject_name(cert), 0, (XN_FLAG_ONELINE | ASN1_STRFLGS_UTF8_CONVERT) & ~ASN1_STRFLGS_ESC_MSB) < 0) { + if (X509_NAME_print_ex(mem, X509_get_subject_name(cert), 0, XN_FLAG_RFC2253) < 0) { BIO_free(mem); Rf_error("X509_NAME_print_ex failed with %s", ERR_error_string(ERR_get_error(), NULL)); }