3

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.

miken32
  • 42,008
  • 16
  • 111
  • 154
UNOBerry
  • 123
  • 1
  • 9
  • 1
    Going from programming language A to programming language B is a 2 step process. Step 1 - translate programming language 1 to the language you speak and write (English, Spanish, etc). Step 2 - treat the result of Step 1 as a specification to be written in programming language B. – Dan Bracuk Dec 21 '18 at 04:54
  • 2
    Add the code that you are trying and include any error messages and include description of how your code is not working. Also let us know what version of ColdFusion you are using. – Miguel-F Dec 21 '18 at 14:19
  • Ben Nadel has written lots of stuff. When you say you have his doe, what exactly do you have. – James A Mohler Dec 21 '18 at 14:56
  • 1
    Check Leigh's answer for more info on signing. https://stackoverflow.com/questions/40733190/using-coldfusion-to-sign-data-for-single-sign-on – Shawn Dec 21 '18 at 15:01
  • 1
    Can you provide an example of the PHP code you're using? – Shawn Dec 21 '18 at 15:02
  • @Shawn I have tried the method in your link. Forgot about that one. Got the code to work but still got invalid signature. I have shared my code. – UNOBerry Dec 24 '18 at 18:52
  • @JamesAMohler Edited question. Has Ben's link. – UNOBerry Dec 24 '18 at 18:53
  • @Miguel-F I updated my question with the info you asked for. – UNOBerry Dec 24 '18 at 19:14
  • @DanBracuk CFML is my primary language. I made myself try it in PHP to check my logic. I was able to get it working in PHP in minutes. When I compare outputs claim and header they are 100% the same expect the signature. Since I can't really compare encryptions I started investigating CFML encryption for SHA-256 with RSA. This is where that brought me. I have seen a few attempts to answer this question but none that got me to google's specific requirements. I decided to ask what the equivalent of openssl_sign() would be since there is a larger audience in PHP. – UNOBerry Dec 24 '18 at 19:18

1 Answers1

2

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":"someemail@example.com","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":"someemail@example.com","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":"someemail@example.com","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 
SOS
  • 6,430
  • 2
  • 11
  • 29
  • I was just about to answer my own question. This is great and thank you for your info here. I got this to work with Ben Nadel's code, but I had to modify it to work. I commented out anything to do with a public key as I was not using one to interface with google. If I was to enhance it I could create logic to look for the use of public or private key. Next I skipped anything with Pem file formatting since google isn't using that. Now it works. – UNOBerry Dec 27 '18 at 16:41
  • @UNOBerry - Yep, I was just about to mention that I figured out one of the reasons it didn't compile was because the CFC expected both keys (public and private). But since you don't really need it for this anyway, using the raw signature code above - is simpler. Anyway, kudos on getting it working! – SOS Dec 27 '18 at 18:00
  • Side note, for the base64url_Encode() udf - you can also use a single ReplaceList(), instead of the 3 separate calls to Replace(). Though either is fine. – SOS Dec 27 '18 at 22:06