1
$\begingroup$

I am new to the forum, so forgive me if this question is out of place or lacks adequate detail.

I am working on a project that involves allowing discreet clients to upload content to a central host. The host is expected to verify ownership and data integrity by testing the signature of the content against a user's supplied public RSA key. For some reason, signatures provided by a test client I have written can not be verified by the host. Signatures created by the host for the sake of testing pass with no problem. (That is the host uses the client's private key to create a signature based on the same content, and test's that signature against the client's public key).

The host is written in Go and uses Go's native rsa package to verify a PKCS1v15 signature of a SHA256 sum. The client is written in Node.JS and uses a third party package, forge, to sign the data, also using PKCS1v15 and a SHA256 sum. I have also tried PSS in place of PKCS1v15 on both sides. The results appear to be the same. Can this sort of pattern (create signature on platform-A and verify it on platform-B) be implemented practically, or is RSA public key encryption open to the interpretation of the stack implementer?

NodeJS Implementation

The following is the client written in NodeJS. Here, the user's private key is read from the file "test-client-rsa." The variable "hex" is the hex-encoded SHA256 sum of the test file being used, hard-coded here for simplicity.

var fs = require('fs');
var forge = require('node-forge')

fs.readFile('test-client-rsa', (err, data) => {
   if (err) throw err;

    var key = forge.pki.privateKeyFromPem(data);
    var hex = 'dac1f3853c1e4c6579692f59af0a30aacb86820d5ad331c792987c4dae047595';
    var md = forge.md.sha256.create();
    md.update(hex, 'utf8');

    var signature = key.sign(md);
    var buf = new Buffer(signature);

    console.log(buf.toString('base64'));   
});

The client's output (encoded to Base64) resembles the following:

JShQKsKAVcKow4HCnMKMw7MXwp3DhhAPXGhGPsOow4U9DQg3acOmIEjDoFR6eCUCwpZ4wq7CmBB7dsOrSMKOw6fDlDfCs8OKwonCm8KVLjMrIsKSwqTDkmnCgcKlRsO4QQrCiWDDjBbClcOTwqfDn8KDw57DgG0Qw7gcHsKSwr57w4rClyrDqkgrwo5owoAHBMOGwq/DpMKDwoVhw5bDtD9zw4NYCxwLwrLCoTp7wo3CtsOqPjFKwrdWw63CpMKQFcOcwpnCpMKhwrDCisOkBCZrw4wkwoYVwotWw7/DrBYSIsKjbXF9wqfDnRTCk0kTE8KTVMOSw6wicElvw45rwolbwpNgS8O+bSwhwqFHw5ICTcKRZcO0w7YOwoTCncOow4pBwo1ow7bCisKYwrRRMiTDpTLDnsKjQcOBwpgPw7rCokRtOcKTP8KiwppLwp3Dn2QYwqZvDEsRw4LCuiYbM8K5w6fClF98EW/CmUTCjcOjMcOHwqLDqsKpwqfDvcKgNA==

Go Lang Implementation

The following is the server implementation, written in Go. The code has been simplified to keep the example focused, and so lacks proper error handling and logging routines. In the method, the object "u" represents the user's identity, or RSA credentials in this example. The variable "u.pkey" is the user's RSA public key represented as bytes, while the input parameters "sig" and "data" represent the signature provided by the test client for the test file, and the content's of the test file itself respectively.

func (u User) VerifySignature(sig []byte, data []byte) bool {
    if !u.IsValid() {
        return false
    }

    hash := sha256.Sum256(data)

    if err := rsa.VerifyPKCS1v15(u.pkey, crypto.SHA256, hash[:], sig); err == nil {
        return true
    } 

    return false    
}

The server's output is consistently false with the call to "rsa.VerifyPKCS1v15" returning the error "crypto/rsa: verification error."

I've run through both layers of code several times now, and I am not able to spot the problem in the logic. Is there something incorrect in this pattern? Any help that can be provided would be greatly appreciated.

$\endgroup$
5
  • 1
    $\begingroup$ Normally, PKCS1v15 (RSASSA-PKCS1-v1_5 is the more formal name) encodes the hash function used as a part of the padding when it generates the signature. I don't see how your NodeJS signer does this; it appears to pass the hash output, but not the hash function used, to the underlying RSA implementation. Could this be your problem? For that matter, are you sure that the NodeJS implementation does any padding at all? $\endgroup$
    – poncho
    Sep 13, 2016 at 4:04
  • 1
    $\begingroup$ In concur with poncho: unless mdcomes with a method allowing to know the hash used, forge.pki.privateKeyFromPem.sign can't know which hash was used, even from its size (that could be SHA-512/256). This could be an indication that it uses no padding, or equivalently PKCS1v1 padding mode 0, allowing certain attacks, and breaking compatibility with RSASSA-PKCS1-v1_5. It could also be that your signer code hashes a message twice as long as the verifier does, consisting of the rendition in (lowercase) hexadecimal of the actual message. Check if changing hexto uppercase changesmd. $\endgroup$
    – fgrieu
    Sep 13, 2016 at 4:47
  • $\begingroup$ @poncho, I am unsure of the padding used by the forge package. This could actually be the problem. I will do some research and update you with my findings. $\endgroup$
    – s.napier
    Sep 14, 2016 at 3:22
  • $\begingroup$ @fgrieu, you may be correct. I will try your suggestion and let you know what I find. $\endgroup$
    – s.napier
    Sep 14, 2016 at 3:23
  • $\begingroup$ I now know a little more than I did when I first posted this question. NodeJS expects the actual data to be to signed in "md.update" called, not a hash of the data. I've made this change, but the test continues to fail. I believe the earlier comments are correct, there is a discrepancy in the padding used between the two implementations. I am still researching for a fix. $\endgroup$
    – s.napier
    Sep 15, 2016 at 1:10

2 Answers 2

2
$\begingroup$

Yes, RSA signature generation and verification should work identically on different libraries. PKCS#1 - the dominant standard for signature generation and verification - has a complete and formal description of how the configuration / input is transformed into the output.

This includes:

  1. the key specifications (and possible encoding);
  2. the hashing;
  3. the padding and hash indicators;
  4. the modular exponentiation;
  5. the transformation into octets of the result.

There are test vectors available from NIST as well, so it is possible to (partially) verify the correctness of an implementation against well known test vectors.


That doesn't mean that there may not be differences in implementation between libraries, so some libraries:

  1. require a key in a specific format (and error handling if the format is incorrect is not always up to par);
  2. require CRT (Chinese Remainder Theorem) parameters for a key;
  3. require a pre-calculated hash value, some libraries an initialized hash function and some libraries just need an indication of the hash function (and some just assume a specific hash function, often SHA-1);
  4. offer implicit character encoding for when the input is a text string (this may also be implicit for the programming language / runtime);
  5. allow for online calculation of the hash / signature by offering update / final functionality or streaming;
  6. implicitly or explicitly perform the choice of the padding mode (PKCS#1, PSS, ISO 9797, RAW, etc.);
  7. offer to encode or even implicitly encode the result into a text string.

Less common, but still possible is the way bytes are ordered for lower level functions. You may also have to configure implementation specific parameters, such as a random number generator for protecting the implementation or performing PSS signature generation. PSS may also require parameters - a second hash function for instance - for Mask Generation Function (MGF-1).

These are all design choices that may influence the outcome of the signature generation or verification. So a developer still needs to study the API to known what to expect, despite the well written PKCS#1 standard.


Fortunately, once the algorithm works it is unlikely to fail unless you manage to bugger up the character encoding of the message or the encoding / decoding of the signature.


It seems you got stuck on (3). Double calculation of the hash is a common mistake. Note that PKCS#1 signature describes the signing of messages and includes the hashing within the signature generation description, so having the signature also generate the hash is common among cryptographic APIs.

$\endgroup$
2
  • $\begingroup$ Answered more in a checklist style format to make this more useful to other readers. $\endgroup$
    – Maarten Bodewes
    Sep 16, 2016 at 11:16
  • $\begingroup$ Thank you for you input @maarten-bodewes. Your answer is very detailed and easy to follow. $\endgroup$
    – s.napier
    Sep 17, 2016 at 1:41
1
$\begingroup$

I solved my problem with a little outside help and some more research. It looks more and more like the node-forge package I was using is doing some proprietary signature padding, which prevented my Go server from parsing the message correctly. I have replaced logic for the node-forge package with Node JS' native crypto package. This conjoined with the knowledge that Node expects the raw message in the call to sign, not the hash, got everything working smoothly.

Node does not appear to have native support of RSASSA-PSS padding, which is a disadvantage, but I believe that PKCS will suffice for now.

Here is a sample of the completed Node test app:

var fs = require('fs');
var crypto = require('crypto');

fs.readFile('keys/test-client-rsa', (err, testKey) => {
   if (err) throw err;

    fs.readFile('files/test-upload.txt', (err, testFile) => {
        if (err) throw err;

        var sign = crypto.createSign('RSA-SHA256');
        sign.update(testFile);

        console.log(sign.sign(testKey, 'base64'))
    });
});
$\endgroup$
3
  • $\begingroup$ PKCS#1 padding for signing - as far as I know - has no significant weaknesses. It can be a bit awkward in the sense that it is deterministic. But otherwise signature generation should be fine. As long as you understand that PKCS#1 padding for encryption does have significant weaknesses. $\endgroup$
    – Maarten Bodewes
    Sep 16, 2016 at 10:34
  • 1
    $\begingroup$ console.log(sign.sign(testKey, 'base64')) red flag: log statement should have no side effects, put the call to sign on a separate line (sorry, doing 3 code reviews at the same time, so I'm still in red pencil mode)! $\endgroup$
    – Maarten Bodewes
    Sep 16, 2016 at 10:35
  • $\begingroup$ You are correct @MaartenBodewes, there are currently no known exploits against PCKS1 and the chances for collision are low. PSS does have the potential for a longer life of no exploits, though, and I am eager to deploy it. Maybe in a not too distant Node build. I also respect your note on breaking out the sign call from the log call, but for the sake of a simple test, it suffices :-) Hopefully more experienced implementers will know enough not to follow my shoddy examples. $\endgroup$
    – s.napier
    Sep 17, 2016 at 1:46

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.