1

As a follow up to this question: Implementing WS-Security in Progress ABL, I'm continuing my struggle to implement WS-Security in Progress OpenEdge.

My problem:

On every request to a specific web service I generate a password digest based on:

  • A "nonce" - a random string
  • A timestamp - the current time
  • A password - a shared secret between me and the web service provider.

The nonce, timestamp and digest are then added to the Soap header of the web service call.

This works fine most of the time but fails in about 5 out of a 100 requests (see more info below).

This is how I generate the digest:

PROCEDURE generatePassHashNonceClear:

/*------------------------------------------------------------------------------
  Purpose: 
    Generates a password hash for WS-Security

  General algorithm:
    Digest = base64(sha1(Nonce +  Timestamp + sha1(Pwd))) 
------------------------------------------------------------------------------*/
    DEFINE INPUT  PARAMETER pcNonce    AS CHARACTER   NO-UNDO.
    DEFINE INPUT  PARAMETER pcCreated  AS CHARACTER   NO-UNDO.
    DEFINE INPUT  PARAMETER pcPassword AS CHARACTER   NO-UNDO.

    DEFINE OUTPUT PARAMETER pcHash     AS CHARACTER   NO-UNDO.

    DEFINE VARIABLE mBytes        AS MEMPTR      NO-UNDO.
    DEFINE VARIABLE mSHA1         AS MEMPTR      NO-UNDO.

   /* 
    Set size of mempointer, add 20 since we are adding the 20 byte 
    SHA1-DIGEST of the clear password in the end.
    */
    SET-SIZE(mBytes) = LENGTH(pcNonce) + LENGTH(pcCreated) + 20.

    /* Put the decoded nonce first */
    PUT-STRING(mBytes, 1) = pcNonce.

    /* Add create time */
    PUT-STRING(mBytes, 1 + LENGTH(pcNonce)) = pcCreated.

    /* Set SHA1 returns a 20 byte raw string. */
    SET-SIZE(mSHA1) = 20.
    mSHA1 = SHA1-DIGEST(pcPassword).

    /* Add password, SHA1-digested (so we need to put bytes instead of a string */
    PUT-BYTES(mBytes, 1 + LENGTH(pcNonce) + LENGTH(pcCreated)) = mSHA1.

    /* Create out-data in B64-encoded format */
    pcHash = STRING(BASE64-ENCODE(SHA1-DIGEST(mBytes))).

    /* Clean up mempointers */
    SET-SIZE(mBytes) = 0.
    SET-SIZE(mSHA1)  = 0.

END PROCEDURE.

And this is how the procedure is called:

DEFINE VARIABLE cPasswordClear   AS CHARACTER   NO-UNDO.
DEFINE VARIABLE dtZuluNow        AS DATETIME    NO-UNDO.
DEFINE VARIABLE cCreated         AS CHARACTER   NO-UNDO.
DEFINE VARIABLE cNonceB64        AS CHARACTER   NO-UNDO.
DEFINE VARIABLE cNonce           AS CHARACTER   NO-UNDO.
DEFINE VARIABLE cPasswordDigest  AS CHARACTER   NO-UNDO.

/* 
Get time in UTC/GMT/ZULU/Timezone 0 and store 
it with 000 as milliseconds + Z for timezone Zulu 

Nonce is a random generated string 
*/
ASSIGN 
    dtZuluNow      = DATETIME-TZ(NOW,0)
    cCreated       = STRING(dtZuluNow, "9999-99-99THH:MM:SS") + ":000Z"
    cPasswordClear = "SECRET"
    cNonceB64      = BASE64-ENCODE(GENERATE-RANDOM-KEY)
    cNonce         = STRING(BASE64-DECODE(cNonceB64)).


RUN generatePassHashNonceClear( cNonce, cCreated, cPasswordClear, OUTPUT cPasswordDigest).

What I know:

This works fine in something like 9 500 out of 10 000 requests. But there's a 5% fail rate. Unfortunately the error message isn't helpful so all I really can see is that the login failed. The web service provider states that the logins are rejected because of incorrect digests.

What I did:

To test my digest procedure I created a small python program. This indeed creates different digests when I try it with the in data (nonce and timestamp) from the failed logins. I am however not a Python programmer so there might very well be something wrong in this program (but it would be a very strange coincidence that it also should work in the same 95% of all cases).

Here's the python program:

import hashlib

def createDigest(Nonce, Created, Password):
    "This function returns a digest"

    NonceB64 = Nonce.decode("base64","strict")

    pdgst = hashlib.sha1()
    pdgst.update(Password)
    PasswordDgst = pdgst.digest()


    FinalDgst = hashlib.sha1()
    FinalDgst.update(NonceB64)
    FinalDgst.update(Created)
    FinalDgst.update(PasswordDgst)

    FinalTxt = FinalDgst.digest().encode("base64","strict")
    print "Final digest : " + FinalTxt

    return

print "This digest is repeated in Progress OpenEdge"
createDigest("tGxF8+DAmJvQo93PNZt5Nw==", "2015-04-08T20:10:44:000Z", "SECRET")

print "This digest isn't repeated in Progress OpenEdge"
createDigest("XdcAW1TdTr+MLp4t0QkJ8g==", "2015-04-08T20:10:44:000Z", "SECRET")

My real password is of course not "SECRET" and this makes me believe that the error has to do with the nonce. Changing the password to "SECRET" made the digest different but the discrepancy between the Progress and Python digests still was there afterwards (the first example above generated similar digests before and after the change but the second did not).

I have an open case with Progress Support but they seem to struggle with this as much as I do.

I've tested this in OpenEdge 11.3.1 and 11.4 on RHEL and Windows 7 and the behavior stays the same.

Community
  • 1
  • 1
Jensd
  • 7,886
  • 2
  • 28
  • 37
  • Are you saying that they python code and the progress code both fail at a 5% rate when accessing this web service? To me having two completely different implementations fail seems like the problem is much more likely to be with the web service. Does the web service provider have a reference implementation or code sample that they say works properly? – Tom Bascom Apr 28 '15 at 13:50
  • No, maybe I'm not describing things very good. They differ in 5% of the cases - those where the Progress login actually failed. This leads me to think that there is a problem in the 4GL and not in the service. All sample code are unfortunently for different java and .net frameworks like Axis2 or JAX-WS. The frameworks seem to take care of the acual digest generation. A thought is that it has to to with codepage conversion? We run everything in iso8859-1 and I guess UTF is supposed to be used, maybe its only rarely (~5%) that a character affected by that is created by chance. (A wild guess). – Jensd Apr 28 '15 at 13:56
  • Ok, that makes better sense. Can you run an experiment with different -cpinternal and -cpstream values? – Tom Bascom Apr 28 '15 at 14:30
  • Yes, but not until tomorrow though – Jensd Apr 28 '15 at 14:33
  • 2
    The docs say that if the argument to sha1-digest() is a char or longchar that it will be converted to UTF-8 in order to avoid code page problems but that conversion will NOT be done for raw or memptr values. You have the nonce and then the nonce + password in memptrs when you pass them to sha1-digest(). – Tom Bascom Apr 28 '15 at 14:58
  • @TomBascom Thanks. That might be it! – Jensd Apr 28 '15 at 15:02
  • This is where it breaks (or at least one of the places): `cNonce = STRING(BASE64-DECODE(cNonceB64)).` The decoded nonce might very well contains characters not supported in our codepage (iso8859-1), in those cases the string seems to be terminated to soon. cpinternal and cpstream doesn't seem to help. – Jensd Apr 29 '15 at 06:17

1 Answers1

1

Answering my own question for future references:

The problem was related to codepage conversion just as @TomBascom pointed out but the actual error was really earlier in the "chain" than the SHA-digestion.

cNonceB64      = BASE64-ENCODE(GENERATE-RANDOM-KEY)
cNonce         = STRING(BASE64-DECODE(cNonceB64))

In the second line the value of cNonce gets destroyed whenever the key generated contains values mismatching between iso8859-1 and UTF-8.

The simple solution was to change the cNonce variable into a mempointer and then rewriting the procedure that generates the digest.

/* Optimistic, should really be based on current symmetric encryption algorithm */
SET-SIZE(mNonce) = 16.

ASSIGN
  mNonce    = GENERATE-RANDOM-KEY
  cNonceB64 = BASE64-ENCODE(mNonce).

And then the new procedure for generating a password digest:

PROCEDURE generateDigest:

/*------------------------------------------------------------------------------
  Purpose:     Generates a password hash for WS-Security
  Parameters:  <none>
  Notes:       
------------------------------------------------------------------------------*/

    DEFINE INPUT  PARAMETER mNonce     AS MEMPTR      NO-UNDO.
    DEFINE INPUT  PARAMETER pcCreated  AS CHARACTER   NO-UNDO.
    DEFINE INPUT  PARAMETER pcPassword AS CHARACTER   NO-UNDO.

    DEFINE OUTPUT PARAMETER pcHash     AS CHARACTER   NO-UNDO.

    DEFINE VARIABLE mBytes        AS MEMPTR      NO-UNDO.
    DEFINE VARIABLE mSHA1         AS MEMPTR      NO-UNDO.

    /* 
    Set size of mempointer, add 20 since we are adding the 20 byte 
    SHA1-DIGEST of the clear password in the end.
    */
    SET-SIZE(mBytes) = LENGTH(pcCreated) + 36. /* 16 + 20 = 36 */

    /* Put the decoded nonce first */
    PUT-BYTES(mBytes, 1) = mNonce.

    /* Add create time */
    PUT-STRING(mBytes, 17) = pcCreated. /* 16 + 1 = 17 */

    /* Set SHA1 returns a 20 byte raw string. */
    SET-SIZE(mSHA1) = 20.
    mSHA1 = SHA1-DIGEST(pcPassword).

    /* Add password, SHA1-digested (so we need to put bytes instead of a string */
    PUT-BYTES(mBytes, 17 + LENGTH(pcCreated)) = mSHA1. /* 16 + 1 = 17 */

    /* Create out-data in B64-encoded format */
    pcHash = STRING(BASE64-ENCODE(SHA1-DIGEST(mBytes))).

    /* Clean up mempointers */
    SET-SIZE(mBytes) = 0.
    SET-SIZE(mSHA1)  = 0.
    SET-SIZE(mNonce) = 0.
END PROCEDURE.
Jensd
  • 7,886
  • 2
  • 28
  • 37