Search code examples
c++goopensslcryptographyecdsa

ECDSA signature verification: Go vs OpenSSL


I'm trying to verify an ECDSA signature for a hash using a public key. I've written a small Go program that successfully does this but I've been unable to port it to C++.

Here is my input data:

  • Public key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDd9VXmpHjV4voFO+0ZoFPlRr5icZXquxsr9EkaOUO9B7Wl1DAgGI0EKCm1++Bl2Od32xZFeFuG07OTpTMVOCPA==
  • Hash: 562d6ddfb3ceb5abb12d97bc35c4963d249f55b7c75eda618d365492ee98d469
  • Signature: 304502204d6d070117d445f4c2fcdbd4df037a1c8cfee2a166353c2e562cd5efd06e914d022100bb06439ded1478bd19022519dc06a84ba18ea4bf30ea9eb9ea90f8b66dad12c7

Here is my working go program:

package main

import (
    "crypto/ecdsa"
    "crypto/x509"
    "encoding/base64"
    "encoding/hex"
)

func main() {
    key_, _ := base64.StdEncoding.DecodeString("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDd9VXmpHjV4voFO+0ZoFPlRr5icZXquxsr9EkaOUO9B7Wl1DAgGI0EKCm1++Bl2Od32xZFeFuG07OTpTMVOCPA==")
    key, _ := x509.ParsePKIXPublicKey(pkey_)
    msg, _ := hex.DecodeString("562d6ddfb3ceb5abb12d97bc35c4963d249f55b7c75eda618d365492ee98d469")
    sig, _ := hex.DecodeString("304502204d6d070117d445f4c2fcdbd4df037a1c8cfee2a166353c2e562cd5efd06e914d022100bb06439ded1478bd19022519dc06a84ba18ea4bf30ea9eb9ea90f8b66dad12c7")

    valid := ecdsa.VerifyASN1(key.(*ecdsa.PublicKey), msg[:], sig)

    if !valid {
      panic("key invalid")
    }
}

And here is my broken C++ program that fails at the very last assertion. I'd like to know why that is:

#include <cassert>
#include <cstdlib>
#include <string>
#include <stdexcept>

#include <openssl/bio.h>
#include <openssl/ec.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/pem.h>

std::string decode_hex_str(std::string const &hex) {
  std::string str;
  for (std::size_t i { 0 }; i < hex.size(); i += 2) {
    auto byte { hex.substr(i,2) };
    str.push_back(std::strtol(byte.c_str(), nullptr, 16));
  }

  return str;
}

int main() {
  std::string key_ {
R"(
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDd9VXmpHjV4voFO+0ZoFPlRr5icZXquxsr9EkaOUO9B7Wl1DAgGI0EKCm1++Bl2Od32xZFeFuG07OTpTMVOCPA==
-----END PUBLIC KEY-----
)"
  };

  std::string msg { decode_hex_str("562d6ddfb3ceb5abb12d97bc35c4963d249f55b7c75eda618d365492ee98d469") };
  std::string sig { decode_hex_str("304502204d6d070117d445f4c2fcdbd4df037a1c8cfee2a166353c2e562cd5efd06e914d022100bb06439ded1478bd19022519dc06a84ba18ea4bf30ea9eb9ea90f8b66dad12c7") };

  auto bio { BIO_new_mem_buf(key_.data(), key_.length()) };
  assert(bio);

  auto key { PEM_read_bio_EC_PUBKEY(bio, nullptr, nullptr, nullptr) };

  EVP_MD_CTX *mdctx { nullptr };
  EVP_PKEY *pkey { nullptr };

  int status;
  bool verified;

  mdctx = EVP_MD_CTX_new();
  assert(mdctx);

  pkey = EVP_PKEY_new();
  assert(pkey);

  assert(EVP_PKEY_assign_EC_KEY(pkey, key) == 1);

  assert(EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pkey) == 1);

  assert(EVP_DigestVerifyUpdate(mdctx, msg.data(), msg.length()) == 1);

  status = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char const *>(sig.data()), sig.length());

  switch (status) {
    case 0:
      verified = false;
      break;
    case 1:
      verified = true;
      break;
    default:
      assert(false);
  }

  EVP_MD_CTX_free(mdctx);
  EVP_PKEY_free(pkey);

  assert(verified);
}

Solution

  • EVP_DigestVerify{Init/Update/Final} do two things:

    • They hash the input data
    • Then they verify the computed hash against the signature

    ecdsa.VerifyASN1 on the other hand does only the second step from the above, already taking in the hash as input.

    The OpenSSL function that does that is ECDSA_verify.

    So you can do something like:

    BIO* bio = BIO_new_mem_buf(key_.data(), key_.length());
    
    EC_KEY* key = PEM_read_bio_EC_PUBKEY(bio, nullptr, nullptr, nullptr);
    
    int verified = ECDSA_verify(
        0,
        (unsigned char const*)msghash.data(), msghash.length(),
        (unsigned char const*)sig.data(), sig.length(),
        key
    );
    
    std::cout << verified << std::endl;
    

    Should print 1.


    Note that the variable naming in your code is confusing - the variable msg actually contains the message digest. So msghash or digest would be a better name for it. It is important to be precise in variable naming when doing crypto.