I am trying to verify a signature with openssl 1.1.1k, but I have trouble importing the DER-encoded SPKI formatted public key that I generated with SubtleCrypto Web Crypto API.
Decoded public key with https://holtstrom.com/:
SEQUENCE {
SEQUENCE {
OBJECTIDENTIFIER 1.2.840.10045.2.1 (ecPublicKey)
OBJECTIDENTIFIER 1.3.132.0.34 (P-384)
}
BITSTRING 0x04b8546c630d4c48195e071d109d36ecbddf328274c6882f6f9de9c112d8691e5428f08baeee7d2f2bf4ea888f759d5313cd4f1ed14862f1d5a24f69520242b116702cc5e573bc7deb392042b8a3a8f00e13f90e69f9c45a8b0ce60aae2c74dcef : 0 unused bit(s)
}
The C++ code with all of my 3 tries. I also commented the errors from the openssl library and its string representation next to the line where the issue occurred:
#include <iostream>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
//include OpenSSL headers
#include <cassert>
#include <openssl/sha.h>
#include <openssl/bn.h>
#include <openssl/hmac.h>
#include <openssl/ec.h>
#include <openssl/ecdsa.h>
#include <openssl/obj_mac.h>
#include <openssl/opensslv.h>
#include <openssl/engine.h>
#include <openssl/ssl.h>
#include <sstream>
#include <vector>
#include <openssl/err.h>
static const std::string base64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
static inline bool is_base64(unsigned char c) {
return (isalnum(c) || (c == '+') || (c == '/'));
}
namespace {
std::string officalHash = "MTY0MjY5ODgyMg==";
std::string officalSignature = "YkTPCGVj1Su+b4cqrOQPSwxHh79BTd9HZk/0OH71HjIbQ8/lSuKOEeNcpY9O7+4vgabIDRlyH5QmZmMV7X9s8eCk4cU7RAfn2YwE2pSvoik+upILLS9qmIDSaDz6LU2x";
const auto officalPublicKey = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEuFRsYw1MSBleBx0QnTbsvd8ygnTGiC9vnenBEthpHlQo8Iuu7n0vK/TqiI91nVMTzU8e0Uhi8dWiT2lSAkKxFnAsxeVzvH3rOSBCuKOo8A4T+Q5p+cRaiwzmCq4sdNzv";
struct BIOFreeAll { void operator()(BIO* p) { BIO_free_all(p); } };
}
std::string base64_decode(std::string const& encoded_string);
void tryOne(const std::vector<uint8_t>& binaryPublicKey);
void tryTwo(const std::vector<uint8_t>& binaryPublicKey);
void tryThree(const std::vector<uint8_t>& binaryPublicKey);
int main()
{
SSL_library_init();
SSL_load_error_strings();
const auto hashBytes = base64_decode(officalHash);
const auto publicKeyTemp = officalPublicKey;
std::unique_ptr<BIO, BIOFreeAll> b64(BIO_new(BIO_f_base64()));
BIO_set_flags(b64.get(), BIO_FLAGS_BASE64_NO_NL);
BIO* source = BIO_new_mem_buf(officalPublicKey, -1); // read-only source
BIO_push(b64.get(), source);
const int maxlen = strlen(officalPublicKey) / 4 * 3 + 1;
std::vector<uint8_t> decoded(maxlen);
const int len = BIO_read(b64.get(), decoded.data(), maxlen);
decoded.resize(len);
tryOne(decoded);
tryTwo(decoded);
tryThree(decoded);
return 0;
}
void tryOne(const std::vector<uint8_t>& binaryPublicKey)
{
EVP_PKEY* key;
EC_KEY* ec_key = EC_KEY_new_by_curve_name(NID_secp384r1);
EVP_PKEY* ret = EVP_PKEY_new();
EVP_PKEY_assign_EC_KEY(ret, ec_key);
unsigned char const* ptr = binaryPublicKey.data();
key = d2i_PublicKey(EVP_PKEY_EC, &ret, &ptr, binaryPublicKey.size());
if (!key) {
std::string error4 = ERR_error_string(ERR_get_error(), nullptr); //<- Error: error:10067066:elliptic curve routines:ec_GFp_simple_oct2point:invalid encoding
return;
}
}
void tryTwo(const std::vector<uint8_t>& binaryPublicKey)
{
const auto tempstr = base64_decode(officalSignature);
std::vector<uint8_t> SignatureBytes(tempstr.begin(), tempstr.end());
std::shared_ptr<EVP_MD_CTX> mdctx = std::shared_ptr<EVP_MD_CTX>(EVP_MD_CTX_create(), EVP_MD_CTX_free);
const EVP_MD* md = EVP_get_digestbyname("SHA512");
if (nullptr == md)
{
return;
}
if (0 == EVP_VerifyInit_ex(mdctx.get(), md, NULL))
{
return;
}
if (0 == EVP_VerifyUpdate(mdctx.get(), binaryPublicKey.data(), binaryPublicKey.size()))
{
return;
}
std::shared_ptr<BIO> b644 = std::shared_ptr<BIO>(BIO_new(BIO_f_base64()), BIO_free);
BIO_set_flags(b644.get(), BIO_FLAGS_BASE64_NO_NL);
std::shared_ptr<BIO> bPubKey = std::shared_ptr<BIO>(BIO_new(BIO_s_mem()), BIO_free);
BIO_puts(bPubKey.get(), officalPublicKey);
BIO_push(b644.get(), bPubKey.get());
std::shared_ptr<EVP_PKEY> pubkey = std::shared_ptr<EVP_PKEY>(d2i_PUBKEY_bio(b644.get(), NULL), EVP_PKEY_free);
if (!pubkey) {
std::string error = ERR_error_string(ERR_get_error(), nullptr);
return;
}
auto b = EVP_VerifyFinal(mdctx.get(), SignatureBytes.data(), SignatureBytes.size(), pubkey.get());
std::string error = ERR_error_string(ERR_get_error(), nullptr); // <- Error: error:0D0680A8:asn1 encoding routines:asn1_check_tlen:wrong tag
}
void tryThree(const std::vector<uint8_t>& binaryPublicKey)
{
EC_KEY* eckey = NULL;
std::vector<uint8_t> pubKeyVC = binaryPublicKey;
const unsigned char* pubKeyVCp = pubKeyVC.data();
const unsigned char** pubKeyVCpp = &pubKeyVCp;
eckey = EC_KEY_new_by_curve_name(NID_secp384r1);
EC_KEY_set_asn1_flag(eckey, OPENSSL_EC_NAMED_CURVE);
eckey = o2i_ECPublicKey(&eckey, pubKeyVCpp, pubKeyVC.size());
if (!EC_KEY_check_key(eckey)) {
std::string error = ERR_error_string(ERR_get_error(), nullptr); //<- Error: "error:0D07803A:asn1 encoding routines:asn1_item_embed_d2i:nested asn1 error"
}
}
std::string base64_decode(std::string const& encoded_string) {
int in_len = encoded_string.size();
int i = 0;
int j = 0;
int in_ = 0;
unsigned char char_array_4[4], char_array_3[3];
std::string ret;
while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
char_array_4[i++] = encoded_string[in_]; in_++;
if (i == 4) {
for (i = 0; i < 4; i++)
char_array_4[i] = base64_chars.find(char_array_4[i]);
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (i = 0; (i < 3); i++)
ret += char_array_3[i];
i = 0;
}
}
if (i) {
for (j = i; j < 4; j++)
char_array_4[j] = 0;
for (j = 0; j < 4; j++)
char_array_4[j] = base64_chars.find(char_array_4[j]);
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (j = 0; (j < i - 1); j++) ret += char_array_3[j];
}
return ret;
}
The key generation: The public key was generated with SubtleCrypto in javascript:
function generateEcdsaKeypair() {
let keypair = window.crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-384"
},
true,
["sign", "verify"]
)
return keypair
}
then it was exported using the SubjectPublicKeyInfo format which is is defined in RFC 5280, Section 4.1 using the ASN.1 notation:
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
async function exportCryptoKey(key) {
const exported = await window.crypto.subtle.exportKey(
"spki",
key
);
const exportedAsString = ab2str(exported);
const exportedAsBase64 = window.btoa(exportedAsString);
console.log("PublicKey: ", exportedAsBase64);
}
For signing the data, I used the following function:
function signEcdsa(privateKey, data) {
let signature = window.crypto.subtle.sign(
{
name: 'ECDSA',
hash: { name: 'SHA-512' },
},
privateKey,
data
)
return signature
}
But I can not even convert the base64 string into a public key. Not to mention the verification.
The whole generation files:
The js.html file:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Programiz</title>
</head>
<body>
<script src="js.js"></script>
</body>
</html>
js.js file: The output can be found in the inspection window under the Console tab.
function generateEcdsaKeypair() {
let keypair = window.crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-384"
},
true,
["sign", "verify"]
)
return keypair
}
// Generates a signature of the data given
function signEcdsa(privateKey, data) {
let signature = window.crypto.subtle.sign(
{
name: 'ECDSA',
hash: { name: 'SHA-512' },
},
privateKey,
data
)
return signature
}
function verifyEcdsa(publicKey, data, signature) {
let result = window.crypto.subtle.verify(
{
name: 'ECDSA',
hash: { name: 'SHA-512' },
},
publicKey,
signature,
data
)
return result
}
function _arrayBufferToBase64( buffer ) {
var binary = '';
var bytes = new Uint8Array( buffer );
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return window.btoa( binary );
}
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
function toHexString(byteArray) {
return Array.from(byteArray, function(byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('')
}
function b64EncodeUnicode(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode('0x' + p1);
}));
}
function b64DecodeUnicode(str) {
return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}
function addNewLines(str) {
var finalString = '';
for(var i=0; i < str.length; i++) {
finalString += str.substring(0, 64) + '\n';
str = str.substring(64);
}
finalString += str;
return finalString;
}
function removeLines(pem) {
var lines = pem.split('\n');
var encodedString = '';
for(var i=0; i < lines.length; i++) {
encodedString += lines[i].trim();
}
return encodedString;
}
function stringToArrayBuffer(byteString){
var byteArray = new Uint8Array(byteString.length);
for(var i=0; i < byteString.length; i++) {
byteArray[i] = byteString.codePointAt(i);
}
return byteArray;
}
function arrayBufferToString(exportedPrivateKey){
var byteArray = new Uint8Array(exportedPrivateKey);
var byteString = '';
for(var i=0; i < byteArray.byteLength; i++) {
byteString += String.fromCodePoint(byteArray[i]);
}
return byteString;
}
async function exportCryptoKey(key) {
const exported = await window.crypto.subtle.exportKey(
"spki",
key
);
const exportedAsString = ab2str(exported);
const exportedAsBase64 = window.btoa(exportedAsString);
console.log("PublicKey: ", exportedAsBase64);
console.log(toHexString(exported));
var privateKeyDer = arrayBufferToString(exported); //pkcs#8 to DER
var privateKeyB64 = b64EncodeUnicode(privateKeyDer); //btoa(privateKeyDer);
var privateKeyPEMwithLines = addNewLines(privateKeyB64); //split PEM into 64 character strings
var privateKeyPEMwithoutLines = removeLines(privateKeyPEMwithLines); //join PEM
var privateKeyDerDecoded = b64DecodeUnicode(privateKeyPEMwithoutLines); // atob(privateKeyB64);
var privateKeyArrayBuffer = stringToArrayBuffer(privateKeyDerDecoded); //DER to arrayBuffer
console.log(exported);
console.log(privateKeyDer);
console.log(privateKeyB64);
console.log(privateKeyPEMwithLines);
console.log(privateKeyPEMwithoutLines);
console.log(privateKeyDerDecoded);
console.log(privateKeyArrayBuffer);
/*const pemExported = `-----BEGIN PUBLIC KEY-----\n${exportedAsBase64}\n-----END PUBLIC KEY-----`;
const exportKeyOutput = document.querySelector(".exported-key");
exportKeyOutput.textContent = pemExported;*/
}
function arrayBufferToString(exportedPrivateKey){
var byteArray = new Uint8Array(exportedPrivateKey);
var byteString = '';
for(var i=0; i < byteArray.byteLength; i++) {
byteString += String.fromCodePoint(byteArray[i]);
}
return byteString;
}
async function testSignEcdsa() {
let {
privateKey: aPrivate,
publicKey: aPublic
} = await generateEcdsaKeypair()
// A sends and signs data for B using their own private key
let data = window.crypto.getRandomValues(new Uint8Array(64))
let signature = await signEcdsa(aPrivate, data)
// B verifies A's signature using A's public key
let result = await verifyEcdsa(aPublic, data, signature)
let base64result = _arrayBufferToBase64(signature)
let signatureBase64 = window.btoa(String.fromCharCode.apply(null, new Uint8Array(signature)))
let dataBase64 = window.btoa(String.fromCharCode.apply(null, new Uint8Array(data)))
// Tests
// Signature is verified
console.log('Data signed/verified successfully: ', result)
console.log('Signature: ', signatureBase64)
console.log('Data: ', dataBase64)
exportCryptoKey(aPublic);
}
testSignEcdsa()
UPDATE:
I put this here so others do not need to go through the openssl's documentation about how to convert r|s signature to ans1 der encoding. Do not forget to check error codes:
std::vector<uint8_t> Vault::convertP1363EncodingSignatureToASN1Der(const std::string& signatureInHexFormat) const
{ std::unique_ptr< ECDSA_SIG, std::function<void(ECDSA_SIG*)>> zSignature(ECDSA_SIG_new(), [](ECDSA_SIG* b) { ECDSA_SIG_free(b); });
std::unique_ptr< BIGNUM, std::function<void(BIGNUM*)>> r(nullptr, [](BIGNUM* b) { BN_free(b); });
BIGNUM* r_ptr = r.get();
std::unique_ptr< BIGNUM, std::function<void(BIGNUM*)>> s(nullptr, [](BIGNUM* b) { BN_free(b); });
BIGNUM* s_ptr = s.get();
const std::string sSignatureR = signatureInHexFormat.substr(0, signatureInHexFormat.size() / 2);
const std::string sSignatureS = signatureInHexFormat.substr(signatureInHexFormat.size() / 2);
BN_hex2bn(&r_ptr, sSignatureR.c_str());
BN_hex2bn(&s_ptr, sSignatureS.c_str());
ECDSA_SIG_set0(zSignature.get(), r_ptr, s_ptr);
unsigned char buffer[256];
unsigned char* pbuffer = buffer;
int signatureLength = i2d_ECDSA_SIG(zSignature.get(), nullptr);
signatureLength = i2d_ECDSA_SIG(zSignature.get(), &pbuffer);
return { buffer, buffer + signatureLength };
}
tryTwo()
allows a successful verification of the posted data with the following changes:
In addition to key and signature, the message itself is also required for verification. However, the message is not used at all in the current code. It must be specified in VerifyUpdate()
(instead of the public key):
void tryTwo(const std::vector<uint8_t>& binaryPublicKey)
{
const auto hashBytes = base64_decode(officalHash);
...
if (0 == EVP_VerifyUpdate(mdctx.get(), hashBytes.c_str(), hashBytes.size()))
{
...
By the way, officalHash
and hashBytes
are misleading names because the unhashed message is applied.
WebCrypto generates the signature in IEEE P1363 (r|s) format, while EVP_VerifyFinal()
expects the signature in ASN.1/DER format. The posted signature in ASN.1/DER format is (Base64 encoded):
std::string officalSignature = "MGUCMGJEzwhlY9Urvm+HKqzkD0sMR4e/QU3fR2ZP9Dh+9R4yG0PP5UrijhHjXKWPTu/uLwIxAIGmyA0Zch+UJmZjFe1/bPHgpOHFO0QH59mMBNqUr6IpPrqSCy0vapiA0mg8+i1NsQ==";
The relation between both formats is explained e.g. here.
With these two changes, verification is successful on my machine.