/** * @file netsslcredentials.cc * * @brief Wrapper for getting or generating server cert and private key. * * Note: this code is derived from the OpenSSL demo program selfsign.c * * Threading: underlying SSL library contains threading * * @invariants: * * Copyright (c) 2011 Perforce Software * Confidential. All Rights Reserved. * @author Wendy Heffner * * Creation Date: October 31, 2011 */ # ifdef USE_SSL # define NEED_GETUID # define NEED_STAT # define NEED_ERRNO # include <stdhdrs.h> # include <strbuf.h> # include <error.h> # include <errorlog.h> # include <enviro.h> # include <debug.h> # include <pathsys.h> # include <filesys.h> # include <fileio.h> # include <hostenv.h> # include <utils.h> # include <msgrpc.h> extern "C" { // OpenSSL # include "openssl/err.h" # include <openssl/x509v3.h> # include <openssl/ssl.h> # include <openssl/x509_vfy.h> } # include <stdio.h> # include "netdebug.h" # include "netsslmacros.h" # include "netsslcredentials.h" //////////////////////////////////////////////////////////////////////////// // NetSslCredentials - Globals and Defines // //////////////////////////////////////////////////////////////////////////// # define SSL_CONFIGFILE (const char*)"config.txt" # define SSL_KEYFILE (const char*)"privatekey.txt" # define SSL_CERTFILE (const char*)"certificate.txt" # define SSL_X509_NUMBITS 2048 # define SSL_X509_VERSION 3 # define SSL_X509_SERIALNUM 01 # define SSL_X509_NOTBEFORE 0 # define SSL_X509_NOTAFTER ((long) 730 ) // expires in two years # define SSL_X509_DAY ((long) 60 * 60 * 24 ) # define SSL_X509_MAX_SECONDS 2147483647 /*((1 << 31) - 1) */ // 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 - entity name e.g. www.example.com // Example Values: // C=MY,ST=another state,L=another city,O=my company,OU=certs, // CN=www.example.com # define SSL_X509_C (const char*)"US" # define SSL_X509_ST (const char*)"CA" # define SSL_X509_L (const char*)"Alameda" # define SSL_X509_O (const char*)"Perforce Autogen Cert" //////////////////////////////////////////////////////////////////////////// // Static Callback Function Implementation // //////////////////////////////////////////////////////////////////////////// static void Callback( int code, int arg, void *cb_arg ) { if ( !SSLDEBUG_FUNCTION ) return; if( code == 0 ) p4debug.printf("."); if( code == 1 ) p4debug.printf("+"); if( code == 2 ) p4debug.printf("*"); if( code == 3 ) p4debug.printf("\n"); } //////////////////////////////////////////////////////////////////////////// // NetSslCredentials - Methods // //////////////////////////////////////////////////////////////////////////// NetSslCredentials::NetSslCredentials() : privateKey( NULL ), certificate( NULL ), fingerprint(), certC( SSL_X509_C ), certCN(), certST( SSL_X509_ST ), certL( SSL_X509_L ), certO( SSL_X509_O ) { HostEnv h; Enviro enviro; char *sslDirStr = NULL; ownCert = false; ownKey = false; certEX = SSL_X509_NOTAFTER; certSV = SSL_X509_NOTBEFORE; certUNITS = SSL_X509_DAY; h.GetHost( certCN ); const StrPtr *cachedServerName = enviro.GetCachedServerName(); if ( cachedServerName ) { enviro.BeServer( cachedServerName ); } sslDirStr = enviro.Get( "P4SSLDIR" ); if ( sslDirStr && sslDirStr[0] != '\0' ) { sslDir = sslDirStr; } } NetSslCredentials::NetSslCredentials( NetSslCredentials &rhs) : privateKey (rhs.privateKey), certificate(rhs.certificate), fingerprint(rhs.fingerprint), certC(rhs.certC), certCN(rhs.certCN), certST(rhs.certST), certL(rhs.certL), certO(rhs.certO), certEX(rhs.certEX), ownKey(false), ownCert(false), sslDir(rhs.sslDir) { } NetSslCredentials::~NetSslCredentials() { if ( privateKey && ownKey ) EVP_PKEY_free( privateKey ); if ( certificate && ownCert ) X509_free( certificate ); } NetSslCredentials & NetSslCredentials::operator =( NetSslCredentials &rhs ) { privateKey = rhs.privateKey; certificate = rhs.certificate; fingerprint = rhs.fingerprint; certC = rhs.certC; certCN = rhs.certCN; certST = rhs.certST; certL = rhs.certL; certO = rhs.certO; certEX = rhs.certEX; ownKey = false; ownCert = false; sslDir = rhs.sslDir; return *this; } void NetSslCredentials::HaveCredentials( Error *e ) { if( !privateKey || !certificate || (fingerprint.Length() == 0) ) e->Set(MsgRpc::SslNoCredentials); } void NetSslCredentials::ReadCredentials( Error *e ) { FILE *fp = NULL; PathSys *keyFile = PathSys::Create(); PathSys *certFile = PathSys::Create(); GetCredentialFilepaths( keyFile, certFile, e); // Validate that P4SSLDIR exists and is a directory ValidateSslDir( e ); P4CHECKERROR( e, "NetSslCredentials::ReadCredentials ValidateSslDir", fail ); ValidateCredentialFiles( e ); P4CHECKERROR( e, "NetSslCredentials::ReadCredentials ValidateCredentialFiles", fail ); // read in private key fp = fopen( keyFile->Text(), "r" ); if( fp == NULL ) { e->Net( "fopen", strerror(errno) ); goto failSetError; } privateKey = PEM_read_PrivateKey(fp, NULL, 0, NULL ); SSLNULLHANDLER( privateKey, e, "NetSslCredentials::ReadCredentials PEM_read_PrivateKey", failSetError ); // verify that RSA key if (privateKey->type != EVP_PKEY_RSA) { e->Set( MsgRpc::SslKeyNotRSA ); goto fail; } fclose( fp ); // read in certificate fp = fopen( certFile->Text(), "r" ); if( fp == NULL ) { e->Net( "fopen", strerror(errno) ); goto failSetError; } certificate = PEM_read_X509(fp, NULL, 0, NULL ); SSLNULLHANDLER( certificate, e, "NetSslCredentials::ReadCredentials PEM_read_X509", failSetError ); ValidateCertDateRange( e ); P4CHECKERROR( e, "NetSslCredentials::ReadCredentials ValidateCertDateRange", fail ); ownCert = true; ownKey = true; GetFingerprintFromCert( e ); if( e->Test() ) { goto fail; } fclose( fp ); delete keyFile; delete certFile; return; failSetError: e->Set( MsgRpc::SslBadKeyFile ); fail: if( fp ) { fclose( fp ); } delete keyFile; delete certFile; return; } void NetSslCredentials::GenerateCredentials( Error *e ) { PathSys *keyFile = PathSys::Create(); PathSys *certFile = PathSys::Create(); GetCredentialFilepaths( keyFile, certFile, e); P4CHECKERROR( e, "NetSslCredentials::GenerateCredentials GetCredentialsFiles", fail ); ValidateSslDir( e ); P4CHECKERROR( e, "NetSslCredentials::GenerateCredentials ValidateSslDir", fail ); // Validate that serverKey.txt and serverCert.txt do not exist in P4SSLDIR if( FileSys::FileExists( keyFile->Text() ) || FileSys::FileExists( certFile->Text() ) ) { e->Set( MsgRpc::SslDirHasCreds ); goto fail; } ParseConfig( e ); P4CHECKERROR( e, "NetSslCredentials::GenerateCredentials ParseConfig", fail ); MakeSslCredentials( e ); P4CHECKERROR( e, "NetSslCredentials::GenerateCredentials MakeSslCredentials", fail ); WriteCredentials( keyFile, certFile, e ); P4CHECKERROR( e, "NetSslCredentials::GenerateCredentials WriteCredentials", fail ); ownCert = true; ownKey = true; fail: delete keyFile; delete certFile; return; } void NetSslCredentials::ValidateSslDir( Error * e) { if ( sslDir.Length() == 0 ) { e->Set( MsgRpc::SslBadDir ); return; } // Validate that P4SSLDIR exists and is a directory FileSys *f = FileSys::Create( FST_BINARY ); f->Set( sslDir ); if( (f->Stat() & (FSF_EXISTS | FSF_DIRECTORY)) != (FSF_EXISTS | FSF_DIRECTORY) ) { e->Set( MsgRpc::SslBadDir ); goto fail; } // Validate that P4SSLDIR permission is 700 or 500 if( (! f->HasOnlyPerm( FPM_RWXO )) && (! f->HasOnlyPerm( FPM_RXO )) ) { e->Set( MsgRpc::SslBadFsSecurity ); goto fail; } // Validate that dir is owned by same owner as p4d CompareDirUid( e ); P4CHECKERROR( e, "NetSslCredentials::ValidateSslDir CompareDirUid", fail ); fail: delete f; return; } void NetSslCredentials::ValidateCredentialFiles( Error *e ) { FileSys *f = NULL; PathSys *keyFile = PathSys::Create(); PathSys *certFile = PathSys::Create(); GetCredentialFilepaths( keyFile, certFile, e); if( e->Test() ) goto fail; // Validate that serverKey.txt and serverCert.txt exist in P4SSLDIR if( !( FileSys::FileExists( keyFile->Text() ) && FileSys::FileExists( certFile->Text() ))) { e->Set( MsgRpc::SslBadKeyFile ); goto fail; } // Validate that files are owned by same owner as p4d CompareFileUids( e ); P4CHECKERROR( e, "NetSslCredentials::ValidateCredentialFiles CompareFileUids", fail ); // Validate that serverKey.txt and serverCert.txt permissions are 600 or 400 f = FileSys::Create( FST_BINARY ); f->Set( keyFile->Text() ); if( (! f->HasOnlyPerm( FPM_RWO )) && (! f->HasOnlyPerm( FPM_ROO )) ) { e->Set( MsgRpc::SslBadFsSecurity ); goto fail; } f->Set( certFile->Text() ); if( ! f->HasOnlyPerm( FPM_RWO ) && ! f->HasOnlyPerm( FPM_ROO ) ) { e->Set( MsgRpc::SslBadFsSecurity ); goto fail; } fail: if( f ) delete f; delete keyFile; delete certFile; return; } void NetSslCredentials::ValidateCertDateRange( Error *e ) { time_t *ptime = NULL; int i; i=X509_cmp_time(X509_get_notBefore(certificate), ptime); if (i >= 0) goto fail; i=X509_cmp_time(X509_get_notAfter(certificate), ptime); if (i <= 0) goto fail; return; fail: e->Set( MsgRpc::SslCertBadDates ); } void NetSslCredentials::GetExpiration( StrBuf &buf ) { Error e; if( !certificate ) { buf.Clear(); return; } BUF_MEM *bufMemPtr = NULL; int retVal = 0; BIO *bio = BIO_new( BIO_s_mem() ); SSLNULLHANDLER( bio, &e, "NetSslCredentials::GetExpiration BIO_new", fail ); retVal = ASN1_TIME_print(bio,X509_get_notAfter(certificate)); SSLHANDLEFAIL( retVal, &e, "NetSslCredentials::GetExpiration BIO_get_mem_ptr", MsgRpc::SslFailGetExpire, failCleanBIO ); retVal = BIO_get_mem_ptr( bio, &bufMemPtr ); SSLHANDLEFAIL( retVal, &e, "NetSslCredentials::GetExpiration BIO_get_mem_ptr", MsgRpc::SslFailGetExpire, failCleanBIO ); buf.Set( bufMemPtr->data, bufMemPtr->length ); buf.Terminate(); BIO_free_all( bio ); return; failCleanBIO: BIO_free_all( bio ); fail: buf.Clear(); return; } void NetSslCredentials::ParseConfig( Error *e ) { StrBuf line, var, value; StrRef configFile( SSL_CONFIGFILE ); SmartPointer <PathSys> p( PathSys::Create() ); FileSys *f = FileSys::Create( FileSysType( FST_TEXT|FST_L_CRLF ) ); // Create full pathname to certificate config file p->SetLocal( sslDir, configFile ); f->Set( *p ); f->Open( FOM_READ, e ); // If file doesn't exist then keep defaults and just return if( e->Test() ) { DEBUGPRINT( SSLDEBUG_FUNCTION, "NetSslCredentials::ParseConfig - config.txt file not found in P4SSLDIR." ); e->Clear(); return; } while( f->ReadLine( &line, e ) ) { line.TruncateBlanks(); char *equals = strchr( line.Text(), '=' ); if( !equals ) continue; var.Set( line.Text(), equals - line.Text() ); var.TrimBlanks(); if( var.Text()[0] == '#' ) continue; value.Set( equals + 1 ); value.TrimBlanks(); DEBUGPRINTF( SSLDEBUG_FUNCTION, "NetSslCredentials::ParseConfig name=%s, value=%s", var.Text(), value.Text() ); if ( var == "C") certC = value; else if ( var == "CN") certCN = value; else if ( var == "ST") certST = value; else if ( var == "L") certL = value; else if ( var == "O") certO = value; else if ( var == "EX") { int expire = StrBuf::Atoi(value.Text()); if( expire > 0 ) { certEX = expire; } else { e->Set( MsgRpc::SslCfgExpire ) << value; return; } } else if ( var == "SV") { int start = StrBuf::Atoi(value.Text()); certSV = start; } else if ( var == "UNITS") if( value == "secs" ) { certUNITS = 1; } else if ( value == "mins") { certUNITS = 60; } else if ( value == "hours") { certUNITS = 3600; } else if ( value == "days") ; // do nothing else { e->Set( MsgRpc::SslCfgUnits ) << value; return; } else { DEBUGPRINTF( SSLDEBUG_ERROR, "Certificate configuration file option \"%s\" unknown.", var.Text() ); } } if( (SSL_X509_MAX_SECONDS / certUNITS) < certEX ) { e->Set( MsgRpc::SslCfgExpire ) << value; return; } f->Close( e ); } void NetSslCredentials::WriteCredentials( PathSys *keyFile, PathSys *certFile, Error * e ) { FILE *fp = NULL; int retVal = 0; FileSys *fsKey = FileSys::Create( FST_TEXT ); FileSys *fsCert = FileSys::Create( FST_TEXT ); // write out in private key fp = fopen( keyFile->Text(), "w" ); if( fp == NULL ) { e->Net( "fopen", strerror(errno) ); goto fail; } retVal = PEM_write_PrivateKey(fp, privateKey, NULL, NULL, 0, 0, NULL ); SSLHANDLEFAIL( retVal, e, "NetSslCredentials::WriteCredentials PEM_write_PrivateKey", MsgRpc::SslCertGen, fail ); fclose( fp ); fsKey->Set(*keyFile); fsKey->Chmod(FPM_RWO, e); // read in certificate fp = fopen( certFile->Text(), "w" ); if( fp == NULL ) { e->Net( "fopen", strerror(errno) ); e->Set( MsgRpc::SslCertGen ); goto fail; } retVal = PEM_write_X509(fp, certificate ); SSLHANDLEFAIL( retVal, e, "NetSslCredentials::WriteCredentials PEM_write_X509", MsgRpc::SslCertGen, fail ); fclose( fp ); fp = NULL; fsCert->Set(*certFile); fsCert->Chmod(FPM_RWO, e); fail: if( fp ) fclose( fp ); delete fsKey; delete fsCert; return; } void NetSslCredentials::CompareFileUids( Error *e ) { # ifdef HAVE_GETUID uid_t keyOwner = 0; uid_t certOwner = 0; uid_t dirOwner = 0; uid_t currUsr = geteuid(); PathSys *keyFile = PathSys::Create(); PathSys *certFile = PathSys::Create(); FileSys *f = FileSys::Create( FST_BINARY ); GetCredentialFilepaths( keyFile, certFile, e); P4CHECKERROR( e, "NetSslCredentials::CompareUids GetCredentialsFiles", fail ); f->Set( keyFile->Text() ); keyOwner = f->GetOwner(); if( currUsr != keyOwner ) { e->Set( MsgRpc::SslCredsBadOwner ); goto fail; } f->Set( certFile->Text() ); certOwner = f->GetOwner(); if( currUsr != certOwner ) { e->Set( MsgRpc::SslCredsBadOwner ); goto fail; } f->Set( sslDir ); dirOwner = f->GetOwner(); if( currUsr != dirOwner ) e->Set( MsgRpc::SslCredsBadOwner ); fail: delete f; delete keyFile; delete certFile; return; # endif // HAVE_GETUID } void NetSslCredentials::CompareDirUid( Error *e ) { # ifdef HAVE_GETUID uid_t dirOwner = 0; uid_t currUsr = geteuid(); FileSys *f = FileSys::Create( FST_BINARY ); f->Set( sslDir ); dirOwner = f->GetOwner(); if( currUsr != dirOwner ) e->Set( MsgRpc::SslCredsBadOwner ); delete f; # endif // HAVE_GETUID } void NetSslCredentials::GetCredentialFilepaths( PathSys *keyFile, PathSys *certFile, Error * e ) { StrRef certFilename( SSL_CERTFILE ); StrRef keyFilename( SSL_KEYFILE ); keyFile->SetLocal(sslDir, keyFilename ); certFile->SetLocal(sslDir, certFilename ); } void NetSslCredentials::MakeSslCredentials( Error *e ) { if ( privateKey && certificate ) { return; } RSA * rsa = NULL; X509_NAME *name = NULL; int retval; if( ( privateKey = EVP_PKEY_new()) == NULL ) { e->Net( "EVP_PKEY_new", "failed" ); e->Set( MsgRpc::SslCertGen ); goto fail; } certificate = X509_new(); SSLHANDLEFAIL( certificate, e, "X509_new", MsgRpc::SslCertGen, fail ); rsa = RSA_generate_key( SSL_X509_NUMBITS, RSA_F4, Callback, NULL ); SSLHANDLEFAIL( rsa, e, "RSA_generate_key", MsgRpc::SslCertGen, fail ); retval = EVP_PKEY_assign_RSA( privateKey, rsa ); SSLHANDLEFAIL( retval, e, "EVP_PKEY_assign_RSA", MsgRpc::SslCertGen, fail ); X509_set_version( certificate, SSL_X509_VERSION ); ASN1_INTEGER_set( X509_get_serialNumber( certificate ), SSL_X509_SERIALNUM ); X509_gmtime_adj( X509_get_notBefore( certificate ), certSV * SSL_X509_DAY ); X509_gmtime_adj( X509_get_notAfter( certificate ), certEX * certUNITS ); X509_set_pubkey( certificate, privateKey ); name = X509_get_subject_name( certificate ); retval = X509_NAME_add_entry_by_txt( name, "C", MBSTRING_ASC, (unsigned char*)certC.Text(), -1, -1, 0 ); SSLHANDLEFAIL( retval, e, "X509_NAME_add_entry_by_txt for \"C\"", MsgRpc::SslCertGen, fail ); retval = X509_NAME_add_entry_by_txt( name, "ST", MBSTRING_ASC, (unsigned char*)certST.Text(), -1, -1, 0 ); SSLHANDLEFAIL( retval, e, "X509_NAME_add_entry_by_txt for \"ST\"", MsgRpc::SslCertGen, fail ); retval = X509_NAME_add_entry_by_txt( name, "L", MBSTRING_ASC, (unsigned char*)certL.Text(), -1, -1, 0 ); SSLHANDLEFAIL( retval, e, "X509_NAME_add_entry_by_txt for \"L\"", MsgRpc::SslCertGen, fail ); retval = X509_NAME_add_entry_by_txt( name, "O", MBSTRING_ASC, (unsigned char*)certO.Text(), -1, -1, 0 ); SSLHANDLEFAIL( retval, e, "X509_NAME_add_entry_by_txt for \"O\"", MsgRpc::SslCertGen, fail ); DEBUGPRINTF( SSLDEBUG_FUNCTION, "Setting CN to Hostname: %s", certCN.Text()) retval = X509_NAME_add_entry_by_txt( name, "CN", MBSTRING_ASC, (unsigned char*)certCN.Text(), -1, -1, 0 ); SSLHANDLEFAIL( retval, e, "X509_NAME_add_entry_by_txt for \"CN\": ", MsgRpc::SslCertGen, fail ); /* Its self signed so set the issuer name to be the same as the * subject. */ X509_set_issuer_name( certificate, name ); if( !X509_sign( certificate, privateKey, EVP_sha1() ) ) { e->Net( "EVP_PKEY_new", "failed" ); e->Set( MsgRpc::SslCertGen ); goto fail; } return; fail: if (certificate) { X509_free( certificate ); certificate = NULL; } if (privateKey) { EVP_PKEY_free( privateKey ); privateKey = NULL; } } void NetSslCredentials::GetFingerprintFromCert( Error *e ) { int retval = 1; int i = 0; unsigned int n = 0; BIO *bio = NULL; BUF_MEM *bufMemPtr = NULL; unsigned char m[300]; unsigned char md[EVP_MAX_MD_SIZE]; const EVP_MD *fdig = EVP_sha1(); unsigned char *ptr; int l; if( !certificate ) { e->Set( MsgRpc::SslNoCredentials ); return; } bio = BIO_new(BIO_s_mem()); SSLNULLHANDLER( bio, e, "GetFingerprintFromCert BIO_new", fail ); ptr = m; l = i2d_X509_PUBKEY( X509_get_X509_PUBKEY( certificate ), &ptr ); EVP_Digest( m, l, md, &n, fdig, NULL ); if( l > sizeof m ) { SSLHANDLEFAIL( retval, e, "GetFingerprintFromCert OVERRUN", MsgRpc::SslGetPubKey, failCleanBIO ); } DEBUGPRINTF( SSLDEBUG_FUNCTION, "pubkey len is: %d", l ); DEBUGPRINTF( SSLDEBUG_FUNCTION, "digest len is: %u", n ); n--; // loop through all but last one for (i=0; i<(int)n; i++) { BIO_printf(bio,"%02X:",md[i]); } // last one do not add a colon BIO_printf(bio,"%02X",md[n]); retval = BIO_get_mem_ptr( bio, &bufMemPtr ); SSLHANDLEFAIL( retval, e, "GetFingerprintFromCert BIO_get_mem_ptr", MsgRpc::SslGetPubKey, failCleanBIO ); fingerprint.Set( bufMemPtr->data, bufMemPtr->length ); fingerprint.Terminate(); DEBUGPRINTF( SSLDEBUG_FUNCTION, "GetFingerprintFromCert Fingerprint is: %s", fingerprint.Text() ); failCleanBIO: BIO_free_all(bio); fail: /* nothing to clean up */ return; } X509 * NetSslCredentials::GetCertificate() const { return certificate; } const StrPtr * NetSslCredentials::GetFingerprint() const { return &fingerprint; } EVP_PKEY * NetSslCredentials::GetPrivateKey() const { return privateKey; } void NetSslCredentials::SetCertificate( X509 *cert, Error *e ) { if( !cert ) { e->Set( MsgRpc::SslNoCredentials ); return; } this->certificate = cert; this->ownCert = false; ValidateCertDateRange( e ); if( e->Test() ) { this->certificate = NULL; return; } GetFingerprintFromCert( e ); if( e->Test() ) { this->certificate = NULL; this->fingerprint.Clear(); return; } } void NetSslCredentials::SetOwnCert( bool ownCert ) { this->ownCert = ownCert; } void NetSslCredentials::SetOwnKey( bool ownKey ) { this->ownKey = ownKey; } void NetSslCredentials::SetCertC( StrBuf &certC ) { this->certC = certC; } void NetSslCredentials::SetCertCN( StrBuf &certCN ) { this->certCN = certCN; } void NetSslCredentials::SetCertL( StrBuf &certL ) { this->certL = certL; } void NetSslCredentials::SetCertO( StrBuf &certO ) { this->certO = certO; } void NetSslCredentials::SetCertST( StrBuf &certST ) { this->certST = certST; } void NetSslCredentials::SetSslDir( StrPtr *sslDir ) { this->sslDir = *sslDir; } # endif // USE_SSL