|
![]() |
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? |