15
\$\begingroup\$

I've just published a small library implementing the RSA Blind Signature process.

Here's the GitHub repo!

I needed something portable, and easy enough to use that I could ask other people to incorporate it into their work. Nothing like that seemed to be available, so I put it together using the Crypto++ library, some example code on their wiki, and some advice from other websites.

The whole thing is ~1000 lines of code, half of which is the parser for PEM-formatted keys (which is lifted from a pre-existing Crypto++ extension).

I've already had someone go over it (briefly) for style, and I'm certainly open to feedback about usability, but the important question at this point is

Would using this library as directed provide the guarantees expected of a blind signature protocol?

In short, we need the same behavior/guarantees we'd need form a normal hash-and-sign protocol, plus "unlinkability". This prior question/answer explains the idea well, and there's some discussion of the math we're relying on here.

I'm really hoping that people will like the library and use it, but if there are problems with it then the sooner we can identify them the better!

Code Files:

Here's the head commit when I posted this question, and from which the below is copied.

makefile

CXX=g++
CXXFLAGS=-I. -Lcryptopp810 -lcryptopp -static
OUTDIR=bin/
PREFIX=$(OUTDIR)blsig_
INCLUDES=includes.h common_functions.h inner_functions.h pem-rd.h

all: $(PREFIX)get_client_secret
all: $(PREFIX)get_blinded_hash
all: $(PREFIX)get_blind_signature
all: $(PREFIX)get_unblinded_signature
all: $(PREFIX)verify_unblinded_signature
all: $(OUTDIR)test

$(PREFIX)%: %.cxx $(INCLUDES)
    $(CXX) $< $(CXXFLAGS) -o $@

$(OUTDIR)test: test.cxx $(INCLUDES)
    $(CXX) $< $(CXXFLAGS) -g -o $@

clean:
    rm -f $(PREFIX)*
    rm $(OUTDIR)test

.PHONY: clean all

includes.h

#ifndef BLSIG_INCLUDES_INCLUDED
# define BLSIG_INCLUDES_INCLUDED

# ifndef DEBUG
#  define DEBUG 0
# endif

# include <iostream>
# include <fstream>
# include <regex>
# include <stdexcept>

// Use "" based includes for the cryptopp library because it's perfectly
// legitimate to install it to the local directory. 
# include "cryptopp810/base64.h"
# include "cryptopp810/cryptlib.h"
# include "cryptopp810/files.h"
# include "cryptopp810/integer.h"
# include "cryptopp810/nbtheory.h"
# include "cryptopp810/osrng.h"
# include "cryptopp810/rsa.h"
# include "cryptopp810/sha.h"

# include "pem-rd.h"
# include "common_functions.h"
# include "inner_functions.h"

#endif

common_functions.h

#ifndef BLSIG_COMMON_H_INCLUDED
# define BLSIG_COMMON_H_INCLUDED
# include "includes.h"

using namespace CryptoPP;

static const std::regex PEM_Key_Regex_Public(
    "-----BEGIN (?:RSA )?PUBLIC KEY-----[\\r\\n]+([^-]*)[\\r\\n]+-----END (?:RSA )?PUBLIC KEY-----");
static const std::regex PEM_Key_Regex_Private(
    "-----BEGIN (?:RSA )?PRIVATE KEY-----[\\r\\n]+([^-]*)[\\r\\n]+-----END (?:RSA )?PRIVATE KEY-----");

/* Generates the SHA512 hash of an arbitrary string.
 */
Integer GenerateHash(const std::string &message)
{
    SHA512 hash;
    SecByteBlock buff;

    SecByteBlock orig((const byte*)message.c_str(), message.size());

    buff.resize(SHA512::DIGESTSIZE);
    hash.CalculateTruncatedDigest(buff, buff.size(), orig, orig.size());

    Integer hashed_message(buff.data(), buff.size());

    #if DEBUG
        std::cout << "Message: " << message << std::endl;
        std::cout << "Hash: " << std::hex << hashed_message << std::dec << std::endl;
    #endif

    return hashed_message;
}

/* Loads an RSA Public Key from the specified file.
 */
RSA::PublicKey ReadPEMPublicKey(std::string file_name)
{
    RSA::PublicKey public_key;
    FileSource public_key_file(file_name.c_str(), true);
    PEM_Load(public_key_file, public_key);
    return public_key;
}

/* Loads an RSA Private Key from the specified file.
 * The key must not be password protected.
 */
RSA::PrivateKey ReadPEMPrivateKey(std::string file_name)
{
    RSA::PrivateKey private_key;
    FileSource private_key_file(file_name.c_str(), true);
    PEM_Load(private_key_file, private_key);
    return private_key;
}

#endif

inner_functions.h

#ifndef BLSIG_INNER_H_INCLUDED
# define BLSIG_INNER_H_INCLUDED
# include "includes.h"

using namespace CryptoPP;

/* Generates a single-use secret value for blinding a message before it is sent
 * to the signer.
 * The public key is needed as a parameter because the space of valid secrets
 * depends on the details of the key.
 */
Integer GenerateClientSecret(const RSA::PublicKey &public_key, AutoSeededRandomPool &rng_source)
{
    const Integer &n = public_key.GetModulus();

    Integer client_secret;
    do
    {
        client_secret.Randomize(rng_source, Integer::One(), n - Integer::One());
    } while (!RelativelyPrime(client_secret, n));

    #if DEBUG
        std::cout << "Random Client Secret: " << std::hex << client_secret << std::dec << std::endl;
    #endif

    return client_secret;
}

/* Generates a blinded version of the message value, to be sent to the signer.
 */
Integer MessageBlinding(const Integer &hashed_message, const RSA::PublicKey &public_key, const Integer &client_secret)
{
    const Integer &n = public_key.GetModulus();
    const Integer &e = public_key.GetPublicExponent();

    Integer b = a_exp_b_mod_c(client_secret, e, n);
    Integer hidden_message = a_times_b_mod_c(hashed_message, b, n);

    #if DEBUG
        std::cout << "Blinding factor: " << std::hex << b << std::dec << std::endl;
        std::cout << "Blinded hashed message: " << std::hex << hidden_message << std::dec << std::endl;
    #endif

    return hidden_message;
}

/* Retrieves the completed signature from a blinded signature.
 */
Integer SignatureUnblinding(const Integer &blinded_signature, const RSA::PublicKey &public_key, const Integer &client_secret)
{
    const Integer &n = public_key.GetModulus();
    const Integer &inverse_secret = client_secret.InverseMod(n);

    Integer signed_unblinded = a_times_b_mod_c(blinded_signature, inverse_secret, n);

    #if DEBUG
        std::cout << "Signed Unblinded: " << std::hex << signed_unblinded << std::dec << std::endl;
    #endif

    return signed_unblinded;
}

/* Blindly signs the provided hash.
 * The returned value is not quite a complete signature; it must be unblinded
 * by the original requestor using the one-time client secret.
 */
Integer SignBlindedMessage(const Integer &blinded_hash, const RSA::PrivateKey &private_key, AutoSeededRandomPool &rng_source)
{
    Integer signed_message = private_key.CalculateInverse(rng_source, blinded_hash);

    #if DEBUG
        std::cout << "Signed Message: " << std::hex << signed_message << std::dec << std::endl;
    #endif

    return signed_message;
}

/* Prior to unblinding a signature, checks if the signature will be valid.
 * **It's unclear if this contributes anything of value to the algorithm or
 * this library. We include it for now for completeness.**
 */
bool PreverifySignature(const Integer &signed_blinded_hash, const Integer &blinded_hash, const RSA::PublicKey &public_key)
{
    bool valid = public_key.ApplyFunction(signed_blinded_hash) == blinded_hash;

    #if DEBUG
        std::cout << "The blind message was" << (valid ? " " : " NOT ") << "properly signed." << std::endl;
    #endif

    return valid;
}

/* Checks that a completed signature is a valid signature of the message hash. 
 */
bool VerifySignature(const Integer &unblinded_signature, const Integer &hashed_message, const RSA::PublicKey &public_key)
{
    Integer signature_payload = public_key.ApplyFunction(unblinded_signature);
    bool valid = hashed_message == signature_payload;

    #if DEBUG
        std::cout << "The signature contained message hash: " << std::hex << signature_payload << std::dec << std::endl;
        std::cout << "The signature is " << (valid ? "valid" : "INVALID") << "." << std::endl;
    #endif

    return valid;
}

#endif

get_client_secret.cxx

#include "includes.h"

using namespace CryptoPP;

static AutoSeededRandomPool rng_source;

#define DOCUMENTATION "Generates a single-use secret for blinding a message."
#define USEAGE "blsig_get_client_secret public_key.pem"
#define ARGUMENT_COUNT 1

static RSA::PublicKey public_key;

int main(int argc, char *argv[])
{
    if(ARGUMENT_COUNT != --argc){
        std::cerr << "Incorrect useage of " << argv[0]
            << ". Expected " << ARGUMENT_COUNT << " arguments; given " << argc << "." << std::endl
            << "Useage: \n\t" << USEAGE << std::endl
            << DOCUMENTATION << std::endl;
        return EXIT_FAILURE;
    }

    try{
        public_key = ReadPEMPublicKey(argv[1]);
    }
    catch(std::runtime_error& e)
    {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    Integer client_secret = GenerateClientSecret(public_key, rng_source);

    std::cout << std::hex << client_secret << std::endl;
    return EXIT_SUCCESS;
}

get_blinded_hash.cxx

#include "includes.h"

using namespace CryptoPP;

#define DOCUMENTATION "Hashes the message and then blinds the hash so it can be sent to the signer."
#define USEAGE "blsig_get_blinded_hash message client_secret public_key.pem"
#define ARGUMENT_COUNT 3

static std::string message;
static Integer client_secret;
static RSA::PublicKey public_key;

int main(int argc, char *argv[])
{
    if(ARGUMENT_COUNT != --argc){
        std::cerr << "Incorrect useage of " << argv[0]
            << ". Expected " << ARGUMENT_COUNT << " arguments; given " << argc << "." << std::endl
            << "Useage: \n\t" << USEAGE << std::endl
            << DOCUMENTATION << std::endl;
        return EXIT_FAILURE;
    }

    try{
        message = argv[1];
        client_secret = Integer(argv[2]);
        public_key = ReadPEMPublicKey(argv[3]);
    }
    catch(std::runtime_error& e)
    {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    Integer hashed_message = GenerateHash(message);
    Integer hidden_message = MessageBlinding(hashed_message, public_key, client_secret);

    std::cout << std::hex << hidden_message << std::endl;
    return EXIT_SUCCESS;
}

get_blind_signature.cxx

#include "includes.h"

using namespace CryptoPP;

static AutoSeededRandomPool rng_source;

#define DOCUMENTATION "Generates a "pre-signature" (or hashed signature or whatever you want to call it) without any knowledge of the message, the message-hash, or the client secret."
#define USEAGE "blsig_get_blind_signature blinded_hash private_key.pem"
#define ARGUMENT_COUNT 2

static Integer blinded_hash;
static RSA::PrivateKey private_key;

int main(int argc, char *argv[])
{
    if(ARGUMENT_COUNT != --argc){
        std::cerr << "Incorrect useage of " << argv[0]
            << ". Expected " << ARGUMENT_COUNT << " arguments; given " << argc << "." << std::endl
            << "Useage: \n\t" << USEAGE << std::endl
            << DOCUMENTATION << std::endl;
        return EXIT_FAILURE;
    }

    try{
        blinded_hash = Integer(argv[1]);
        private_key = ReadPEMPrivateKey(argv[2]);
    }
    catch(std::runtime_error& e)
    {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    Integer signed_message = SignBlindedMessage(blinded_hash, private_key, rng_source);

    std::cout << std::hex << signed_message << std::endl;
    return EXIT_SUCCESS;
}

get_unblinded_signature.cxx

#include "includes.h"

using namespace CryptoPP;

#define DOCUMENTATION "Un-blinds the pre-signature using the same client_secret used to generate the blinded-hash. Also verifies the signature. The client secret should not be stored once it has served its purpose once."
#define USEAGE "blsig_get_unblinded_signature blind_signature blinded_hash client_secret public_key.pem"
#define ARGUMENT_COUNT 4

static Integer blinded_signature;
static Integer blinded_hash;
static Integer client_secret;
static RSA::PublicKey public_key;

int main(int argc, char *argv[])
{
    if(ARGUMENT_COUNT != --argc){
        std::cerr << "Incorrect useage of " << argv[0]
            << ". Expected " << ARGUMENT_COUNT << " arguments; given " << argc << "." << std::endl
            << "Useage: \n\t" << USEAGE << std::endl
            << DOCUMENTATION << std::endl;
        return EXIT_FAILURE;
    }

    try{
        blinded_signature = Integer(argv[1]);
        blinded_hash = Integer(argv[2]);
        client_secret = Integer(argv[3]);
        public_key = ReadPEMPublicKey(argv[4]);
    }
    catch(std::runtime_error& e)
    {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    if(PreverifySignature(blinded_signature, blinded_hash, public_key))
    {
        Integer unblinded_signature = SignatureUnblinding(blinded_signature, public_key, client_secret);

        std::cout << std::hex << unblinded_signature << std::endl;
        return EXIT_SUCCESS;
    }
    else
    {
        std::cerr << "There is a problem with the provided signature: it does not match the blinded hash." << std::endl;
        return EXIT_FAILURE;
    }
}

verify_unblinded_signature.cxx

#include "includes.h"

using namespace CryptoPP;

#define DOCUMENTATION "Confirms that a provided signature is a valid signature, by the corresponding private-key, of the provided message. Prints true for success."
#define USEAGE "blsig_verify_unlinded_signature unblinded_signature message public_key.pem"
#define ARGUMENT_COUNT 3

static Integer unblinded_signature;
static std::string message;
static RSA::PublicKey public_key;

int main(int argc, char *argv[])
{
    if(ARGUMENT_COUNT != --argc){
        std::cerr << "Incorrect useage of " << argv[0]
            << ". Expected " << ARGUMENT_COUNT << " arguments; given " << argc << "." << std::endl
            << "Useage: \n\t" << USEAGE << std::endl
            << DOCUMENTATION << std::endl;
        return EXIT_FAILURE;
    }

    try{
        unblinded_signature = Integer(argv[1]);
        message = argv[2];
        public_key = ReadPEMPublicKey(argv[3]);
    }
    catch(std::runtime_error& e)
    {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    Integer hashed_message = GenerateHash(message);

    if(VerifySignature(unblinded_signature, hashed_message, public_key))
    {
        std::cout << "true" << std::endl;
        return EXIT_SUCCESS;
    }
    else
    {
        std::cerr << "That is not a valid signature for the provided message." << std::endl;
        return EXIT_FAILURE;
    }
}

test.cxx

#define DEBUG 1
#include "includes.h"

using namespace CryptoPP;

static AutoSeededRandomPool rng_source;

/* Generates a key pair using system calls to openssl.
 * Then loads the keys and uses them to walk through the steps of hashing, blind-signing, and verifying the signature. 
 */
int main(int argc, char *argv[])
{
    if(0 == std::system(NULL)
            || 0 != std::system("which openssl")
            || 0 != std::system("which rm")){
        std::cerr << "The test script will not work on this system." << std::endl;
        exit(EXIT_FAILURE);
    }

    std::system("openssl genrsa -out scratch/._blsig_test_rsa_key_priv.pem 2048");
    std::system("openssl rsa -in scratch/._blsig_test_rsa_key_priv.pem -out scratch/._blsig_test_rsa_key_pub.pem -pubout");

    RSA::PublicKey public_key = ReadPEMPublicKey("scratch/._blsig_test_rsa_key_pub.pem");
    RSA::PrivateKey private_key = ReadPEMPrivateKey("scratch/._blsig_test_rsa_key_priv.pem");

    // Alice create a blind message
    Integer client_secret = GenerateClientSecret(public_key, rng_source);
    std::string message = "Hello world! How are you doing to day? It's a pretty nice day if i do say so myself1.";
    Integer original_hash = GenerateHash(message);
    Integer blinded = MessageBlinding(original_hash, public_key, client_secret);

    // Send blinded message for signing
    Integer signed_blinded = SignBlindedMessage(blinded, private_key, rng_source);

    // Alice will remove blinding factor
    Integer signed_unblinded = SignatureUnblinding(signed_blinded, public_key, client_secret);

    // Eve verification stage
    Integer message_hash = GenerateHash(message);
    Integer received_hash = public_key.ApplyFunction(signed_unblinded);
    std::cout << "Signature payload: " << received_hash << std::endl;
    if (!VerifySignature(signed_unblinded, message_hash, public_key))
    {
        std::cout << "Verification failed" << std::endl;
        exit(EXIT_FAILURE);
    }

    std::cout << "Signature Verified" << std::endl;
    // return success
    return EXIT_SUCCESS;
}

pem-rd.h

#ifndef BLSIG_PEM_H_INCLUDED
# define BLSIG_PEM_H_INCLUDED
# include "includes.h"

// pem-rd.cpp - PEM read routines. Written and placed in the public domain by Jeffrey Walton
//              Copyright assigned to the Crypto++ project.
//
// Modified for selective standalone use by Mako Bates
//
// Crypto++ Library is copyrighted as a compilation and (as of version 5.6.2) licensed
// under the Boost Software License 1.0, while the individual files in the compilation
// are all public domain.

///////////////////////////////////////////////////////////////////////////
// For documentation on the PEM read and write routines, see
//   http://www.cryptopp.com/wiki/PEM_Pack
///////////////////////////////////////////////////////////////////////////

#include <string>
#include <algorithm>

#include <cctype>

#include "cryptopp810/secblock.h"
#include "cryptopp810/gfpcrypt.h"
#include "cryptopp810/camellia.h"
#include "cryptopp810/smartptr.h"
#include "cryptopp810/filters.h"
#include "cryptopp810/queue.h"
#include "cryptopp810/modes.h"
#include "cryptopp810/asn.h"
#include "cryptopp810/aes.h"
#include "cryptopp810/idea.h"
#include "cryptopp810/des.h"
#include "cryptopp810/hex.h"

NAMESPACE_BEGIN(CryptoPP)

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
//I want this to be just one file, so I'm pulling some stuff in from the original headers.
enum PEM_Type { PEM_PUBLIC_KEY = 1, PEM_PRIVATE_KEY,
    PEM_RSA_PUBLIC_KEY, PEM_RSA_PRIVATE_KEY, PEM_RSA_ENC_PRIVATE_KEY,
    PEM_DSA_PUBLIC_KEY, PEM_DSA_PRIVATE_KEY, PEM_DSA_ENC_PRIVATE_KEY,
    PEM_EC_PUBLIC_KEY, PEM_ECDSA_PUBLIC_KEY, PEM_EC_PRIVATE_KEY, PEM_EC_ENC_PRIVATE_KEY,
    PEM_EC_PARAMETERS, PEM_DH_PARAMETERS, PEM_DSA_PARAMETERS,
    PEM_X509_CERTIFICATE, PEM_REQ_CERTIFICATE, PEM_CERTIFICATE,
    PEM_UNSUPPORTED = 0xFFFFFFFF };

static inline SecByteBlock StringToSecByteBlock(const std::string& str)
{
    return SecByteBlock(reinterpret_cast<const byte*>(str.data()), str.size());
}
static inline SecByteBlock StringToSecByteBlock(const char* str)
{
    return SecByteBlock(reinterpret_cast<const byte*>(str), strlen(str));
}
static inline const byte* BYTE_PTR(const char* cstr)
{
    return reinterpret_cast<const byte*>(cstr);
}
static inline byte* BYTE_PTR(char* cstr)
{
    return reinterpret_cast<byte*>(cstr);
}

static const SecByteBlock CR(BYTE_PTR("\r"), 1);
static const SecByteBlock LF(BYTE_PTR("\n"), 1);
static const SecByteBlock CRLF(BYTE_PTR("\r\n"), 2);

static const unsigned int RFC1421_LINE_BREAK = 64;
static const std::string RFC1421_EOL = "\r\n";

static const SecByteBlock SBB_PEM_BEGIN(BYTE_PTR("-----BEGIN"), 10);
static const SecByteBlock SBB_PEM_TAIL(BYTE_PTR("-----"), 5);
static const SecByteBlock SBB_PEM_END(BYTE_PTR("-----END"), 8);

static const size_t PEM_INVALID = static_cast<size_t>(-1);

static const std::string LBL_PUBLIC_BEGIN("-----BEGIN PUBLIC KEY-----");
static const std::string LBL_PUBLIC_END("-----END PUBLIC KEY-----");
static const std::string LBL_PRIVATE_BEGIN("-----BEGIN PRIVATE KEY-----");
static const std::string LBL_PRIVATE_END("-----END PRIVATE KEY-----");
static const std::string LBL_RSA_PUBLIC_BEGIN("-----BEGIN RSA PUBLIC KEY-----");
static const std::string LBL_RSA_PUBLIC_END("-----END RSA PUBLIC KEY-----");
static const std::string LBL_RSA_PRIVATE_BEGIN("-----BEGIN RSA PRIVATE KEY-----");
static const std::string LBL_RSA_PRIVATE_END("-----END RSA PRIVATE KEY-----");
static const std::string LBL_PROC_TYPE_ENC("Proc-Type: 4,ENCRYPTED");
static const SecByteBlock SBB_PUBLIC_BEGIN(StringToSecByteBlock(LBL_PUBLIC_BEGIN));
static const SecByteBlock SBB_PUBLIC_END(StringToSecByteBlock(LBL_PUBLIC_END));
static const SecByteBlock SBB_PRIVATE_BEGIN(StringToSecByteBlock(LBL_PRIVATE_BEGIN));
static const SecByteBlock SBB_PRIVATE_END(StringToSecByteBlock(LBL_PRIVATE_END));
static const SecByteBlock SBB_RSA_PUBLIC_BEGIN(StringToSecByteBlock(LBL_RSA_PUBLIC_BEGIN));
static const SecByteBlock SBB_RSA_PUBLIC_END(StringToSecByteBlock(LBL_RSA_PUBLIC_END));
static const SecByteBlock SBB_RSA_PRIVATE_BEGIN(StringToSecByteBlock(LBL_RSA_PRIVATE_BEGIN));
static const SecByteBlock SBB_RSA_PRIVATE_END(StringToSecByteBlock(LBL_RSA_PRIVATE_END));
static const SecByteBlock SBB_PROC_TYPE_ENC(StringToSecByteBlock(LBL_PROC_TYPE_ENC));

//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

static size_t PEM_ReadLine(BufferedTransformation& source, SecByteBlock& line, SecByteBlock& ending);
static PEM_Type PEM_GetType(const BufferedTransformation& bt);
static PEM_Type PEM_GetType(const SecByteBlock& sb);

static void PEM_StripEncapsulatedBoundary(BufferedTransformation& bt, const SecByteBlock& pre, const SecByteBlock& post);
static void PEM_StripEncapsulatedBoundary(SecByteBlock& sb, const SecByteBlock& pre, const SecByteBlock& post);

static inline SecByteBlock::const_iterator Search(const SecByteBlock& source, const SecByteBlock& target);

static void PEM_LoadPublicKey(BufferedTransformation& bt, X509PublicKey& key, bool subjectInfo = false);
static void PEM_LoadPrivateKey(BufferedTransformation& src, PKCS8PrivateKey& key, bool subjectInfo);

static void PEM_NextObject(BufferedTransformation& src, BufferedTransformation& dest);

static void PEM_Base64Decode(BufferedTransformation& source, BufferedTransformation& dest);

static void PEM_WriteLine(BufferedTransformation& bt, const std::string& line);
static void PEM_WriteLine(BufferedTransformation& bt, const SecByteBlock& line);

//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

void PEM_Load(BufferedTransformation& bt, RSA::PublicKey& rsa)
{
    ByteQueue obj;
    PEM_NextObject(bt, obj);

    PEM_Type type = PEM_GetType(obj);
    if (type == PEM_PUBLIC_KEY)
        PEM_StripEncapsulatedBoundary(obj, SBB_PUBLIC_BEGIN, SBB_PUBLIC_END);
    else if(type == PEM_RSA_PUBLIC_KEY)
        PEM_StripEncapsulatedBoundary(obj, SBB_RSA_PUBLIC_BEGIN, SBB_RSA_PUBLIC_END);
    else
        throw InvalidDataFormat("PEM_Load: not a RSA public key");

    ByteQueue temp;
    PEM_Base64Decode(obj, temp);

    PEM_LoadPublicKey(temp, rsa, type == PEM_PUBLIC_KEY);
}

void PEM_Load(BufferedTransformation& bt, RSA::PrivateKey& rsa)
{
    ByteQueue obj;
    PEM_NextObject(bt, obj);

    PEM_Type type = PEM_GetType(obj);
    if(type == PEM_PRIVATE_KEY)
        PEM_StripEncapsulatedBoundary(obj, SBB_PRIVATE_BEGIN, SBB_PRIVATE_END);
    else if(type == PEM_RSA_PRIVATE_KEY)
        PEM_StripEncapsulatedBoundary(obj, SBB_RSA_PRIVATE_BEGIN, SBB_RSA_PRIVATE_END);
    else if(type == PEM_RSA_ENC_PRIVATE_KEY)
        throw InvalidArgument("PEM_Load: RSA private key is encrypted");
    else
        throw InvalidDataFormat("PEM_Load: not a RSA private key");

    ByteQueue temp;
    PEM_Base64Decode(obj, temp);

    PEM_LoadPrivateKey(temp, rsa, type == PEM_PRIVATE_KEY);
}

void PEM_LoadPublicKey(BufferedTransformation& src, X509PublicKey& key, bool subjectInfo)
{
    X509PublicKey& pk = dynamic_cast<X509PublicKey&>(key);

    if (subjectInfo)
        pk.Load(src);
    else
        pk.BERDecode(src);

    #if !defined(NO_OS_DEPENDENCE)
    AutoSeededRandomPool prng;
    if(!pk.Validate(prng, 2))
        throw Exception(Exception::OTHER_ERROR, "PEM_LoadPublicKey: key validation failed");
    #endif
}

void PEM_LoadPrivateKey(BufferedTransformation& src, PKCS8PrivateKey& key, bool subjectInfo)
{
    if (subjectInfo)
        key.Load(src);
    else
        key.BERDecodePrivateKey(src, 0, src.MaxRetrievable());

    #if !defined(NO_OS_DEPENDENCE)
    AutoSeededRandomPool prng;
    if(!key.Validate(prng, 2))
        throw Exception(Exception::OTHER_ERROR, "PEM_LoadPrivateKey: key validation failed");
    #endif
}

PEM_Type PEM_GetType(const BufferedTransformation& bt)
{
    const size_t size = bt.MaxRetrievable();
    SecByteBlock sb(size);

    bt.Peek(sb.data(), sb.size());
    return PEM_GetType(sb);
}

PEM_Type PEM_GetType(const SecByteBlock& sb)
{
    SecByteBlock::const_iterator it;

    // Uses an OID to identify the public key type
    it = Search(sb, SBB_PUBLIC_BEGIN);
    if (it != sb.end())
        return PEM_PUBLIC_KEY;

    // Uses an OID to identify the private key type
    it = Search(sb, SBB_PRIVATE_BEGIN);
    if (it != sb.end())
        return PEM_PRIVATE_KEY;

    // RSA key types
    it = Search(sb, SBB_RSA_PUBLIC_BEGIN);
    if(it != sb.end())
        return PEM_RSA_PUBLIC_KEY;

    it = Search(sb, SBB_RSA_PRIVATE_BEGIN);
    if(it != sb.end())
    {
        it = Search(sb, SBB_PROC_TYPE_ENC);
        if(it != sb.end())
            return PEM_RSA_ENC_PRIVATE_KEY;

        return PEM_RSA_PRIVATE_KEY;
    }

    return PEM_UNSUPPORTED;
}

void PEM_StripEncapsulatedBoundary(BufferedTransformation& bt, const SecByteBlock& pre, const SecByteBlock& post)
{
    ByteQueue temp;
    SecByteBlock::const_iterator it;
    int n = 1, prePos = -1, postPos = -1;

    while(bt.AnyRetrievable() && n++)
    {
        SecByteBlock line, unused;
        PEM_ReadLine(bt, line, unused);

        // The write associated with an empty line must to occur. Otherwise, we loose the CR or LF
        //    in an ecrypted private key between the control fields and the encapsulated text.
        //if(line.empty())
        //    continue;

        it = Search(line, pre);
        if(it != line.end())
        {
            prePos = n;
            continue;
        }
        it = Search(line, post);
        if(it != line.end())
        {
            postPos = n;
            continue;
        }

        PEM_WriteLine(temp, line);
    }

    if(prePos == -1)
    {
        std::string msg = "PEM_StripEncapsulatedBoundary: '";
        msg += std::string((char*)pre.data(), pre.size()) + "' not found";
        throw InvalidDataFormat(msg);
    }

    if(postPos == -1)
    {
        std::string msg = "PEM_StripEncapsulatedBoundary: '";
        msg += std::string((char*)post.data(), post.size()) + "' not found";
        throw InvalidDataFormat(msg);
    }

    if(prePos > postPos)
        throw InvalidDataFormat("PEM_StripEncapsulatedBoundary: header boundary follows footer boundary");

    temp.TransferTo(bt);
}

void PEM_NextObject(BufferedTransformation& src, BufferedTransformation& dest)
{
    if(!src.AnyRetrievable())
        return;

    // We have four things to find:
    //   1. -----BEGIN (the leading begin)
    //   2. ----- (the trailing dashes)
    //   3. -----END (the leading end)
    //   4. ----- (the trailing dashes)

    // Once we parse something that purports to be PEM encoded, another routine
    //  will have to look for something particular, like a RSA key. We *will*
    //  inadvertently parse garbage, like -----BEGIN FOO BAR-----. It will
    //  be caught later when a PEM_Load routine is called.

    static const size_t BAD_IDX = PEM_INVALID;

    // We use iterators for the search. However, an interator is invalidated
    //  after each insert that grows the container. So we save indexes
    //  from begin() to speed up searching. On each iteration, we simply
    //  reinitialize them.
    SecByteBlock::const_iterator it;
    size_t idx1 = BAD_IDX, idx2 = BAD_IDX, idx3 = BAD_IDX, idx4 = BAD_IDX;

    // The idea is to read chunks in case there are multiple keys or
    //  paramters in a BufferedTransformation. So we use CopyTo to
    //  extract what we are interested in. We don't take anything
    //  out of the BufferedTransformation (yet).

    // We also use indexes because the iterator will be invalidated
    //   when we append to the ByteQueue. Even though the iterator
    //   is invalid, `accum.begin() + index` will be valid.

    // Reading 8 or 10 lines at a time is an optimization from testing
    //   against cacerts.pem. The file has 153 certs, so its a good test.
    // +2 to allow for CR + LF line endings. There's no guarantee a line
    //   will be present, or it will be RFC1421_LINE_BREAK in size.
    static const size_t READ_SIZE = (RFC1421_LINE_BREAK + 1) * 10;
    static const size_t REWIND = std::max(SBB_PEM_BEGIN.size(), SBB_PEM_END.size()) + 2;

    SecByteBlock accum;
    size_t idx = 0, next = 0;

    size_t available = src.MaxRetrievable();
    while(available)
    {
        // How much can we read?
        const size_t size = (std::min)(available, READ_SIZE);

        // Ideally, we would only scan the line we are reading. However,
        //   we need to rewind a bit in case a token spans the previous
        //   block and the block we are reading. But we can't rewind
        //   into a previous index. Once we find an index, the variable
        //   next is set to it. Hence the reason for the max()
        if(idx > REWIND)
        {
            const size_t x = idx - REWIND;
            next = std::max(next, x);
        }

        // We need a temp queue to use CopyRangeTo. We have to use it
        //   because there's no Peek that allows us to peek a range.
        ByteQueue tq;
        src.CopyRangeTo(tq, static_cast<lword>(idx), static_cast<lword>(size));

        const size_t offset = accum.size();
        accum.Grow(offset + size);
        tq.Get(accum.data() + offset, size);

        // Adjust sizes
        idx += size;
        available -= size;

        // Locate '-----BEGIN'
        if(idx1 == BAD_IDX)
        {
            it = std::search(accum.begin() + next, accum.end(), SBB_PEM_BEGIN.begin(), SBB_PEM_BEGIN.end());
            if(it == accum.end())
                continue;

            idx1 = it - accum.begin();
            next = idx1 + SBB_PEM_BEGIN.size();
        }

        // Locate '-----'
        if(idx2 == BAD_IDX && idx1 != BAD_IDX)
        {
            it = std::search(accum.begin() + next, accum.end(), SBB_PEM_TAIL.begin(), SBB_PEM_TAIL.end());
            if(it == accum.end())
                continue;

            idx2 = it - accum.begin();
            next = idx2 + SBB_PEM_TAIL.size();
        }

        // Locate '-----END'
        if(idx3 == BAD_IDX && idx2 != BAD_IDX)
        {
            it = std::search(accum.begin() + next, accum.end(), SBB_PEM_END.begin(), SBB_PEM_END.end());
            if(it == accum.end())
                continue;

            idx3 = it - accum.begin();
            next = idx3 + SBB_PEM_END.size();
        }

        // Locate '-----'
        if(idx4 == BAD_IDX && idx3 != BAD_IDX)
        {
            it = std::search(accum.begin() + next, accum.end(), SBB_PEM_TAIL.begin(), SBB_PEM_TAIL.end());
            if(it == accum.end())
                continue;

            idx4 = it - accum.begin();
            next = idx4 + SBB_PEM_TAIL.size();
        }
    }

    // Did we find `-----BEGIN XXX-----` (RFC 1421 calls this pre-encapsulated boundary)?
    if(idx1 == BAD_IDX || idx2 == BAD_IDX)
        throw InvalidDataFormat("PEM_NextObject: could not locate boundary header");

    // Did we find `-----END XXX-----` (RFC 1421 calls this post-encapsulated boundary)?
    if(idx3 == BAD_IDX || idx4 == BAD_IDX)
        throw InvalidDataFormat("PEM_NextObject: could not locate boundary footer");

    // *IF* the trailing '-----' occurred in the last 5 bytes in accum, then we might miss the
    // End of Line. We need to peek 2 more bytes if available and append them to accum.
    if(available >= 2)
    {
        ByteQueue tq;
        src.CopyRangeTo(tq, static_cast<lword>(idx), static_cast<lword>(2));

        const size_t offset = accum.size();
        accum.Grow(offset + 2);
        tq.Get(accum.data() + offset, 2);
    }
    else if(available == 1)
    {
        ByteQueue tq;
        src.CopyRangeTo(tq, static_cast<lword>(idx), static_cast<lword>(1));

        const size_t offset = accum.size();
        accum.Grow(offset + 1);
        tq.Get(accum.data() + offset, 1);
    }

    // Final book keeping
    const byte* ptr = accum.begin() + idx1;
    const size_t used = idx4 + SBB_PEM_TAIL.size();
    const size_t len = used - idx1;

    // Include one CR/LF if its available in the accumulator
    next = idx1 + len;
    size_t adjust = 0;
    if(next < accum.size())
    {
        byte c1 = accum[next];
        byte c2 = 0;

        if(next + 1 < accum.size())
            c2 = accum[next + 1];

        // Longest match first
        if(c1 == '\r' && c2 == '\n')
            adjust = 2;
        else if(c1 == '\r' || c1 == '\n')
            adjust = 1;
    }

    dest.Put(ptr, len + adjust);
    dest.MessageEnd();

    src.Skip(used + adjust);
}

size_t PEM_ReadLine(BufferedTransformation& source, SecByteBlock& line, SecByteBlock& ending)
{
    if(!source.AnyRetrievable())
    {
        line.New(0);
        ending.New(0);

        return 0;
    }

    ByteQueue temp;

    while(source.AnyRetrievable())
    {
        byte b;
        if(!source.Get(b))
            throw Exception(Exception::OTHER_ERROR, "PEM_ReadLine: failed to read byte");

        // LF ?
        if(b == '\n')
        {
            ending = LF;
            break;
        }

        // CR ?
        if(b == '\r')
        {
            // CRLF ?
            if(source.AnyRetrievable() && source.Peek(b))
            {
                if(b == '\n')
                {
                    source.Skip(1);

                    ending = CRLF;
                    break;
                }
            }

            ending = CR;
            break;
        }

        // Not End-of-Line, accumulate it.
        temp.Put(b);
    }

    if(temp.AnyRetrievable())
    {
        line.Grow(temp.MaxRetrievable());
        temp.Get(line.data(), line.size());
    }
    else
    {
        line.New(0);
        ending.New(0);
    }

    // We return a line stripped of CRs and LFs. However, we return the actual number of
    //   of bytes processed, including the CR and LF. A return of 0 means nothing was read.
    //   A return of 1 means an empty line was read (CR or LF). A return of 2 could
    //   mean an empty line was read (CRLF), or could mean 1 character was read. In
    //   any case, line will hold whatever was parsed.
    return line.size() + ending.size();
}

SecByteBlock::const_iterator Search(const SecByteBlock& source, const SecByteBlock& target)
{
    return std::search(source.begin(), source.end(), target.begin(), target.end());
}

void PEM_Base64Decode(BufferedTransformation& source, BufferedTransformation& dest)
{
    Base64Decoder decoder(new Redirector(dest));
    source.TransferTo(decoder);
    decoder.MessageEnd();
}

void PEM_WriteLine(BufferedTransformation& bt, const SecByteBlock& line)
{
    bt.Put(line.data(), line.size());
    bt.Put(reinterpret_cast<const byte*>(RFC1421_EOL.data()), RFC1421_EOL.size());
}

void PEM_WriteLine(BufferedTransformation& bt, const std::string& line)
{
    bt.Put(reinterpret_cast<const byte*>(line.data()), line.size());
    bt.Put(reinterpret_cast<const byte*>(RFC1421_EOL.data()), RFC1421_EOL.size());
}

NAMESPACE_END

#endif

Licence
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following:
The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Bounty

I'm going to let the bounty run for the maximum time.

Useability feedback, or even style or efficiency feedback, is welcome, but the pricipal concern is if there is any concrete reason for the library not to be used in a production environment.

I'm not going to accept a "don't roll your own crypto" answer unless it provides a suitable alternative.
I'm not going to accept a "this all looks fine" answer unless it includes some analysis of why other people should trust the library.
That said, toward the end I'll upvote all answers that contribute to the collective assesment.

\$\endgroup\$
9
  • \$\begingroup\$ It is possible to post the entire solution here, I've posted question with over 3000 lines of code. Do you only want the code posted to be reviewed or do you want the entire solution reviewed? \$\endgroup\$
    – pacmaninbw
    May 7, 2019 at 15:07
  • \$\begingroup\$ I'm hoping people will look at the whole repository. I could certainly copy paste it here if you think that's a good idea, but it seems like reading the code sequentially like that would be harder, and it would clutter this page. Do you think I should? \$\endgroup\$ May 7, 2019 at 15:12
  • \$\begingroup\$ You break it up by file, make the file names bold. FYI, I've already downloaded the zip. \$\endgroup\$
    – pacmaninbw
    May 7, 2019 at 15:16
  • 2
    \$\begingroup\$ @pacmaninbw: done. You're right that it's not awful; I forget that SE has scroll boxes for very large code blocks. \$\endgroup\$ May 7, 2019 at 19:51
  • 2
    \$\begingroup\$ I've asked a question on meta about the necessity of including the Boost Software License in a question. \$\endgroup\$ May 11, 2019 at 17:52

1 Answer 1

5
+75
\$\begingroup\$

It is good to see cstdlib, EXIT_SUCCESS and EXIT_FAILURE in use. This is a very interesting concept with some good ideas. I admire your tenacity because it really must have been difficult to debug with all the functions in the headers.

It might be good to have a Installshield setup or some other mechanism to install the source code as well as the cryptopp810 header files.

Would using this library as directed provide the guarantees expected of a blind signature protocol?

I may not be able to provide a security audit, this review is based primarily on the code itself.

This is a set of binaries, where is the library? I would expect to see a shared library generated by the make file. I would also expect to see the header files contain only function prototypes or classes and not the functions themselves. Including the function bodies in the header file makes the project more difficult to maintain. It means that any time a bug is fixed or code changes then everything that includes the header file needs to recompile. Creating a shared library would be better because the library interfaces would stay constant and the user code would not have to rebuild, only re-link. The current implementation could lead to multiple definitions of functions at link time.

It might also be possible to eliminate the inclusion and use of the cryptopp810 header files and the using namespace CryptoPP; from the end users code if the library is implemented as a shared library rather than a header file. This would decrease compile/build times for the end user.

Embedding the cryptopp810 header files in the user space prevents the user from upgrading to newer libraries.

If the code continues to be in the header file it might be better to adopt the Boost Library filename.hpp extensions instead of .h.

I'm curious as to why the library wasn't implemented in a class for encapsulate and data hiding reasons.

The use of Macros as Constants in C++
C++ provides the const or constexpr to create symbolic constants, the use of #define is really the C programming language and is generally avoided because it is not type safe. The const symbolic constants have a type and can be checked at compile time. This stackoverflow question may provide more background. Using #ifdef and #define within header files to prevent repeated includes is still an accepted practice.

The try/catch Blocks
It might be better to include all of the successful code in the try block. The code is a little confusing the way it is implemented now.

Instead of

    try{
        message = argv[1];
        client_secret = Integer(argv[2]);
        public_key = ReadPEMPublicKey(argv[3]);
    }
    catch(std::runtime_error& e)
    {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    Integer hashed_message = GenerateHash(message);
    Integer hidden_message = MessageBlinding(hashed_message, public_key, client_secret);

    std::cout << std::hex << hidden_message << std::endl;
    return EXIT_SUCCESS;

This might be better, as it provides a continuos flow of the code.

    try{
        message = argv[1];
        client_secret = Integer(argv[2]);
        public_key = ReadPEMPublicKey(argv[3]);
        Integer hashed_message = GenerateHash(message);
        Integer hidden_message = MessageBlinding(hashed_message, public_key, client_secret);

        std::cout << std::hex << hidden_message << std::endl;
        return EXIT_SUCCESS;
    }
    catch(std::runtime_error& e)
    {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }

If this alternate method is used then the declarations for current global variables could be moved into the try block. Even in the current implementation the global variables should be declared as local variables in main().

Complexity
While the Single Responsibility Principle is primarily an object oriented it can and should be applied to functional programs as well. The Single Responsibility Principle states:

every module, class, or function should have responsibility over a single part of the functionality provided by the software...

The function void PEM_NextObject(BufferedTransformation& src, BufferedTransformation& dest) is overly complex and could be broken up into multiple functions.

The PEM code is also a good candidate for being turned into an object. Most of the PEM functions could be private functions in the class.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thank you for the above, Pacmanintw. Some of it is immediately actionable (although I probably won't get around to it this week!). As for giving a more traditional C++ style "library", I'm not sure of my way forward. The ideal solution would be to get the protocol added to the next version of Crypto++ itself, but I don't know if I'll have time in any sense of the word. \$\endgroup\$ May 16, 2019 at 0:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.