|
![]() |
Sign JWT for Azure AD access token |
Peter Royston 2023-07-13 02:57:37 Registered user |
I'm trying to use StreamSec Tools 4.0 to sign a JWT on the client side, to be validated by the the Azure auth platform.
Azure provides a place to upload a cert under the auth app, which will be used to authenticate client requests for an access token; i.e. implementing the "client assertion" model. My general plan goes like this... User presses a button in our web service, WhenToWork, to generate a private key, which stores it with their account, and then downloads it to a file. User then uses that key to generate a csr. User acquires a cert from a CA using the csr. User uploads the cert into the Azure Ad app and saves the thumbprint to their WhenToWork account. ... Later when WhenToWork needs to grant access to a user, it requests the access token using a client assertion REST request, signed-JTW/thumbprint, etc. I have code in Delphi which doesn't succeed just yet, but I have an equivalent Powershell script which is working, but there is a difference which I believe may be related to my problem on the Delphi side. As a first/common step, I generated PRIVATEKEYBLOB-RSA content and stored it in a file (private_key.pvk) as follows/example: In Delphi... var RSAKey: TstRSAKey; lSS: TStringStream; begin RSAKey := TstRSAKey.Create(nil); try RSAKey.KeyGenerationKind := RSAKey.KeyGenerationKind - [ifkgProvable]; RSAKey.KeyGenerationKind := RSAKey.KeyGenerationKind - [ifkgNIST]; RSAKey.KeyGenerationKind := RSAKey.KeyGenerationKind + [ifkgExportable]; RSAKey.KeyGenerationKind := RSAKey.KeyGenerationKind - [ifkgSignatureKey]; RSAKey.DigestAlgorithm := haSHA256; RSAKey.MGFHashAlgorithm := haSHA256; case KeySize of 0: RSAKey.GenerateNewKeys(512,False); 1: RSAKey.GenerateNewKeys(768,False); 2: RSAKey.GenerateNewKeys(1024,False); 3: RSAKey.GenerateNewKeys(2048,False); 4: RSAKey.GenerateNewKeys(3072,False); 5: RSAKey.GenerateNewKeys(4096,False); 6: RSAKey.GenerateNewKeys(8192,False); end; lSS := TStringStream.Create(''); try RSAKey.PrivateKeyFormat.SelectedOptionNames := 'PRIVATEKEYBLOB-RSA-SIGN,PRIVATEKEYBLOB-RSA,PRIVATEKEYBLOB-RSA-KEYX,RSA-XmlString'; RSAKey.SavePrivateKeyToStream(lSS,iUnknown); result := Wrap(StrToMIME64Str(lSS.DataString),65); finally lSS.Free; end; finally RSAKey.Free; end; Which produces... -----BEGIN RSA PRIVATE KEY----- BwIAAAAEAABSU0EyAAgAAAEAAQC9M+f4M88CpZKpl7/DMfOz4rGXGjZO1gvPyLa4 PwAY2imDJP01nykzb6bh8fKg8N2Aj6II7CTsngGnLUt87IV0/6JX98wgYJnf2/ho ... m6ll+9mQlmpVNMQiuiOBOoNw9RFbJLqIeT6pWv0948OIHGfE6P9oXNNVaBJkLrLC 9ynY6PcFiD5wCMt+1qMHGYUKxlg= -----END RSA PRIVATE KEY----- Then produce the csr, with something like this. openssl.exe req -new -key C:\Temp\out\private_key.pvk -out myreq.csr Then produce a cert as follows: openssl x509 -signkey private_key.pvk -in myreq.csr -req -days 365 -out domain.crt To utilize this private key on the Powershell script side, I needed a .pfx to import into the windows user stores. openssl.exe pkcs12 -export -out domain.pfx -inkey private_key.pvk -in domain.crt Then import the .pfx using MMC The relevant signing related code in Powershell looks like this: #Get cert using thumbprint $Certificate = Get-Item Cert:\CurrentUser\My\d650bf...f6df450a $PrivateKey = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)) # Define RSA signature and hashing algorithm $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1 $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256 # Create a signature of the JWT $Signature = [Convert]::ToBase64String($PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)) -replace '\+','-' -replace '/','_' -replace '=' # Join the signature to the JWT with "." $JWT = $JWT + "." + $Signature ... Send REST request, which returns the access token. ...Delphi-side code for preparing and signing the JWT function SignJWT(KeyText, Password, tokenURI, ClientId, ThumbPrint: string): string; var RSAKey: TstRSAKey; lSS: TStringStream; lPwd: iSecretKey; lPriv: iIFPrivateKey; jsoHeader, jsoPayload: ISuperobject; jwt, Signature: string; begin RSAKey := TstRSAKey.Create(nil); try with RSAKey do begin Name := 'RSAKey'; EnabledDesignTime := False; EnabledRunTime := False; SignEncoding := seEMSA3; EncryptEncoding := eeEME_PKCS1_v1_5; DigestAlgorithm := haSHA256; MGFHashAlgorithm := haSHA256; PublicKeyFormat.SetSelectedOptionNames(['RSA-XmlString-Public']); lSS := TStringStream.Create(PEMToStr(KeyText,PEMType)); try lPwd := tSecretKey.CreateStr(PAnsiChar(AnsiString(Password))); PrivateKeyFormat.SelectedOptionNames := 'PRIVATEKEYBLOB-RSA'; PK := PrivateKeyFormat.LoadFromStream(lPwd,lSS,lPriv); SetPrivateKey(PK); finally lSS.Free; end; end; jsoHeader := SO; jsoHeader.s['alg'] := 'RS256'; jsoHeader.s['typ'] := 'JWT'; jsoHeader.s['x5t'] := Thumbprint; jsoPayload := SO; jsoPayload.s['aud'] := tokenURI; jsoPayload.s['iss'] := ClientId; jsoPayload.s['sub'] := ClientId; jsoPayload.s['jti'] := NewGUID; jsoPayload.i['exp'] := StrToInt(GetDateTimeUnix(IncHour(Now,1), False)); jsoPayload.i['nbf'] := StrToInt(GetDateTimeUnix(Now, False)); jwt := JwtEncode(StrToMIME64Str(jsoHeader.AsJson))+'.'+JwtEncode(StrToMIME64Str(jsoPayload.AsJSon)); Signature := RSAKey.GenerateSignature(jwt,sfBase64); result := jwt+'.'+JwtEncode(Signature.TrimEnd(['='])); finally RSAKey.Free; end; end; This delphi code fails with message: "...Key was found, but use of the key to verify the signature failed..." Is it possible that the Powershell script works correctly because the private key has been included in .pfx file and then extracted by the script? Any help you can offer would be greatly appreciated! Todd |
Peter Royston 2023-07-14 00:08:26 Registered user |
Finally got this working, and it turned out to be something simple.
Instead of this: Signature := RSAKey.GenerateSignature(jwt,sfBase64); result := jwt+'.'+JwtEncode(Signature.TrimEnd(['='])); Fixed code: Signature := RSAKey.GenerateSignature(jwt,sfBinary); Signature64 := StrToMIME64Str(Signature); result := jwt+'.'+JwtEncode(Signature64.TrimEnd(['='])); |
Henrick Wibell Hellström 2023-07-15 03:07:08 Registered user |
Yes, you have to beware, when using the sfBase64 encoding option. It puts a zero byte in front of the binary representation of the signature. This is because some implementations encode some signatures as one's-complement-integers, which means that zero is required, or else the value is interpreted as a negative integer. Other implementations interpret signatures, not as integers, but as binary bit strings, which means an appended leading zero will instead bit-shift the value by eight. Can't please both, so if sfBase64 doesn't work, use sfBinary and encode yourself, like you did.
|
Peter Royston 2023-07-15 23:43:17 Registered user |
Thanks for the clarification. I'll definitely be on the lookout for this in future.
Todd |