Home 
username password  
Welcome, Guest.
Your IP: 18.97.14.82
2025-02-10 10:56:18 
 Public Support
 Verify content or digest using RSA signature
Bottom
 
Total posts: 12
 Author Verify content or digest using RSA signature
Peter Royston

2024-05-02 12:47:30
Registered user
I'm trying to use StreamSec Tools 4.0 to verify a SAML repsonse. There are several steps involved, but eventually I need to check the signature of content in various canonicolized xml nodes.

Thus, I started by comparing results against openssl, as follows:

function VerifyContent(Content, PubKey, Signature: ansistring; ContentIsDigest: boolean): boolean;
var
  RSAKey: TstRSAKey;
  lPwd: iSecretKey;
  lSS: TStringStream;
  MS: TMemoryStream;
  stRSAPublicKey1: TstRSAPublicKey;
begin
    lSS := TStringStream.Create(MIME64ToStr(OSToBytes(PubKey)));
    stRSAPublicKey1 := TstRSAPublicKey.Create(nil);
    try
      stRSAPublicKey1.Name := 'stRSAPublicKey1';
      stRSAPublicKey1.EnabledDesignTime := False;
      stRSAPublicKey1.EnabledRunTime := False;

      stRSAPublicKey1.PublicKeyFormat.SelectedOptionNames := 'PKCS8-RSAPublic-Open';

      stRSAPublicKey1.SetPublicKey(stRSAPublicKey1.PublicKeyFormat.LoadFromStream(lSS,stRSAPublicKey1));

      stRSAPublicKey1.SignEncoding := seEMSA3;
      stRSAPublicKey1.EncryptEncoding := eeEME_PKCS1_v1_5;
      stRSAPublicKey1.DigestAlgorithm := haSHA256;
      stRSAPublicKey1.MGFHashAlgorithm := haSHA256;

      if ContentIsDigest
        then result := stRSAPublicKey1.VerifySignedDigest(Content,Signature,sfBinary,sfBinary,false)
        else result := stRSAPublicKey1.VerifySignature(Content,Signature,sfBinary{sfBase64});
    finally
      stRSAPublicKey1.Free;
      lSS.Free;
    end;
end;

function VerifySAMLResponse(XMLText, PubKey: ansistring): boolean;
var
  Digest: ansistring;
begin
  (*
  openssl dgst -sha256 content.txt > content_digest.txt

  openssl dgst -sign private_key7.pvk -keyform pem -sha256 -out digest.txt.sig -binary content_digest.txt

  openssl dgst -verify public_key7.pem -keyform pem -sha256 -signature digest.txt.sig -binary content_digest.txt
  Verified OK  <<<<<<<<<<<<<<<<<<<<<<<<
  *)

  Digest := MIME64ToStr(OSToBytes(Trim(filetostring('C:\Temp\SAML\digest_mime.txt'))));
  result := VerifyContent(Digest, PubKey, filetostring('C:\Temp\SAML\digest.txt.sig'), true);
  //...false/fail

  result := VerifyContent(filetostring('C:\Temp\SAML\content.txt'), PubKey, filetostring('C:\Temp\SAML\digest.txt.sig'), false);
  //...false/fail
end;

Can you spot what I'm doing wrong?

Thanks,
Todd
Peter Royston

2024-05-02 12:56:49
Registered user
Here's a little more for context...

function TestSAML: string;

  function RemovePEM(PEMContent: String): string;
  var
    M: TMatch;
  begin
    M := TRegEx.Match(PEMContent,'-----BEGIN.*?-----(.*)-----END.*?-----',[roSingleLine]);
    if M.Success
      then result := M.Groups.Item[1].Value.Replace(#13,'',[rfReplaceAll]).Replace(#10,'',[rfReplaceAll])
      else result := PEMContent;
  end;

var
  PubKey: string;
  KeyFormatType: TKeyFormatType;
begin
  PubKey := filetostring('public_key7.pem');
  KeyFormatToStr(PubKey, KeyFormatType);
  case KeyFormatType of
    kftRSAPublic,
    kftRSAPublicOpen: PubKey := RemovePEM(PubKey);
  end;

  result := BoolToString(VerifySAMLResponse('doesnt matter' {content to verify}, PubKey),'success','fail');
end;

Wherein:
public_key7.pem looks like...
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApqOtsIFCyWYN12cjRrhx
...
KwIDAQAB
-----END PUBLIC KEY-----

Also, "PKCS8-RSAPublic-Open" is a format that I created for BEGIN PUBLIC KEY.

Thanks again
Henrick Wibell Hellström

2024-05-02 18:10:20
Registered user
I recommend against using TStringStream in this context. There is a class named tOctetStringStream in unit stReadStream, which is a better choice.

It also seems to be a bad idea to use a function named FileToString, in order to read the "binary" contents of a file. You should use OctetString or tBytes, when dealing with binary content. Otherwise you might run into problems with the string encoders.

There might be other problems.
Peter Royston

2024-05-03 15:37:41
Registered user
Thanks for the quick response. I implemented your suggestions, and some other simplifications, as follows:

--------- After revisions ----------------------------------------------------------------------

function VerifySAMLContent(Content, PubKey, Signature: ansistring): boolean;
var
  stRSAPublicKey1: TstRSAPublicKey;
  lSS: tOctetStringStream;
begin
    lSS := tOctetStringStream.Create(MIME64ToStr(OSToBytes(PubKey)));
    stRSAPublicKey1 := TstRSAPublicKey.Create(nil);
    try
      stRSAPublicKey1.Name := 'stRSAPublicKey1';
      stRSAPublicKey1.EnabledDesignTime := False;
      stRSAPublicKey1.EnabledRunTime := False;

      stRSAPublicKey1.PublicKeyFormat.SelectedOptionNames := 'PKCS8-RSAPublic-Open';

      stRSAPublicKey1.SetPublicKey(stRSAPublicKey1.PublicKeyFormat.LoadFromStream(lSS,stRSAPublicKey1));

      stRSAPublicKey1.SignEncoding := seEMSA3;
      stRSAPublicKey1.EncryptEncoding := eeEME_PKCS1_v1_5;
      stRSAPublicKey1.DigestAlgorithm := haSHA256;
      stRSAPublicKey1.MGFHashAlgorithm := haSHA256;

      result := stRSAPublicKey1.VerifySignedDigest(Content,Signature,sfBinary,sfBinary,false);
    finally
      stRSAPublicKey1.Free;
      lSS.Free;
    end;
end;

function VerifySAMLResponse(XMLText, PubKey: ansistring): boolean;
var
  content_digest_mime, Sig, Digest: ansistring;
  bs: TBytesStream;
begin
  (*
    #Digest the content
    C:\TCPDev\OpenSSL>openssl dgst -sha256 C:\Temp\SAML\content.txt > c:\temp\saml\digest.txt

    #Use private key to sign the digest
    C:\TCPDev\OpenSSL>openssl dgst -sign C:\Users\troark\Documents\WhenToWork\SSOKeyFiles\private_key7.pvk -keyform pem -sha256 -out C:\Temp\SAML\digest.txt.sig -binary C:\Temp\SAML\digest.txt

    #Verify the signature using the public key
    C:\TCPDev\OpenSSL>openssl dgst -verify C:\Users\troark\Documents\WhenToWork\SSOKeyFiles\public_key7.pem -keyform pem -sha256 -signature C:\Temp\SAML\digest.txt.sig -binary C:\Temp\SAML\digest.txt
    Verified OK
  *)

  bs := TBytesStream.create;
  try
    bs.LoadFromFile('C:\Temp\SAML\digest.txt.sig');
    SetLength(Sig,bs.Size);
    Move(bs.Bytes[0], Sig[1], bs.Size); //Size is 256
  finally
    bs.Free;
  end;

  content_digest_mime := '02cdce786418c84269f4eb4443a802b08056e6534b6a1cc05ac40065abe5f80c';
  Digest := MIME64ToStr(OSToBytes(content_digest_mime)); //length = 48
  result := VerifySAMLContent(Digest, PubKey, Sig);
  //...false/fail
end;

------------------------------------------------------------------------------------------

When...
stRSAPublicKey1.SignEncoding := seEMSA3; // Raises error on assert in EMSA3Verification

  Assert(aCount = lHC.DigestSize); //Fails
        aCount = 48
        lHC.DigestSize = 32

stRSAPublicKey1.SignEncoding := seEMSA4; //does not error, but VerifySignedDigest returns false

In the first case, I can see where it's failing, but I don't know how to fix it. The second case "seEMSA4" is just me guessing, and hoping for the best :)

Thanks for any advice you can offer.
Todd
Henrick Wibell Hellström

2024-05-03 16:36:17
Registered user
It is important to manually check the input, because it usually gives important clues about what might be wrong. In this case, the value '02cdce786418c84269f4eb4443a802b08056e6534b6a1cc05ac40065abe5f80c' has the character set of an hexadecimal value; not base64. The string length 64 indicated a corresponding octet length of 32 (two hexadecimal characters per octet), which matches the length of a SHA-256 digest. Hence, use:

  content_digest_hex := '02cdce786418c84269f4eb4443a802b08056e6534b6a1cc05ac40065abe5f80c';
  Digest := HexToOS(content_digest_hex);
Peter Royston

2024-05-03 20:11:34
Registered user
Oh sorry/thanks, I should've noticed that. That change eliminated the assertion error, of course.

Now no error is raised... So I must be getting closer to success, but unfortunately the result is false.

If it matters, in my PUBLIC KEY format reader code, I am setting Result.Scheme := ifRSA1; Is that correct for this case?

I can share my PUBLIC KEY format reader code here if that would help.

I happened onto a post here...

"How does openssl signature verification work?" (https://crypto.stackexchange.com/questions/66615/how-does-openssl-signature-verification-work)

...which talks to some nuances in this regard, but it's beyond my understanding on this topic.

Again, thanks for your help!
Henrick Wibell Hellström

2024-05-04 06:08:12
Registered user
I took a closer look at what you wrote first. A file 'public_key7.pem' typically contains a PKCS#7 certificate chain. (PKCS#8 is used for private key storage.)

There isn't any public key format for extracting public keys from PKCS#7 files, already, because that would bypass all PKI certificate verification and chaining. Instead, if you want a solution that is close to the metal, use interface iContentInfo of the class tContentInfo in unit StreamSec.DSI.CMS for loading the PKCS#7 file. Then, you can use the PKI architecture in StreamSec.DSI.PkixKeyManager (and associated units), if you need a fine tuned PKI management of the certificates involved.

If you do not have any specific PKI needs, you could probably also use the TsmX509TrustedCertificates component. It has a method ImportFromCMSFile that will (probably) allow you to parse the PKCS#7 file, and chain the included certificates. You have to begin by adding the root ca certificate(s) that will anchor the chain. Once you got that certificate, load it into a iCertificate interface of the class tCertificate in unit StreamSec.DSI.PkixCert. For example:

uses
  stGC,
  StreamSec.Mobile.
  StreamSec.DSI.PkixCert,
  stPEMCoder,
  stDERCoder;

  procedure TForm1.AddRootCertificate(const aFileName: string);
  var
    lC: iCertificate;
    lStatus: tCertStatusCode;
  begin
    lC := tCertificate.Create(gcamFirstAssignment);
    lC.GetStruct.LoadFromFile(aFileName,fmtPEM); // or fmtDER
    smX509TrustedCertificates1.AddCertificate(lC,{ExplicitTrust=}True,lStatus,{AllowExpired=}False);
  end;

Note that ExplicitTrust=True is necessary for trusted root certificates only. In nearly all other cases you should set ExplicitTrust=False, because otherwise the certificate chaining will be bypassed. AllowExpired=False should always be used for real time verification of signatures from live sources. Only use AllowExpired=True when verifying historical signatures.

Next, use the ImportFromCMSFile method. After that, locate the end entity certificate, e.g. by iterating the TsmX509TrustedCertificates.Certs[] property. If you have imported a single PKCS#7 file, the end entity certificate is usually TsmX509TrustedCertificates.Certs[TsmX509TrustedCertificates.Count-1], but this is not guaranteed.

You can set a public key directly, without using any key format, by using the TstRSAPublicKey.SetPublicKey(lCert as iIFPublicKey) method.
Peter Royston

2024-05-04 11:51:36
Registered user
Sorry about my misleadingly named public_key7.pem file. Coincidentally, it got its name by being the 7th in a series of tests (_key1, 2, 3, ..., _key7) I was conducting for some unrelated/prior Azure IAM dev work.

In any case, I do want to make sure it has the expected contents, so I checked my public key format loader to insure that it is checking for the correct OID for a PKCS8 public key as follows:

  if not CompareBytes(OID,0,OIDToBytes('1.2.840.113549.1.1.1',opOIDTag),0,length(OID))
    then raise eKeyFormatError.Create('Invalid object identifier value');

This check passes for my so I think my file named public_key7.pem should be okay for this purpose. (From what I could find, OID for PKCS7 is 1.2.840.113549.1.7)

Thanks for the detailed advice regarding an X509 cert solution, but my StreamSec Tools 4.0 doesn't include support for those structures. I suppose I could upgrade to get that support, but if my current public key pem file is already in the expected 1.2.840.113549.1.1.1 format, it seems like I should be able to get VerifySignedDigest() to work with that using what I already have.

Is there anything else I might be missing?
Todd
Henrick Wibell Hellström

2024-05-04 16:56:53
Registered user
Thanks for the additional info. However, the OID 1.2.840.113549.1.1.1 stands for rsaEncryption, and it is used everywhere. You can forget PKCS#8, which is never used for public keys. OpenSSL outputs RSA Public Keys in a variety of formats. You can get it to use the PKCS#1 format, with the PEM header/footer identifier 'RSA PUBLIC KEY'. If the PEM header/footer identifier is just 'PUBLIC KEY', it is in all likelihood a X.509 SubjectPublicKeyInfo, which is already implemented in the unit StreamSec.DSI.X509Format.
Peter Royston

2024-05-04 18:39:34
Registered user
Oh, ok. wow, so much to learn.  I'll probably go the RSA PUBLIC KEY route, since I don't have StreamSec version/product that includes StreamSec.DSI.X509Format.

Thanks again for the great info and fast response!
Todd
Peter Royston

2024-05-06 11:57:22
Registered user
I converted my public_key7.pem file to public_keyRSA7.pem file using:

openssl rsa -in public_key7.pem -pubin -RSAPublicKey_out -out public_keyRSA7.pem

which produced the file:

-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEApqOtsIFCyWYN12cjRrhxVec27DprksxSmDMzWHOSyN5r96ebLGRF
...
3crpPbKxTcSB9rSOCzEVm4Jf6pVBGEzZKwIDAQAB
-----END RSA PUBLIC KEY-----

Then changed VerifyContent() to use my PKCS1 reader as follows:

function VerifyContent(Content, PubKey, Signature: ansistring): boolean;
var
  stRSAPublicKey1: TstRSAPublicKey;
  lSS: tOctetStringStream;
begin
    lSS := tOctetStringStream.Create(MIME64ToStr(OSToBytes(PubKey)));
    stRSAPublicKey1 := TstRSAPublicKey.Create(nil);
    try
      stRSAPublicKey1.Name := 'stRSAPublicKey1';
      stRSAPublicKey1.EnabledDesignTime := False;
      stRSAPublicKey1.EnabledRunTime := False;

      stRSAPublicKey1.PublicKeyFormat.SelectedOptionNames := 'PKCS1-RSAPublic-a'; //<<<<<<<<<<<<<<<< RSA PUBLIC KEY

      stRSAPublicKey1.SetPublicKey(stRSAPublicKey1.PublicKeyFormat.LoadFromStream(lSS,stRSAPublicKey1));

      stRSAPublicKey1.SignEncoding := seEMSA3;
      stRSAPublicKey1.EncryptEncoding := eeEME_PKCS1_v1_5;
      stRSAPublicKey1.DigestAlgorithm := haSHA256;
      stRSAPublicKey1.MGFHashAlgorithm := haSHA256;

      result := stRSAPublicKey1.VerifySignedDigest(Content,Signature,sfBinary,sfBinary,false);
    finally
      stRSAPublicKey1.Free;
      lSS.Free;
    end;
end;

...and calling it as follows:

function RemovePEM(PEMContent: String): string;
var
  M: TMatch;
begin
  M := TRegEx.Match(PEMContent,'-----BEGIN.*?-----(.*)-----END.*?-----',[roSingleLine]);
  if M.Success
    then result := M.Groups.Item[1].Value.Replace(#13,'',[rfReplaceAll]).Replace(#10,'',[rfReplaceAll])
    else result := PEMContent;
end;

....

  bs := TBytesStream.create;
  try
    bs.LoadFromFile('C:\Temp\SAML\digest.txt.sig');
    SetLength(Sig,bs.Size);
    Move(bs.Bytes[0], Sig[1], bs.Size); //Size is 256
  finally
    bs.Free;
  end;

  PubKey := RemovePEM(filetostring('C:\Users\troark\Documents\WhenToWork\SSOKeyFiles\public_keyRSA7.pem'));

  Digest := HexToOS('02cdce786418c84269f4eb4443a802b08056e6534b6a1cc05ac40065abe5f80c');
  result := VerifyContent(Digest, PubKey, Sig, true);

No errors are raised, but the result is still false.
Henrick Wibell Hellström

2024-05-06 12:54:30
Registered user
My next step would, in such case, be to debug the signature verification, and check at which step the verification fails.

If decrypting the signature results in something that is not correctly formatted, there is something wrong with the public key; either it is the wrong public key, or something went wrong in the processing of the contents of the file 'public_keyRSA7.pem'.

If decryption went OK, but the digest in the signature doesn't match the value of the argument Content, you should check if the digest and the signature don't correspond to the same to be signed data.

Another thing: Does the key format 'PKCS1-PEM-RSAPublic' not work with the key file you are using?
Top

:: Written with and Powered by the RealThinClient SDK and StreamSec Tools 4.0::
Copyright (c) Danijel Tkalcec, StreamSec HB