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.
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.