Search code examples
phpcoldfusionjwtcoldfusion-2016

ColdFusion equivalent of PHP openssl_sign()


What is the equivalent of PHP's openssl_sign() for ColdFusion? This works perfect in PHP but I need to do this in CFML:

<?php
//helper function
function base64url_encode($data) {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

// Read the JSON credential file my-private-key.json download from Google
$root = realpath($_SERVER["DOCUMENT_ROOT"]);
$private_key_file="$root/file.json";
$json_file = file_get_contents($private_key_file);

$info = json_decode($json_file);
$private_key = $info->{'private_key'};

//{Base64url encoded JSON header}
$jwtHeader = base64url_encode(json_encode(array(
    "alg" => "RS256",
    "typ" => "JWT"
)));

//{Base64url encoded JSON claim set}
$now = time();
$jwtClaim = base64url_encode(json_encode(array(
    "iss" => $info->{'client_email'},
    "scope" => "scope",
    "aud" => "https://www.googleapis.com/oauth2/v4/token",
    "exp" => $now + 3600,
    "iat" => $now
)));

$data = $jwtHeader.".".$jwtClaim;

// Signature
$Sig = '';
openssl_sign($data,$Sig,$private_key,'SHA256');
$jwtSign = base64url_encode( $Sig  );


//{Base64url encoded JSON header}.{Base64url encoded JSON claim set}.{Base64url encoded signature}

$jwtAssertion = $data.".".$jwtSign;

$ch = curl_init();

$url = "https://www.googleapis.com/oauth2/v4/token";
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");



$data = array(
    "grant_type" => "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "assertion" => $jwtAssertion
);



$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch) ;
echo $response;

For ColdFusion I was a able to get some sample code working. By working I mean flow with no errors. At the end I still get invalid jwt signature. ColdFusion code:

<cffunction name="base64url_encode" returntype="any" output="false">
     <cfargument name="stringValue" required="true">

    <cfset rawData = binaryEncode(binaryDecode(arguments.stringValue)>
    <cfset rawData = replace(rawData,"+","-","ALL")>
    <cfset rawData = replace(rawData,"/","_","ALL")>
    <cfset rawData = replace(rawData,"=","","ALL")>

    <cfreturn rawData>
</cffunction>

<cfobject name="main" component="a_assets.cfc.main">
<cfobject name="jwt" component="a_assets.cfc.JWT.sign.RSASigner">
<cfset privateKeyFile = ExpandPath('file.json')>
<cfset jsonFile = FileRead(privateKeyFile, 'utf-8')>
<cfset json = deserializeJSON(jsonFile)>
<cfset privateKey = json['private_key']>

<cfset signer = new a_assets.cfc.JWT.sign.RSASigner(privateKey, "SHA512withRSA")>
<cfset signer.addBouncyCastleProvider()>


<cfset JWT_header = structNew('ordered')>
<cfset JWT_header['alg'] = 'RS256'>
<cfset JWT_header['typ'] = 'JWT'>
<cfset JWT_header = serializeJSON(JWT_header)>

<cfset JWT_claim_set = structNew('ordered')>
<cfset JWT_claim_set['iss'] = json['client_email']>
<cfset JWT_claim_set['scope'] = 'scope'>
<cfset JWT_claim_set['aud'] = 'https://www.googleapis.com/oauth2/v4/token'>
<cfset JWT_claim_set['exp'] = main.fnEpochTime(DateAdd('h', 8, NOW()))>
<cfset JWT_claim_set['iat'] = main.fnEpochTime(DateAdd('h', 7, NOW()))>
<cfset JWT_claim_set = serializeJSON(JWT_claim_set)>

<cfset data = main.base64url_encode(JWT_header) & '.' & main.base64url_encode(JWT_claim_set)>

<cfset hashedData = signer.sign( data )>
<cfset signature = main.base64url_encode(hashedData)>
<cfset JWTData = data & '.' & signature>

<cfhttp url="https://www.googleapis.com/oauth2/v4/token" method="post" result="result">
    <cfhttpparam name="grant_type"          type="formField" value="urn:ietf:params:oauth:grant-type:jwt-bearer" />
    <cfhttpparam name="assertion"       type="formField" value="#JWTData#" />
</cfhttp>

<cfoutput>#result.filecontent#</cfoutput>

CFHTTP response error

{ "error": "invalid_grant", "error_description": "Invalid JWT Signature." }

I have compared all Base64 created in PHP to Coldfusion. Everything is identical until I get to encryption(). Which doesn't support SHA256withRSA as far I can tell. I have tried HMAC(). Which also doesn't support SHA256withRSA as far I can tell. Lastly I tried Ben's code which I have linked. I apologize for not uploading more detail the first time. I was fairly certain that PhP's openssl_sign() is what I need to replicate. I am using ColdFusion 2016.


Solution

  • The sample CF code posted didn't compile for me, so I used the signing example from the thread Shawn posted. With some small modifications, it worked perfectly. The only differences I could detect are spaces and that the php code escapes the slashes in the URL. Other than that, it produced the same results in both PHP and CF.

    For clarity the example uses hard coded JSON strings and sample keys since those are easy enough to compare.

    ColdFusion

    <cfscript>
        privateKey = "-----BEGIN PRIVATE KEY-----#chr(10)#"
                  & "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i"
                  & "kv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0"
                  & "zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw"
                  & "4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/Gr"
                  & "CtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6"
                  & "D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrP"
                  & "SXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAut"
                  & "LPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEA"
                  & "gidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJ"
                  & "ADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ"
                  & "==#chr(10)#-----END PRIVATE KEY-----#chr(10)#";
    
        // remove key header/trailer
        privateKey = privateKey.replaceAll("^-+BEGIN PRIVATE KEY-+", "");
        privateKey = privateKey.replaceAll("-+END PRIVATE KEY-+", "");
        privateKey = privateKey.replaceAll(chr(10), "").trim();
    
        // sample JSON
        jwtHeader   = '{"alg":"RS256","typ":"JWT"}';    
        jwtClaim    = '{"iss":"[email protected]","scope":"scope","aud":"https:\/\/www.googleapis.com\/oauth2\/v4\/token","exp":1545747624,"iat":1545744024}';
    
        data = base64url_encode(jwtHeader) &"."& base64url_encode(jwtClaim);
    
        // sign with private key and SHA256withRSA
        keyFactory = createObject("java", "java.security.KeyFactory").getInstance("RSA");
        privateSignature = createObject("java", "java.security.Signature").getInstance("SHA256withRSA");
        keyBytes = binaryDecode(privateKey, "base64");
        keySpec = createObject("java", "java.security.spec.PKCS8EncodedKeySpec").init(keyBytes);
        privateSignature.initSign(keyFactory.generatePrivate(keySpec));
        privateSignature.update(data.getBytes("utf-8"));
        signedBytes = privateSignature.sign();
        signature = base64url_encode(signedBytes);
        jwtAssertion = data &"."& signature;
    
        // Verify input strings match
        writeOutput("<hr>jwtHeader=<br>"& jwtHeader);
        writeOutput("<hr>jwtClaim =<br>"& jwtClaim);
        writeOutput("<hr>data=<br>"& data);
        writeOutput("<hr>jwtSign=<br>"& signature);
        writeOutput("<hr>jwtAssertion=<br>"& jwtAssertion);
    </cfscript>
    

    PHP

    <?php
    $privateKey = "-----BEGIN PRIVATE KEY-----\n"
                  ."MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i"
                  . "kv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0"
                  . "zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw"
                  . "4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/Gr"
                  . "CtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6"
                  . "D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrP"
                  . "SXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAut"
                  . "LPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEA"
                  . "gidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJ"
                  . "ADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ"
                  . "==\n-----END PRIVATE KEY-----\n";
    
    //helper function
    function base64url_encode($data) {
        return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
    }
    
    
    $private_key = $privateKey;
    $jwtHeader  = '{"alg":"RS256","typ":"JWT"}';    
    $jwtClaim   = '{"iss":"[email protected]","scope":"scope","aud":"https:\/\/www.googleapis.com\/oauth2\/v4\/token","exp":1545747624,"iat":1545744024}';
    
    
    $data = base64url_encode( $jwtHeader) . ".". base64url_encode( $jwtClaim);
    
    // Signature
    $Sig = '';
    openssl_sign($data,$Sig,$private_key,'SHA256');
    $jwtSign = base64url_encode( $Sig  );
    $jwtAssertion = $data.".".$jwtSign;
    
    echo "\njwtHeader=\n ". $jwtHeader;
    echo "\njwtClaim=\n ". $jwtClaim;
    echo "\ndata=\n". $data;
    echo "\njwtSign =\n ". $jwtSign;
    echo "\njwtAssertion =\n ". $jwtAssertion;
    

    Results:

    jwtHeader=
    {"alg":"RS256","typ":"JWT"}
    
    jwtClaim =
    {"iss":"[email protected]","scope":"scope","aud":"https:\/\/www.googleapis.com\/oauth2\/v4\/token","exp":1545747624,"iat":1545744024}
    
    data=
    eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzb21lZW1haWxAZXhhbXBsZS5jb20iLCJzY29wZSI6InNjb3BlIiwiYXVkIjoiaHR0cHM6XC9cL3d3dy5nb29nbGVhcGlzLmNvbVwvb2F1dGgyXC92NFwvdG9rZW4iLCJleHAiOjE1NDU3NDc2MjQsImlhdCI6MTU0NTc0NDAyNH0
    
    jwtSign=
    Ls59xceJGsv-z0A6cZKgJVIQIqFF3pWBSIR1HECLlfXcPWbFgKCfmpf0NPkJAnypOrAkdGWkwer5tp1xoogljhcd0CctoD4ckeM6FP7trJzEG1HGudwbghLlNHGmS4iYH-wFp5rLcO605ERbxpP4LZ0Y000sAVI-LWrzC0hdEMw
    
    jwtAssertion=
    eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzb21lZW1haWxAZXhhbXBsZS5jb20iLCJzY29wZSI6InNjb3BlIiwiYXVkIjoiaHR0cHM6XC9cL3d3dy5nb29nbGVhcGlzLmNvbVwvb2F1dGgyXC92NFwvdG9rZW4iLCJleHAiOjE1NDU3NDc2MjQsImlhdCI6MTU0NTc0NDAyNH0.Ls59xceJGsv-z0A6cZKgJVIQIqFF3pWBSIR1HECLlfXcPWbFgKCfmpf0NPkJAnypOrAkdGWkwer5tp1xoogljhcd0CctoD4ckeM6FP7trJzEG1HGudwbghLlNHGmS4iYH-wFp5rLcO605ERbxpP4LZ0Y000sAVI-LWrzC0hdEMw