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.
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:
Hash(s1)
, Hash(s2)
and Hash(s1+s2)
are printed.Hash(s1+s2)
is created using two StringSources
Hash(s1+s2)
is created using one StringSource
and one FileSource
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();