在 Delphi 中使用 OpenSSL 验证 SHA256 签名失败

Verifying SHA256 signature with OpenSSL in Delphi fails

我正在尝试使用 OpenSSL libeay32.dll 在 Delphi 中实施 SHA256 签名和验证。因此,在第一步中,我使用以下 OpenSSL 命令创建了一个 RSA 2048 位密钥对:

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem

就这么简单。我做的下一步是创建一个能够从 PEM 文件中读取 public 和私钥的函数:

function TSignSHA256.ReadKeyFile( aFileName : String; aType : TKeyFileType ) : pEVP_PKEY;
var locFile : RawByteString;
    locBIO  : pBIO;
begin
  locFile := UTF8Encode( aFileName );

  locBIO := BIO_new( BIO_s_file() );

  try
    BIO_read_filename( locBIO, PAnsiChar(locFile) );

    result := NIL;
    case aType of
      kfPrivate : result := PEM_read_bio_PrivateKey( locBIO, result, nil, nil );
      kfPublic  : result := PEM_read_bio_PUBKEY( locBIO, result, nil, nil );
    end;
  finally
    BIO_free( locBIO );
  end;
end;

这似乎也有效。所以我实现了一些签名程序:

procedure TSignSHA256.Sign;
var locData   : RawByteString;
    locKey    : pEVP_PKEY;
    locCtx    : pEVP_MD_CTX;
    locSHA256 : pEVP_MD;
    locSize   : Cardinal;
    locStream : TBytesStream;
begin
  locKey  := ReadKeyFile( 'private.pem', kfPrivate );
  locData := ReadMessage( 'message.txt' );

  locCtx := EVP_MD_CTX_create;
  try
    locSHA256 := EVP_sha256();

    EVP_DigestSignInit( locCtx, NIL, locSHA256, NIL, locKey );
    EVP_DigestSignUpdate( locCtx, PAnsiChar(locData), Length(locData) );
    EVP_DigestSignFinal( locCtx, NIL, locSize );

    locStream := TBytesStream.Create;
    try
      locStream.SetSize( locSize );
      EVP_DigestSignFinal( locCtx, PAnsiChar( locStream.Memory ), locSize );
      WriteSignature( 'message.sig', locStream.Bytes, locSize );
    finally
      FreeAndNIL(locStream);
    end;
  finally
    EVP_MD_CTX_destroy( locCtx );
  end;
end;

如您所见,该过程正在读取名为 message.txt 的文件,计算签名并将该 sig 存储到 message.sig 。如果我 运行 下面的 OpenSSL 命令结果是 Verified OK:

openssl dgst -sha256 -verify public.pem -signature message.sig message.txt

看来我的签名程序也能正常工作。所以我终于实现了一个验证程序:

function TSignSHA256.Verify : Boolean;
var locData   : RawByteString;
    locSig    : TArray<Byte>;
    locKey    : pEVP_PKEY;
    locCtx    : pEVP_MD_CTX;
    locSHA256 : pEVP_MD;
    locSize   : Cardinal;
    locStream : TBytesStream;
begin
  locKey  := ReadKeyFile( 'public.pem', kfPublic );
  locData := ReadMessage( 'message.txt' );
  locSig  := ReadSignature( 'message.sig' );
  locSize := Length(locSig);

  locCtx := EVP_MD_CTX_create;
  try
    locSHA256 := EVP_sha256();

    EVP_DigestVerifyInit( locCtx, NIL, EVP_sha256(), NIL, locKey ); //Returns 1
    EVP_DigestVerifyUpdate( locCtx, PAnsiChar(locData), Length(locData) ); //Returns 1

    locStream := TBytesStream.Create( locSig );
    try
      result := ( EVP_DigestVerifyFinal( locCtx, PAnsiChar(locStream.Memory), locSize ) = 1 ); //Returns false! WHY???
    finally
      FreeAndNIL(locStream);
    end;
  finally
    EVP_MD_CTX_destroy( locCtx );
  end;
end;

如您所见,我实施此过程的方式与实施签名过程的方式完全相同。不幸的是,结果是 false。 OpenSSL返回的错误码是

error04091077:lib(4):func(145):reason:(119)

转换为lib RSA、函数int_rsa_verify中的错误,原因错误的签名长度。我搜索了 Google 但没有找到有关该错误的任何有用信息。我也试图了解 OpenSSL 源代码,但我对 C 的了解并不深入,而且似乎要花很长时间才能搞清楚。

我个人的感觉是我读public键有误。但这只是一种感觉,我不知道如何以不同的方式做到这一点。 我的第二个猜测是我在验证过程中初始化上下文时做错了。但我不知道那可能是什么。

为什么签名验证失败?

签名不是文本签名。它由一个字节数组组成,其中的字节可以具有任何值。您正在将该字节数组直接与 ANSI 字符串相互转换。如果数组包含 ANSI 范围之外的值(无论可能是什么,我假设是 ASCII),那将失败。

您需要将签名视为二进制数据。如果需要将其视为字符串(包含文本),则可以使用 base 64 编解码器。

好的,我找到了解决方案。事实上,我不得不处理两个错误。第一个错误是我以错误的方式将签名传递给 EVP_DigestVerifyFinal。这就是 Maarten Bodewes 在他的回答中所说的,我会接受它作为我问题的答案。

第二个问题是在我对 DLL 入口点的定义中。我已经将 EVP_DigistVerifyFinal 的第三个参数声明为 var 参数。可能是复制和过去的错误,因为 EVP_DigistSignFinal 的第三个参数是一个 var 参数。

对于所有必须做同样事情的人,我 post 在此提供我的解决方案。它的灵感来自阅读 EVP Signing and Verifying, DelphiOpenSSL 和 OpenSSL 资源(主要是 dgst.c)。代码是使用 Delphi XE2 实现和测试的。

请注意,我的代码没有进行任何错误处理,也没有太在意释放内存。这意味着代码尚未准备好生产,您应该小心使用它!

导入单位:

unit uOpenSSLCrypt;

interface

type
  pBIO = Pointer;
  pBIO_METHOD = Pointer;

  pEVP_MD_CTX = Pointer;
  pEVP_MD = Pointer;

  pEVP_PKEY_CTX = Pointer;
  pEVP_PKEY = Pointer;

  ENGINE = Pointer;

  TPWCallbackFunction = function( buffer : PAnsiChar; length : Integer; verify : Integer; data : Pointer ) : Integer; cdecl;

  //Error functions
  function ERR_get_error : Cardinal; cdecl;
  function ERR_error_string( e : Cardinal; buf : PAnsiChar ) : PAnsiChar; cdecl;

  function ERR_GetErrorMessage : String;

  //BIO functions
  function BIO_new( _type : pBIO_METHOD ) : pBIO; cdecl;
  function BIO_new_file( const aFileName : PAnsiChar; const aMode : PAnsiChar ) : pBIO; cdecl;
  function BIO_free(a: pBIO): integer; cdecl;
  function BIO_s_file : pBIO_METHOD; cdecl;
  function BIO_f_md : pBIO_METHOD; cdecl;
  function BIO_ctrl( bp : pBIO; cmd : Integer; larg : Longint; parg : Pointer ) : Longint; cdecl;
  function BIO_read( b : pBIO; buf : Pointer; len : Integer ) : integer; cdecl;
  function BIO_get_md_ctx(bp: pBIO; mdcp: Pointer): Longint;
  function BIO_read_filename( bp : pBIO; filename : PAnsiChar ) : Integer;

  function PEM_read_bio_PrivateKey( bp : pBIO; x : pEVP_PKEY; cb : TPWCallbackFunction; u : pointer ) : pEVP_PKEY; cdecl;
  function PEM_read_bio_PUBKEY( bp : pBIO; x : pEVP_PKEY; cb : TPWCallbackFunction; u : Pointer ) : pEVP_PKEY; cdecl;

  //EVP functions
  function EVP_MD_CTX_create() : pEVP_MD_CTX; cdecl;
  procedure EVP_MD_CTX_destroy( ctx : pEVP_MD_CTX ); cdecl;
  function EVP_sha256() : pEVP_MD; cdecl;

  function EVP_PKEY_size(key: pEVP_PKEY): integer; cdecl;
  function EVP_DigestSignInit( aCtx : pEVP_MD_CTX; aPCtx : pEVP_PKEY_CTX; aType : pEVP_MD; aEngine : ENGINE; aKey : pEVP_PKEY  ) : Integer; cdecl;
  function EVP_DigestSignUpdate( ctx : pEVP_MD_CTX; const d : Pointer; cnt : Cardinal ) : Integer; cdecl;
  function EVP_DigestSignFinal( ctx : pEVP_MD_CTX; const d : PByte; var cnt : Cardinal ) : Integer; cdecl;

  function EVP_DigestVerifyInit( aCtx : pEVP_MD_CTX; aPCtx : pEVP_PKEY_CTX; aType : pEVP_MD; aEngine : ENGINE; aKey : pEVP_PKEY  ) : Integer; cdecl;
  function EVP_DigestVerifyUpdate( ctx : pEVP_MD_CTX; const d : Pointer; cnt : Cardinal ) : Integer; cdecl;
  function EVP_DigestVerifyFinal( ctx : pEVP_MD_CTX; const d : PByte; cnt : Cardinal ) : Integer; cdecl;

  function CRYPTO_malloc( aLength : LongInt; const f : PAnsiChar; aLine : Integer ) : Pointer; cdecl;
  procedure CRYPTO_free( str : Pointer ); cdecl;

const BIO_C_SET_FILENAME = 108;
      BIO_C_GET_MD_CTX   = 120;

      BIO_CLOSE   = ;
      BIO_FP_READ = ;

implementation

uses System.SysUtils, Windows;

const LIBEAY_DLL_NAME = 'libeay32.dll';

function ERR_get_error : Cardinal; external LIBEAY_DLL_NAME;
function ERR_error_string;         external LIBEAY_DLL_NAME;

function ERR_GetErrorMessage : String;
var locErrMsg: array [0..160] of Char;
begin
  ERR_error_string( ERR_get_error, @locErrMsg );
  result := String( StrPas( PAnsiChar(@locErrMsg) ) );
end;

function BIO_new;                 external LIBEAY_DLL_NAME;
function BIO_new_file;            external LIBEAY_DLL_NAME;
function BIO_free;                external LIBEAY_DLL_NAME;
function BIO_ctrl;                external LIBEAY_DLL_NAME;
function BIO_s_file;              external LIBEAY_DLL_NAME;
function BIO_f_md;                external LIBEAY_DLL_NAME;
function BIO_read;                external LIBEAY_DLL_NAME;

function BIO_get_md_ctx( bp : pBIO; mdcp : Pointer ) : Longint;
begin
  result := BIO_ctrl( bp, BIO_C_GET_MD_CTX, 0, mdcp );
end;

function BIO_read_filename( bp : pBIO; filename : PAnsiChar ) : Integer;
begin
  result := BIO_ctrl( bp, BIO_C_SET_FILENAME, BIO_CLOSE or BIO_FP_READ, filename );
end;

function PEM_read_bio_PrivateKey;   external LIBEAY_DLL_NAME;
function PEM_read_bio_PUBKEY;       external LIBEAY_DLL_NAME;

function EVP_MD_CTX_create;   external LIBEAY_DLL_NAME;
procedure EVP_MD_CTX_destroy; external LIBEAY_DLL_NAME;
function EVP_sha256;          external LIBEAY_DLL_NAME;

function EVP_PKEY_size;         external LIBEAY_DLL_NAME;
function EVP_DigestSignInit;    external LIBEAY_DLL_NAME;
function EVP_DigestSignUpdate;  external LIBEAY_DLL_NAME name 'EVP_DigestUpdate';
function EVP_DigestSignFinal;   external LIBEAY_DLL_NAME;

function EVP_DigestVerifyInit;   external LIBEAY_DLL_NAME;
function EVP_DigestVerifyUpdate; external LIBEAY_DLL_NAME name 'EVP_DigestUpdate';
function EVP_DigestVerifyFinal;  external LIBEAY_DLL_NAME;

function CRYPTO_malloc; external LIBEAY_DLL_NAME;
procedure CRYPTO_free;  external LIBEAY_DLL_NAME;

end.

实施:

unit uSignSHA256;

interface

uses uOpenSSLCrypt;

type
  TKeyFileType = ( kfPrivate, kfPublic );

  TSignSHA256 = class(TObject)
  private
    function ReadKeyFile( aFileName : String; aType : TKeyFileType ) : pEVP_PKEY;
    function ReadMessage( aName : String ) : RawByteString;
    function ReadSignature( aName : String; var aLength : Cardinal ) : Pointer;
    procedure FreeSignature( aSig : Pointer );

    procedure WriteSignature( aName : String; aSignature : TArray<Byte>; aLength : Integer );

  public
    constructor Create;
    destructor Destroy; override;

    procedure Sign( aKeyFile : String; aMsgFile : String; aSigFile : String );
    function Verify( aKeyFile : String; aMsgFile : String; aSigFile : String ) : Boolean;
  end;

implementation

uses System.Classes, System.SysUtils;

{ TSignSHA256 }

constructor TSignSHA256.Create;
begin

end;

destructor TSignSHA256.Destroy;
begin

  inherited;
end;

procedure TSignSHA256.FreeSignature( aSig : Pointer );
begin
  CRYPTO_free( aSig );
end;

function TSignSHA256.ReadKeyFile( aFileName : String; aType : TKeyFileType ) : pEVP_PKEY;
var locFile : RawByteString;
    locBIO  : pBIO;
begin
  locFile := UTF8Encode( aFileName );

  locBIO := BIO_new( BIO_s_file() );

  try
    BIO_read_filename( locBIO, PAnsiChar(locFile) );

    result := NIL;
    case aType of
      kfPrivate : result := PEM_read_bio_PrivateKey( locBIO, nil, nil, nil );
      kfPublic  : result := PEM_read_bio_PUBKEY( locBIO, nil, nil, nil );
    end;
  finally
    BIO_free( locBIO );
  end;
end;

function TSignSHA256.ReadMessage( aName : String ) : RawByteString;
var locFileStream : TFileStream;
    locSize       : Cardinal;
    locBytes      : TArray<Byte>;
    locText       : String;
begin
  locFileStream := TFileStream.Create( aName, fmOpenRead );
  try
    locSize := locFileStream.Size;

    SetLength(locBytes, locSize);
    locFileStream.Read( locBytes[0], locSize );
  finally
    FreeAndNIL(locFileStream);
  end;

  SetString( locText, PAnsiChar(locBytes), locSize );
  result := UTF8Encode( locText );
end;

function TSignSHA256.ReadSignature( aName : String; var aLength : Cardinal ) : Pointer;
var locSigBio : pBIO;
    locFile   : RawByteString;
    locMode   : RawByteString;
begin
  locFile := UTF8Encode( aName );
  locMode := UTF8Encode('rb');

  locSigBio := BIO_new_file( PAnsiChar(locFile), PAnsiChar(locMode) );
  try
    result := CRYPTO_malloc( aLength, NIL, 0 );
    aLength := BIO_read( locSigBio, result, aLength );
  finally
    BIO_free( locSigBio );
  end;
end;

procedure TSignSHA256.Sign( aKeyFile : String; aMsgFile : String; aSigFile : String );
var locData   : RawByteString;
    locKey    : pEVP_PKEY;
    locCtx    : pEVP_MD_CTX;
    locSHA256 : pEVP_MD;
    locSize   : Cardinal;
    locStream : TBytesStream;
begin
  locKey  := ReadKeyFile( aKeyFile, kfPrivate );
  locData := ReadMessage( aMsgFile );

  locCtx := EVP_MD_CTX_create;
  try
    locSHA256 := EVP_sha256();

    EVP_DigestSignInit( locCtx, NIL, locSHA256, NIL, locKey );
    EVP_DigestSignUpdate( locCtx, PAnsiChar(locData), Length(locData) );
    EVP_DigestSignFinal( locCtx, NIL, locSize );

    locStream := TBytesStream.Create;
    try
      locStream.SetSize( locSize );
      EVP_DigestSignFinal( locCtx, PByte( locStream.Memory ), locSize );
      WriteSignature( aSigFile, locStream.Bytes, locSize );
    finally
      FreeAndNIL(locStream);
    end;
  finally
    EVP_MD_CTX_destroy( locCtx );
  end;
end;

function TSignSHA256.Verify( aKeyFile : String; aMsgFile : String; aSigFile : String ) : Boolean;
var locData   : RawByteString;
    locSig    : Pointer;
    locKey    : pEVP_PKEY;
    locBio    : pBIO;
    locCtx    : pEVP_MD_CTX;
    locKeyCtx : pEVP_PKEY_CTX;
    locSHA256 : pEVP_MD;
    locSize   : Cardinal;
    locStream : TBytesStream;
begin
  locKey    := ReadKeyFile( aKeyFile, kfPublic );
  locData   := ReadMessage( aMsgFile );
  locSize   := EVP_PKEY_size( locKey );

  locBio := BIO_new( BIO_f_md );
  try
    BIO_get_md_ctx( locBio, @locCtx );
    locSHA256 := EVP_sha256();

    EVP_DigestVerifyInit( locCtx, NIL, locSHA256, NIL, locKey );
    EVP_DigestVerifyUpdate( locCtx, PAnsiChar(locData), Length(locData) );

    try
      locSig := ReadSignature( aSigFile, locSize );
      result := ( EVP_DigestVerifyFinal( locCtx, PByte(locSig), locSize ) = 1 );
    finally
      FreeSignature( locSig );
    end;
  finally
    BIO_free( locBio );
  end;
end;

procedure TSignSHA256.WriteSignature( aName : String; aSignature : TArray<Byte>; aLength : Integer );
var locFileStream : TFileStream;
begin
  locFileStream := TFileStream.Create( aName, fmCreate );
  try
    locFileStream.Write( aSignature[0], aLength );
  finally
    FreeAndNIL(locFileStream);
  end;
end;

end.