c++crypto++dsa

How to combine two Sources into new one in Crypto++?


Situation

I have two arbitrary sources, lets say a StringSource from a signature and a FileSource from the corresponding signed file. I now want to verify the files signature which is currently performed like this:

bool VerifyFile(const ECDSA<ECP, SHA512>::PublicKey &key,
                const std::string &filename,
                const std::string &signatureString) {
    std::string fileContentString;
    FileSource(filename.c_str(), true,
               new CryptoPP::StringSink(fileContentString));

    bool result = false;
    StringSource(signatureString + fileContentString, true,
                 new SignatureVerificationFilter(
                         ECDSA<ECP, SHA512>::Verifier(key),
                         new ArraySink((byte *) &result, sizeof(result))
                 ) // SignatureVerificationFilter
    );
    return result;
}

My Problem

I don't want to explicitly extract the file's content to a string, then do a concatenation and verify afterwards.

Question

Is there a way to pass two arbitrary sources where one represents the signature and the other one the signed content (might be a file or a string) to the verification entity?

What I tried so far

I tried Source::TransferAll(...) to a Redirecter, redirecting to SignatureVerificationFilter with no luck.


Solution

  • I have two arbitrary sources, lets say a StringSource from a signature and a FileSource from the corresponding signed file. I now want to verify the files signature ...

    Using multiple sources on the same filter chain can be tricky. I know the library has some baked-in classes for it but I have never liked them. They take multiple input channels and de-multiplexes them into a single channel. You can see them in action in test.cpp, functions SecretRecoverFile (around line 650) and InformationRecoverFile (around line 700).


    Is there a way to pass two arbitrary sources where one represents the signature and the other one the signed content (might be a file or a string) to the verification entity?

    Here is how I would handle what you want to do. The example below uses two sources and shares a filter chain. I reduced the complexity by hashing two strings using a HashFilter. Your example uses message, signature, key pairs and SignatureVerificationFilter but it is more complex than needed to show you how to do it.

    The example proceeds in four parts:

    To state the obvious, the simplified example calculates Hash(s1+s2). In your context the operation is Verify(key, s1+s2), where key is the public key, s1 is the signature and s2 is the contents of the file.

    Part 0 - The data is setup below. It is pretty boring. Notice s3 is a concatenation of s1 and s2.

    std::string s1, s2, s3;
    const size_t size = 1024*16+1;
    
    random_string(s1, size);
    random_string(s2, size);
    
    s3 = s1 + s2;
    

    Part 1 - The data is printed below. The hashes of s1, s2 and s3 are printed. s3 is the important one. s3 is what we need to arrive at using two separate sources.

    std::string r;
    StringSource ss1(s1, true, new HashFilter(hash, new StringSink(r)));
    
    std::cout << "s1: ";
    hex.Put((const byte*)r.data(), r.size());
    std::cout << std::endl;
    
    r.clear();
    StringSource ss2(s2, true, new HashFilter(hash, new StringSink(r)));
    
    std::cout << "s2: ";
    hex.Put((const byte*)r.data(), r.size());
    std::cout << std::endl;
    
    r.clear();
    StringSource ss3(s3, true, new HashFilter(hash, new StringSink(r)));
    
    std::cout << "s3: ";
    hex.Put((const byte*)r.data(), r.size());
    std::cout << std::endl;
    

    Output looks like so:

    $ ./test.exe
    s1: 45503354F9BC56C9B5B61276375A4C60F83A2F01
    s2: 6A3AD5B683DE7CA57F07E8099268A8BC80FA200B
    s3: BFC1882CEB24697A2B34D7CF8B95604B7109F28D
    ...
    

    Part 2 - This is where things get interesting. We use two different StringSource to process s1 and s2 individually.

    StringSource ss4(s1, false);
    StringSource ss5(s2, false);
    
    HashFilter hf1(hash, new StringSink(r));
    
    ss4.Attach(new Redirector(hf1));
    ss4.Pump(LWORD_MAX);
    ss4.Detach();
    
    ss5.Attach(new Redirector(hf1));
    ss5.Pump(LWORD_MAX);
    ss5.Detach();
    
    hf1.MessageEnd();
    
    std::cout << "s1 + s2: ";
    hex.Put((const byte*)r.data(), r.size());
    std::cout << std::endl;
    

    It produces the following output:

    $ ./test.exe
    s1: 45503354F9BC56C9B5B61276375A4C60F83A2F01
    s2: 6A3AD5B683DE7CA57F07E8099268A8BC80FA200B
    s3: BFC1882CEB24697A2B34D7CF8B95604B7109F28D
    s1 + s2: BFC1882CEB24697A2B34D7CF8B95604B7109F28D
    ...
    

    There are several things going on in the code above. First, we dynamically attach and detach the hash filter chain to sources ss4 and ss5.

    Second, once the filter is attached we use Pump(LWORD_MAX) to pump all the data from the source into the filter chain. We don't use PumpAll() because PumpAll() signals the end of the current message and generates a MessageEnd(). We are processing one message in multiple parts; we are not processing multiple messages. So we want only one MessageEnd() when we determine.

    Third, once we are done with the source, we call Detach so StringSource destructors don't cause a spurious MessageEnd() message to enter the filter chain. Again, we are processing one message in multiple parts; we are not processing multiple messages. So we want only one MessageEnd() when we determine.

    Fourth, when we are done sending our data into the filter, we call hf.MessageEnd() to tell the filter to process all pending or buffered data. This is when we want the MessageEnd() call, and not before.

    Fifth, we call Detach() when done rather than Attach(). Detach() deletes the existing filter chain and avoids memory leaks. Attach() attaches a new chain but does not delete the existing filter or chain. Since we are using a Redirector our HashFilter survives. The HashFilter is eventually cleaned as an automatic stack variable.

    As an aside, if ss4.PumpAll() and ss5.PumpAll() were used (or allowed destructors to send MessageEnd() into the filter chain) then you would get a concatenation of Hash(s1) and Hash(s2) because it would look like two different messages to the filter instead of one message over two parts. The code below is wrong:

    StringSource ss4(s1, false);
    StringSource ss5(s2, false);
    
    HashFilter hf1(hash, new StringSink(r));
    
    ss4.Attach(new Redirector(hf1));
    // ss4.Pump(LWORD_MAX);
    ss4.PumpAll();  // MessageEnd
    ss4.Detach();
    
    ss5.Attach(new Redirector(hf1));
    // ss5.Pump(LWORD_MAX);
    ss5.PumpAll();  // MessageEnd
    ss5.Detach();
    
    // Third MessageEnd
    hf1.MessageEnd();
    

    The incorrect code above produces Hash(s1) || Hash(s2) || Hash(<empty string>):

    $ ./test.exe
    s1: 45503354F9BC56C9B5B61276375A4C60F83A2F01
    s2: 6A3AD5B683DE7CA57F07E8099268A8BC80FA200B
    s3: BFC1882CEB24697A2B34D7CF8B95604B7109F28D
    s1 + s2: 45503354F9BC56C9B5B61276375A4C60F83A2F016A3AD5B683DE7CA57F07E8099268A8BC80FA200BDA39A3EE5E6B4B0D3255BFEF95601890AFD80709
    

    Part 3 - This is your use case. We use a StringSource and FileSource to process s1 and s2 individually. Remember, the string s2 was written to a file named test.dat.

    StringSource ss6(s1, false);
    FileSource fs1("test.dat", false);
    
    HashFilter hf2(hash, new StringSink(r));
    
    ss6.Attach(new Redirector(hf2));
    ss6.Pump(LWORD_MAX);
    ss6.Detach();
    
    fs1.Attach(new Redirector(hf2));
    fs1.Pump(LWORD_MAX);
    fs1.Detach();
    
    hf2.MessageEnd();
    
    std::cout << "s1 + s2 (file): ";
    hex.Put((const byte*)r.data(), r.size());
    std::cout << std::endl;
    

    Here is what running the full example looks like:

    $ ./test.exe
    s1: 45503354F9BC56C9B5B61276375A4C60F83A2F01
    s2: 6A3AD5B683DE7CA57F07E8099268A8BC80FA200B
    s3: BFC1882CEB24697A2B34D7CF8B95604B7109F28D
    s1 + s2: BFC1882CEB24697A2B34D7CF8B95604B7109F28D
    s1 + s2 (file): BFC1882CEB24697A2B34D7CF8B95604B7109F28D
    

    Notice s3 = s1 + s2 = s1 + s2 (file).


    $ cat test.cxx
    
    #include "cryptlib.h"
    #include "filters.h"
    #include "files.h"
    #include "sha.h"
    #include "hex.h"
    
    #include <string>
    #include <iostream>
    
    void random_string(std::string& str, size_t len)
    {
        const char alphanum[] =
            "0123456789"
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
            "abcdefghijklmnopqrstuvwxyz";
        const size_t size = sizeof(alphanum) - 1;
    
        str.reserve(len);
        for (size_t i = 0; i < len; ++i)
            str.push_back(alphanum[rand() % size]);
    }
    
    int main(int argc, char* argv[])
    {
        using namespace CryptoPP;
    
        ////////////////////////// Part 0 //////////////////////////
    
        // Deterministic
        std::srand(0);
    
        std::string s1, s2, s3, r;
        const size_t size = 1024*16+1;
    
        random_string(s1, size);
        random_string(s2, size);
    
        // Concatenate for verification
        s3 = s1 + s2;
    
        // Write s2 to file
        StringSource(s2, true, new FileSink("test.dat"));
    
        // Hashing, resets after use
        SHA1 hash;
    
        // Printing hex encoded string to std::cout
        HexEncoder hex(new FileSink(std::cout));
    
        ////////////////////////// Part 1 //////////////////////////
    
        r.clear();
        StringSource ss1(s1, true, new HashFilter(hash, new StringSink(r)));
    
        std::cout << "s1: ";
        hex.Put((const byte*)r.data(), r.size());
        std::cout << std::endl;
    
        r.clear();
        StringSource ss2(s2, true, new HashFilter(hash, new StringSink(r)));
    
        std::cout << "s2: ";
        hex.Put((const byte*)r.data(), r.size());
        std::cout << std::endl;
    
        r.clear();
        StringSource ss3(s3, true, new HashFilter(hash, new StringSink(r)));
    
        std::cout << "s3: ";
        hex.Put((const byte*)r.data(), r.size());
        std::cout << std::endl;
    
        ////////////////////////// Part 2 //////////////////////////
    
        r.clear();
        StringSource ss4(s1, false);
        StringSource ss5(s2, false);
    
        HashFilter hf1(hash, new StringSink(r));
    
        ss4.Attach(new Redirector(hf1));
        ss4.Pump(LWORD_MAX);
        ss4.Detach();
    
        ss5.Attach(new Redirector(hf1));
        ss5.Pump(LWORD_MAX);
        ss5.Detach();
    
        hf1.MessageEnd();
    
        std::cout << "s1 + s2: ";
        hex.Put((const byte*)r.data(), r.size());
        std::cout << std::endl;
    
        ////////////////////////// Part 3 //////////////////////////
    
        r.clear();
        StringSource ss6(s1, false);
        FileSource fs1("test.dat", false);
    
        HashFilter hf2(hash, new StringSink(r));
    
        ss6.Attach(new Redirector(hf2));
        ss6.Pump(LWORD_MAX);
        ss6.Detach();
    
        fs1.Attach(new Redirector(hf2));
        fs1.Pump(LWORD_MAX);
        fs1.Detach();
    
        hf2.MessageEnd();
    
        std::cout << "s1 + s2 (file): ";
        hex.Put((const byte*)r.data(), r.size());
        std::cout << std::endl;
    
        return 0;
    }
    

    And:

    $ g++ test.cxx ./libcryptopp.a -o test.exe
    $ ./test.exe
    s1: 45503354F9BC56C9B5B61276375A4C60F83A2F01
    s2: 6A3AD5B683DE7CA57F07E8099268A8BC80FA200B
    s3: BFC1882CEB24697A2B34D7CF8B95604B7109F28D
    s1 + s2: BFC1882CEB24697A2B34D7CF8B95604B7109F28D
    s1 + s2 (file): BFC1882CEB24697A2B34D7CF8B95604B7109F28D
    

    Here's a class that may ease your pain. It brings together the concepts above in a MultipleSources class. MultipleSources is only a partial implementation of the Source interface, but it should have all the pieces you need.

    class MultipleSources
    {
    public:
        MultipleSources(std::vector<Source*>& source, Filter& filter)
        : m_s(source), m_f(filter)
        {
        }
    
        void Pump(lword pumpMax, bool messageEnd)
        {
            for (size_t i=0; pumpMax && i<m_s.size(); ++i)
            {
                lword n = pumpMax;
                m_s[i]->Attach(new Redirector(m_f));            
                m_s[i]->Pump2(n);
                m_s[i]->Detach();
                pumpMax -= n;
            }
    
            if (messageEnd)
                m_f.MessageEnd();
        }
    
        void PumpAll()
        {
            for (size_t i=0; i<m_s.size(); ++i)
            {
                m_s[i]->Attach(new Redirector(m_f));
                m_s[i]->Pump(LWORD_MAX);
                m_s[i]->Detach();
            }
    
            m_f.MessageEnd();
        }
    
    private:
        std::vector<Source*>& m_s;
        Filter &m_f;
    };
    

    You would call it like so:

    StringSource ss(s1, false);
    FileSource fs("test.dat", false);
    HashFilter hf(hash, new StringSink(r));
    
    std::vector<Source*> srcs;
    srcs.push_back(&ss);
    srcs.push_back(&fs);
    
    MultipleSources ms(srcs, hf);
    ms.Pump(LWORD_MAX, false);
    
    hf.MessageEnd();
    

    Or you can use PumpAll and get the same result, but you don't call hf.MessageEnd(); in this case because PumpAll signals the end of the message.

    MultipleSources ms(srcs, hf);
    ms.PumpAll();