I have a Yubikey 5A, and have signed a PDF document using the standard yubico-piv-tool library, using the ECCP-256 algorithm, embedded into the PDF using the PoDoFo c++ library, now I have been trying to verify the signature using the OpenSSL library, the code is given below.
#include <cstddef>
#include <fstream>
#include <iostream>
#include <openssl/evp.h>
#include <ostream>
#include <podofo/base/PdfDictionary.h>
#include <podofo/base/PdfError.h>
#include <podofo/base/PdfObject.h>
#include <podofo/base/PdfReference.h>
#include <podofo/base/PdfString.h>
#include <podofo/doc/PdfSignatureField.h>
#include <podofo/podofo.h>
#include <stdio.h>
#include <vector>
#include <ykpiv/ykpiv.h>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
// max signature size is 256 BYTES
#define MAX_SIGNATURE_SIZE 256
// Function to compute the hash of PDF document content
std::string computePdfHash(const char *pdfPath) {
std::ifstream pdfFile(pdfPath, std::ios::binary);
std::vector<char> pdfContent((std::istreambuf_iterator<char>(pdfFile)),
std::istreambuf_iterator<char>());
size_t pdfContentSize = pdfContent.size();
// Compute hash of PDF document content
unsigned char pdfHash[EVP_MAX_MD_SIZE];
unsigned int pdfHashLen;
EVP_MD_CTX *mdctx = EVP_MD_CTX_new();
EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL);
EVP_DigestUpdate(mdctx, pdfContent.data(), pdfContentSize);
EVP_DigestFinal_ex(mdctx, pdfHash, &pdfHashLen);
EVP_MD_CTX_free(mdctx);
// Convert the hash to a string
return std::string(reinterpret_cast<const char *>(pdfHash), pdfHashLen);
}
void signWithYubiKey(const unsigned char *hash, size_t hashLen,
unsigned char *signature, size_t *signatureLen) {
const char *pin = "123456";
ykpiv_state *state;
ykpiv_rc res = ykpiv_init(&state, true);
if (res != YKPIV_OK) {
fprintf(stderr, "Failed to initialize ykpiv: %sn", ykpiv_strerror(res));
return;
}
res = ykpiv_connect(state, NULL);
if (res != YKPIV_OK) {
fprintf(stderr, "Failed to connect to ykpiv: %sn", ykpiv_strerror(res));
ykpiv_done(state);
return;
}
res = ykpiv_verify(state, pin, NULL);
if (res != YKPIV_OK) {
fprintf(stderr, "Failed to verify pin with ykpiv: %sn",
ykpiv_strerror(res));
ykpiv_done(state);
return;
}
res = ykpiv_sign_data(state, hash, hashLen, signature, signatureLen,
YKPIV_ALGO_ECCP256, YKPIV_KEY_AUTHENTICATION);
if (res != YKPIV_OK) {
fprintf(stderr, "Failed to sign data with ykpiv: %sn",
ykpiv_strerror(res));
ykpiv_done(state);
return;
}
ykpiv_done(state);
}
void signPdfDocument(const char *pdfPath) {
std::ifstream pdfFile(pdfPath, std::ios::binary);
std::vector<char> pdfContent((std::istreambuf_iterator<char>(pdfFile)),
std::istreambuf_iterator<char>());
size_t pdfContentSize = pdfContent.size();
PoDoFo::PdfMemDocument pdfDoc(pdfPath);
// Sign the hash with YubiKey
std::string pdfHash = computePdfHash(pdfPath);
unsigned char signature[MAX_SIGNATURE_SIZE];
size_t signatureLen = MAX_SIGNATURE_SIZE;
signWithYubiKey(reinterpret_cast<const unsigned char *>(pdfHash.c_str()),
pdfHash.size(), signature, &signatureLen);
PoDoFo::PdfPage *firstPage = pdfDoc.GetPage(0);
// Create a signature field rectangle, which is very small and made invisible
// by setting negative width and height, because PoDoFo does not support
// invisible signature fields
PoDoFo::PdfRect rect(0, 0, -1, -1);
// Create a signature field
PoDoFo::PdfSignatureField *signatureField =
new PoDoFo::PdfSignatureField(firstPage, rect, &pdfDoc);
signatureField->SetFieldName("Test Field Name");
PoDoFo::PdfDate signatureDate;
signatureField->SetSignatureDate(signatureDate);
signatureField->SetSignatureCreator("Test User");
signatureField->SetSignatureReason("Test Reason");
signatureField->SetAlternateName("Test Alternate Name");
// Set the signature data
PoDoFo::PdfData signatureData(reinterpret_cast<const char *>(signature),
signatureLen);
signatureField->SetSignature(signatureData);
// Save the PDF
pdfDoc.Write("signed_example.pdf");
}
bool verifySignatureWithCert(const std::string &rawSignature, X509 *cert,
const std::string &pdfContent) {
// Retrieve the public key from the certificate
EVP_PKEY *publicKey = X509_get_pubkey(cert);
if (!publicKey) {
std::cerr << "Failed to retrieve the public key from the certificate."
<< std::endl;
return false;
}
// Create an OpenSSL verification context
EVP_MD_CTX *verifyCtx = EVP_MD_CTX_new();
if (!verifyCtx) {
std::cerr << "Failed to create verification context." << std::endl;
EVP_PKEY_free(publicKey);
return false;
}
// Initialize the verification context with the public key and the appropriate
// hash algorithm
const EVP_MD *md = EVP_sha256();
if (EVP_DigestVerifyInit(verifyCtx, nullptr, md, nullptr, publicKey) != 1) {
std::cerr << "Failed to initialize verification context." << std::endl;
EVP_MD_CTX_free(verifyCtx);
EVP_PKEY_free(publicKey);
return false;
}
// Update the verification context with the raw signature data
if (EVP_DigestVerifyUpdate(verifyCtx, pdfContent.c_str(),
pdfContent.size()) != 1) {
std::cerr << "Failed to update verification context." << std::endl;
EVP_MD_CTX_free(verifyCtx);
EVP_PKEY_free(publicKey);
return false;
}
// Perform the signature verification
int verificationResult = EVP_DigestVerifyFinal(
verifyCtx, reinterpret_cast<const unsigned char *>(rawSignature.c_str()),
rawSignature.size());
if (verificationResult != 1) {
// Get the error message
unsigned long errCode = ERR_get_error();
char errMessage[120];
ERR_error_string_n(errCode, errMessage, sizeof(errMessage));
std::cerr << "Signature verification failed: " << errMessage << std::endl;
}
bool verificationSuccess = (verificationResult == 1);
// Free the verification context and public key
EVP_MD_CTX_free(verifyCtx);
EVP_PKEY_free(publicKey);
return verificationSuccess;
}
bool verifyPDF(const char *pdfPath, const char *certPath) {
try {
// Open the PDF document
PoDoFo::PdfMemDocument pdfDocObj(pdfPath);
PoDoFo::PdfMemDocument *pdfDoc = &pdfDocObj;
PoDoFo::PdfAcroForm *pAcroForm = pdfDoc->GetAcroForm();
if (!pAcroForm) {
std::cerr << "No AcroForm found in the PDF." << std::endl;
return false;
}
// Load the certificate from the file
FILE *certFile = fopen(certPath, "r");
if (!certFile) {
std::cerr << "Failed to open certificate file: " << certPath << std::endl;
return false;
}
// Read the certificate
X509 *cert = PEM_read_X509(certFile, nullptr, nullptr, nullptr);
fclose(certFile);
if (!cert) {
std::cerr << "Failed to read certificate from file: " << certPath
<< std::endl;
return false;
}
// Access the signature fields
PoDoFo::PdfObject *fields =
pAcroForm->GetObject()->GetIndirectKey(PoDoFo::PdfName("Fields"));
if (!fields || fields->IsNull()) {
std::cerr << "No fields found in the PDF." << std::endl;
X509_free(cert);
return false;
}
PoDoFo::PdfArray fieldArray = fields->GetArray();
std::cout << "Size of fieldArray: " << fieldArray.size() << std::endl;
// PDF does not contain any signature, hence not verified.
if (fieldArray.empty()) {
return false;
}
bool allVerified = true;
// Iterate through the fields
for (size_t i = 0; i < fieldArray.size(); i++) {
PoDoFo::PdfObject fieldObj = fieldArray[i];
// Get the object from the document using the resolved reference
PoDoFo::PdfObject *pObj =
pdfDoc->GetObjects().GetObject(fieldObj.GetReference());
// Check if the object is a dictionary and contains the "FT" key
if (pObj && pObj->IsDictionary()) {
PoDoFo::PdfDictionary &fieldDict = pObj->GetDictionary();
// Check if the object is a signature field
if (fieldDict.HasKey(PoDoFo::PdfName("FT")) &&
fieldDict.GetKey(PoDoFo::PdfName("FT"))->GetName() == "Sig") {
std::cout << "Got a signature field." << std::endl;
// The object is a signature field
// Retrieve signature data, i.e., it's value
PoDoFo::PdfObject *signatureRef =
fieldDict.GetKey(PoDoFo::PdfName("V"));
PoDoFo::PdfObject *pObj =
pdfDoc->GetObjects().GetObject(signatureRef->GetReference());
if (!pObj) {
std::cerr << "Failed to get signature object." << std::endl;
allVerified = false;
continue;
}
PoDoFo::PdfDictionary &signatureDict = pObj->GetDictionary();
if (!signatureDict.HasKey(PoDoFo::PdfName("Contents"))) {
std::cerr << "Signature field does not contain Contents key."
<< std::endl;
allVerified = false;
continue;
}
PoDoFo::PdfObject *signatureHex =
signatureDict.GetKey(PoDoFo::PdfName("Contents"));
if (!signatureHex) {
std::cerr << "Failed to get signature contents." << std::endl;
allVerified = false;
continue;
}
if (!signatureHex->IsHexString()) {
std::cerr << "Signature is not a hex string." << std::endl;
allVerified = false;
continue;
}
std::cout << "Value of signature field: "
<< signatureHex->GetString().GetUnicodeLength() << std::endl;
// Verify the signature using the provided certificate
std::string pdfHash = computePdfHash(pdfPath);
std::cout << "Computed hash of PDF for verification: " << pdfHash
<< std::endl;
bool verified = verifySignatureWithCert(
signatureHex->GetString().GetStringUtf8(), cert, pdfHash);
// Check the verification result
if (!verified) {
std::cerr << "Signature verification failed for signature field."
<< std::endl;
allVerified = false;
}
}
}
}
// Clean up the certificate
X509_free(cert);
// Return whether all fields were verified successfully
return allVerified;
} catch (const PoDoFo::PdfError &e) {
std::cerr << "Error: " << e.what() << std::endl;
return false;
}
}
int main() {
std::string choice;
std::cout << "Enter 0 to sign a PDF or 1 to verify a PDF (default is 0): ";
std::getline(std::cin, choice);
if (choice.empty()) {
choice = "0"; // Default choice
}
if (choice == "0") {
const char *pdfToSignPath = "example.pdf";
signPdfDocument(pdfToSignPath);
} else if (choice == "1") {
const char *pdfToVerifyPath = "signed_example.pdf";
const char *certPath = "cert.crt";
bool verified = verifyPDF(pdfToVerifyPath, certPath);
if (verified) {
std::cout << "The signature is verified." << std::endl;
} else {
std::cout << "The signature is not verified." << std::endl;
}
} else {
std::cerr << "Invalid choice. Please enter 0 or 1." << std::endl;
}
return 0;
}
My problem is that the code throws an error in the verifySignatureWithCert function,
// Perform the signature verification
int verificationResult = EVP_DigestVerifyFinal(
verifyCtx, reinterpret_cast<const unsigned char *>(rawSignature.c_str()),
rawSignature.size());
if (verificationResult != 1) {
// Get the error message
unsigned long errCode = ERR_get_error();
char errMessage[120];
ERR_error_string_n(errCode, errMessage, sizeof(errMessage));
std::cerr << "Signature verification failed: " << errMessage << std::endl;
}
And, the error message states,
error:02000077:rsa routines::wrong signature length
I can’t figure out how to change the algorithm in the verification logic, since I have signed using the ECCP-256, and the error states something about RSA routines, also, why is the signature length coming out to be wrong, since I am using the same PDF signed using the signPDF function, so shouldn’t be the signature length be 256 bytes ? ( I have tried changing the MAX_SIGNATURE_SIZE constant to 2048, in case the input is in bits, but no luck !! ).
Can someone nudge me in the right direction, if I should use something other than OpenSSL for verification, or if there is the signature being retrieved in the Hexadecimal format is causing some problem ?