Search code examples
gorsadigital-signature

Verify and recover signed file in golang


I have signed file using openssl pkeyutl -sign -in data.txt -inkey key.pem -out data.sig

I can verify it and recover content using openssl pkeyutl -verifyrecover -in data.sig -pubin -inkey pubkey.pem -out recovered.txt

But I want to verify it and recover using golang. How can I do this? I want to load data.sig and pubkey.pem in go application and verify signature and recover data. I want to avoid exec.Command(openssl ...) stuff. So I found functions like rsa.DecryptPKCS1v15 but it wants private key instead of public and rsa.VerifyPKCS1v15 which returns only error and not []byte which I need to recover data.


Solution

  • Signing with openssl-pkeyutl allows low level signing. The statement

    openssl pkeyutl -sign -in data.txt -inkey key.pem -out data.sig
    

    generates a RSASSA-PKCS1-v1_5 signature, but without creating the DER encoding of the DigestInfo value from the message (which consists of the digest OID and the message hash).

    Libraries often implement the entire process, including the creation of the DER encoded DigestInfo, and are therefore not compatible. Thus, to reconstruct the data, you need a Go library that supports low level signing.
    If you cannot find such a library, you can implement it yourself (as the logic used in RSASSA-PKCS1-v1_5 is relatively simple).

    Sample implementation:

    import (
        "bytes"
        "crypto/rsa"
        "math/big"
    )
    
    func recover(ciphertext []byte, pubKey *rsa.PublicKey) []byte {
        decryptedPadded := decryptTextbook(ciphertext, pubKey)
        if len(decryptedPadded) != pubKey.Size() {
            return nil // wrong size
        }
        padding, decrypted := separate(decryptedPadded)
        if !verifyPadding(padding) {
            return nil // wrong padding
        }
        return decrypted
    }
    
    func decryptTextbook(ciphertext []byte, pub *rsa.PublicKey) []byte {
        ciphertextInt := new(big.Int)
        ciphertextInt.SetBytes(ciphertext)
        e := big.NewInt(int64(pub.E))
        decryptedInt := new(big.Int)
        decryptedInt.Exp(ciphertextInt, e, pub.N)
        decrypted := make([]byte, pub.Size())
        decryptedInt.FillBytes(decrypted)
        return decrypted
    }
    
    func separate(decryptedPadded []byte) ([]byte, []byte) {
        separatorIndex := bytes.Index(decryptedPadded[1:], []byte{0}) + 1
        return decryptedPadded[:separatorIndex], decryptedPadded[separatorIndex+1:]
    }
    
    func verifyPadding(padding []byte) bool { // check if padding is 0001FF..FF
        if len(padding) < 2+8 || padding[0] != 0 || padding[1] != 1 {
            return false
        } else {
            for i := 2; i < len(padding); i++ {
                if padding[i] != 255 {
                    return false
                }
            }
        }
        return true
    }
    

    Explanation:

    A typical use case for low level signing is, if only the message hash and the digest used are known but not the message itself.
    The user manually creates the DER encoding of the DigestInfo value and passes this to openssl-pkeyutl, which first pads the data by prepending the padding 0x0001FF...FF00 to the data, s. EMSA-PKCS1-v1_5. Then, the padded data is signed with textbook RSA.
    Note that the number of 0xFF in the padding is determined by the fact that the total length of the padded data must correspond to the key size. An additional condition is that the number of 0xFF is at least 8, which results in a maximum data size that is e.g. 256-8-3=245 bytes in the case of a 2048 bit key.

    In order to reconstruct the data, the above code must first recreate the padded data using textbook RSA (decryptTextbook()) and then separate the padding and data (separate()).
    The reconstruction of the data can be considered successful if the length of the padded data and the padding itself (verifyPadding()) are valid.