The following PHP function verifies that the $data string was signed using $key to create the $signature:
<?php
$result = openssl_verify( $data , $signature , $key , OPENSSL_ALGO_SHA1 );
?>
Is there an equivalent PHP function where I can get the original $data string if I have the correct $signature and $key
I think OP had a misconception about the signing process that I understand because I had it too.
The short answer is: no, there is no such PHP function, because it is impossible.
The digital signature is not just the original message ($data) encrypted with a Private Key.
It's the original message hashed into a "message digest" and then encrypted using a Private Key.
If you were to decrypt the signature with the matching public key, you would actually obtain the "message digest".
However there is no way to obtain the original message from the "message digest" because the hashing process is one way by design.
Verifying the signature is the process of hashing $data with the same hashing function and comparing it to the "message digest" obtained from decrypting the signature with the Public Key.
I guess that's what php function openssl_verify does.
This page helped me understand:
http://www.youdzone.com/signature.html
$data IS the original $data string, that's what the openssl_verify call does - verify that $data is unchanged, using $signature and $key
If you're asking if a $signature can be used to generate the data, then no - even if you have the private key, you still cannot recreate the data.
Imagine if you have a physical document (like a Will), which is signed, and a signature card. While the signature card ($key)allows you to verify the signature is authentic ($signature), the signature cannot be used to recreate the original content of the document (say, for example, if ink is spilled across the top of the document, but not across the signature line) - even if you're the original signer of the document. It can only be used to authenticate THAT document (Will) was, in fact, signed (legally binding).
Digital signatures are a bit more complicated from a math perspective, and provide some subtly different mechanisms for authentication, but in this case the result is the same - a signature cannot be used to recreate an original document, even if you have the signature card ($key), and even if you have the private key.
Related
I'm implementing a validator for validating incoming requests from Amazon Alexa. I'm on step 5 and 6 which state:
5) base64-decode the Signature header value on the request to obtain the encrypted signature.
6) Use the public key extracted from the signing certificate to decrypt the encrypted signature to produce the asserted hash value.
I've managed to extract the public key from the PEM-encoded X.509 certificate by doing:
$publicKey = openssl_pkey_get_public($pem);
$keyData = openssl_pkey_get_details($publicKey);
Which returns my public key. I've then attempted to decrypt the signature like so:
openssl_public_decrypt(base64_decode($this->signature), $decryptedSignature, $keyData['key']);
Which should return me a sha1 hash of the request body for me to compare with the actual request body, but what I get back from $decryptedSignature doesn't appear to be a sha1 hash. I'm hoping I'm missing something obvious but I can't see it.
To make things a little easier, here's a real life base64_encoded signature header returned from Alexa's test service:
DElCRMK3sXkhmnmu3D2HzVyuLHJ3JkABuBy2LCRX+winUhV6pSC9p1ASKFi9DzESsCyQ74izlFSvi3zECbSbT45bI38JpARJlal81YpWKxz2zTX+y6Qi+We/bFHHpU4gZO7nTTVQDWG4ua6EuWDTt3jL4B+hPOzO1OKix0jHKQldaTd9meyanttZ5QK7WotBeS6xU+Pum/dmiQ+LM39NERUCrCRyeU07PUdQt+L5PI8MehMz5ClHFOTWgyjE/J/b4zrX4weppb/KJhqQVmbw79BWMPuaSwf6BIHyf+4+/NSMmoaJ2WMKKEXf1aV7ac71QFFx9pw4P0BX7DK/hqy98Q==
And here is the public key extracted from https://s3.amazonaws.com/echo.api/echo-api-cert-4.pem:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnK+zBruRA1TnbgQGxE+b
4XiTTZyDkGwJ6068AGsXQmgt9lVhC8CTTC4wdR5NXosboV6/63worQCNo412csBV
jUy3H1/VEs+5Kv+AiAOUuKoBfEU8zAvHCc7GmOKUgNidcDA0MSpx3ZTMSGGbkfaL
ikRzne6nFZ6jNOnkqTtGD6SrCIYgLNArScYoPzIcXEypHFrognzrR4Ee0YcefGZy
S81Yqev/lli01dAgRvpnAty68rYTmxkNhzUSG6IIbFHIxXJKAETAkGiKJcgZpfG2
1Ok5Dk3yGrESY/ID5OnxvMxiXSnXwht8JD6bd15ui0tPDa85B0jpZLloqQZe26oR
owIDAQAB
-----END PUBLIC KEY-----
OK, I've realised my mistake. The decrypted signature is returned in binary, so I need to do: bin2hex($decryptedSignature) in order to get the sha1 hash. Bizarrely, the returned signature hash has 30 extra chars prepended, so the actual Alexa hash comparison needs to be:
public function compareHashes($pem) {
$publicKey = openssl_pkey_get_public($pem);
openssl_public_decrypt(base64_decode($this->signature), $decryptedSignature, $publicKey);
$decryptedSignature = hex2bin($decryptedSignature);
return sha1($this->responseBody) === substr($decryptedSignature, 30);
}
Anyway, I will open source my Alexa validation class and add a link back here once I've passed Alexa certification.
Edit
I'm through certification now, so here's the class I wrote for the Alexa validation: https://github.com/craigh411/alexa-request-validator
I am attempting to hash and sign user data on iOS (14.4), send that to my server, and have the server verify the hash and the signature with a previously uploaded public key (sent on keypair generation during user creation). It seems a number of people have run into issues with this, but all of the answers I've been able to find are very old, don't factor in using Apple's Secure Enclave, or revolve around signing and verifying on the same iOS device.
The general workflow is: User creates an account on iOS, and a random keypair is created on the device with the private key remaining in the Secure Enclave, while the public key is converted to ASN.1 format, PEM encoded and uploaded to the server. When the user later signs data, the data is JSONEncoded, hashed with sha512, and signed by their private key in the Secure Enclave. This is then packaged into a base64EncodedString payload, and sent to the server for verification. The server first verifies the hash using openssl_digest and then checks the signature using openssl_verify.
I have been unable to get the openssl_verify method to successfully verify the signature. I have also attempted using the phpseclib library (to get more insight into why the verification fails) without success. I understand phpseclib uses the openssl library if it is available, but even if this is disabled, phpseclib's internal verification fails because the resulting values after modulus do not match. Interestingly, phpseclib converts the public key to what looks like PKCS8 formatting with a large amount of padding.
It appears the public key is being parsed and loaded properly by openssl, as a proper reference is being created prior to verification. However, since the private key is opaque (residing in the Secure Enclave) I don't have a way to externally "check" how the signatures themselves are generated/encoded or if the same signature would be created outside of the iOS device. I'm wondering if I have an encoding error, or if external verification is possible with keys generated in the Secure Enclave.
iOS Public Key Upload method- I am using CryptoExportImportManager which converts the raw bytes to DER, adds the ASN.1 header, and adds the BEGIN and END key tags.
public func convertPublicKeyForExport() -> String?
{
let keyData = SecKeyCopyExternalRepresentation(publicKey!, nil)! as Data
let keyType = kSecAttrKeyTypeECSECPrimeRandom
let keySize = 256
let exportManager = CryptoExportImportManager()
let exportablePEMKey = exportManager.exportECPublicKeyToPEM(keyData, keyType: keyType as String,
keySize: keySize)
return exportablePEMKey
}
An example of what one of the public keys looks like after upload
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf16tnH8YPjslaacdtdde4wRQs0PP
zj/nWgBC/JY5aeajHhbKAf75t6Umz6vFGBsdgM/AFMkeB4n2Qi96ePNjFg==
-----END PUBLIC KEY-----
let encoder = JSONEncoder()
guard let payloadJson = try? encoder.encode(["user_id": "\(user!.userID)", "random_id": randomID])
else
{
onCompletion(nil, NSError())
print("Failed creating data")
return
}
let hash = SHA512.hash(data: payloadJson)
guard let signature = signData(payload: payloadJson, key: (user?.userKey.privateKey)!) else
{
print("Could not sign data payload")
onCompletion(nil, NSError())
return
}
let params = Payload(
payload_hash: hash.hexString,
payload_json: payloadJson,
signatures: ["user": [
"signature": signature.base64EncodedString(),
"type": "ecdsa-sha512"
]]
)
let encoding = try? encoder.encode(params).base64EncodedString()
The sign data function is pretty close to Apple's documentation code, but I'm including it for reference
private func signData(payload: Data, key: SecKey) -> Data?
{
var error: Unmanaged<CFError>?
guard let signature = SecKeyCreateSignature(key,
SecKeyAlgorithm.ecdsaSignatureMessageX962SHA512,
payload as CFData, &error)
else
{
print("Signing payload failed with \(error)")
return nil
}
print("Created signature as \(signature)")
return signature as Data
}
I actually stumbled upon the solution while doing additional research and experimentation while writing this question. The problem of course had nothing to do with the keys or algorithms, and everything to do with the way Apple hashes data objects.
I had discovered a similar problem when trying to determine why my hashes were not matching on the server-side vs the ones created on the iOS device. The user JSONEncoded data is hashed and signed as a base64Encoded data object, but unknown to me (and not in any documentation I could discover) iOS decodes the Data object and hashes the raw object, and re-encodes it (since this is opaque code it's possible this is not precisely accurate, but the result is the same). Therefore when checking the hash on the user data, I had to first base64decode the object, and then perform the hash. I had assumed that Apple would sign the encoded object as is (in order to not contaminate its integrity), but in fact, when Apple creates the digest before signing, it hashes the decoded raw object and creates a signature on the raw object.
Therefore the solution was to again base64decode the object before sending it to the openssl_verify function.
Checking the hash on the server
public function is_hash_valid($payload) {
$server_payload_hash = openssl_digest(base64_decode($payload["payload_json"]), "SHA512");
$client_payload_hash = $payload["payload_hash"];
if ($client_payload_hash != $server_payload_hash) {
return false;
}
return true;
}
Verifying the signature on the server
function is_signature_valid($data, $signature, $public_key) {
$public_key = openssl_get_publickey($public_key);
$ok = openssl_verify(base64_decode($data), base64_decode($signature), $public_key, "SHA512");
if ($ok === 1) {
return true;
} else {
return false;
}
}
After discovering this, and verifying that openssl_verify and phpseclib's verify function worked correctly, I almost considered deleting the question entirely but realized that if I had discovered a question similar to this in my research, it might have saved me a good deal of time. Hopefully to anyone else that has a similar issue, this will prove helpful.
I have a NodeJS application generating JSON Web Tokens with the PS256 algorithm. I want to try and verify the signatures in these tokens in a PHP application.
So far I've got the following:
My JWT:
eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIwMTBiYjllYS00YTg0LTQ1ZTMtOTg5My0wYzNhNDYxZmQzMGUiLCJpYXQiOjE2MDU4OTI5NjcsImV4cCI6MTYwNjQ5Nzc2NywiYXVkIjoiNzBiYzcxMTQ1MWM2NDBjOTVlZjgzYjdhNDliMWE0MWIiLCJpc3MiOiIyM2ZhYTRiNC0wNmVlLTRlNGEtYTVjZC05NjJmOTRhMjEzYmYiLCJqdGkiOiI1MTNiYjczZC0zOTY3LTQxYzUtODMwOS00Yjc1ZDI4ZGU3NTIifQ.kLtaSYKyhqzx7Dc7UIz7tqU8TsXabRLxGiaqw21lgCcuf_eBvpiLkFOuXpUs-V8XQunQg8jV-bKlKUIb0pLvipjhRP50IwKDClQgNtIwn4yyX5RyDNGJur0qHNnkHMLaF11NsXGPyhvh-6ogSZjWgyZnkQJkXpz4jggBetwqz1hnicapGfNb6C-UdRcOLyCaiMD4OmvniFVCY6YoKlC6eHdwxsgHAxOSgD1QKiiQX_yAe39ja_axZD2Ii3QaNgO0WXzfWMbqRg_yl0y3kjQFys9iXGvQ1JIKDMLffR3rKVL5PgKSU3e472xcPKf6PNSJzphPi1G_xH2gqg1VVXo3Lg
Decoded:
Header:
(
[alg] => PS256
[typ] => JWT
)
Body:
(
[sub] => 010bb9ea-4a84-45e3-9893-0c3a461fd30e
[iat] => 1605892967
[exp] => 1606497767
[aud] => 70bc711451c640c95ef83b7a49b1a41b
[iss] => 23faa4b4-06ee-4e4a-a5cd-962f94a213bf
[jti] => 513bb73d-3967-41c5-8309-4b75d28de752
)
sub is a GUID user ID (we utilize GUIDs so that if a user's ID is leaked no information can be extrapolated, like the number of users in our system or when a user signed up)
iat is the epoch time that the token was issued (UTC)
exp is the epoch time that the token will expire (UTC)
aud doesn't conform to the JWT spec. I abused this claim to mitigate the effects of stolen tokens. It's the MD5 hash of data sent with every client request that would be difficult for someone to guess. So if someone were to steal this token and use it without sending the appropriate passphrase, the token would be automatically revoked
iss also doesn't conform to the JWT spec. I abused this claim to list the ID of the key used for signing the JWT. This way I can rotate my public-private key pair and know which key to use when validating signatures
jti is a GUID uniquely identifying the JWT. Compared against an in-memory store of revoked tokens
I went with the PS256 algorithm over RS256 because I read on a blog post that it's more secure. Honestly I don't know the difference.
I went with the PS256 algorithm over ES256 because upon testing I found that while ES256 generated smaller signatures (and therefore smaller tokens), it took about 3x longer to compute. My goal is to make this app as scalable as possible, so long computation time is to be avoided.
My public key:
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEA0wO7By66n38BCOOPqxnj78gj8jMKACO5APe+M9wbwemOoipeC9DR
CGxC9q+n/Ki0lNKSujjCpZfnKe5xepL2klyKF7dGQwecgf3So7bve8vvb+vD8C6l
oqbCYEOHdLCDoC2UXEWIVRcV5H+Ahawym+OcE/0pzWlNV9asowIFWj/IXgDVKCFQ
nj164UFlxW1ITqLOQK1WlxqHAIoh20RzpeJTlX9PYx3DDja1Pw7TPomHChMeRNsw
Z7zJiavYrBCTvYE+tm7JrPfbIfc1a9fCY3LlwCTvaBkL2F5yeKdH7FMAlvsvBwCm
QhPE4jcDINUds8bHu2on5NU5VmwHjQ46xwIDAQAB
-----END RSA PUBLIC KEY-----
Using jsonwebtoken for NodeJS I can verify this token and authorize requests made using it. So all of the data seems good, the key works, and the math checks out.
However I've run into two problems when trying to verify the token in PHP:
1. The public key doesn't seem to be valid?
$key = openssl_pkey_get_public($pem);
print_r($key);
die();
This code prints out "false" - suggesting that the key could not be read from the PEM text posted above. Googling around I found this comment in the PHP manual which provided a solution. I did as instructed (removed new-lines from my key, prepended MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A, then wrapped to 64 characters) and for some reason openssl_pkey_get_public($pem) actually returned an OpenSSL Public Key now. I'm not really keen on using copy/paste solutions I don't understand, though, and the comment mentioned that this will only work for 2048-bit keys, which concerns me if we ever want to upgrade our security in the future.
After making the changes suggested to my key the new key looks like this:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0wO7By66n38BCOOPqxnj
78gj8jMKACO5APe+M9wbwemOoipeC9DRCGxC9q+n/Ki0lNKSujjCpZfnKe5xepL2
klyKF7dGQwecgf3So7bve8vvb+vD8C6loqbCYEOHdLCDoC2UXEWIVRcV5H+Ahawy
m+OcE/0pzWlNV9asowIFWj/IXgDVKCFQnj164UFlxW1ITqLOQK1WlxqHAIoh20Rz
peJTlX9PYx3DDja1Pw7TPomHChMeRNswZ7zJiavYrBCTvYE+tm7JrPfbIfc1a9fC
Y3LlwCTvaBkL2F5yeKdH7FMAlvsvBwCmQhPE4jcDINUds8bHu2on5NU5VmwHjQ46
xwIDAQAB
-----END PUBLIC KEY-----
(note that this is the same key, just with 32 magic bytes prepended to the beginning of it and "BEGIN RSA PUBLIC KEY" replaced with "BEGIN PUBLIC KEY")
2. The signature fails to verify (possibly because I'm using PS256 and not RS256)
Ignoring the issues with #1 for now and moving on to the next step, I tried to verify my signature like so:
$success = openssl_verify($jwtHeader . "." . $jwtBody, $jwtSignature, $key, OPENSSL_ALGO_SHA256);
This returned false. Meaning the signature was not valid. But I know the signature was valid because it worked fine in NodeJS. So I suspect the issue here revolves around my choice of algorithm.
How do I get PHP to properly verify this token?
Update 1
Here's the code that I'm using to verify my tokens in NodeJS. This is a HapiJS + TypeScript project, but you should be able to make sense of it. jwt is just defined as import * as jwt from "jsonwebtoken";:
jwt.verify(
token,
server.plugins["bf-jwtAuth"].publicKeys[tokenData.iss].key,
{
algorithms: [options.algorithm],
audience: userHash,
maxAge: options.tokenMaxAge
},
err =>
{
// We can disregard the "decoded" parameter
// because we already decoded it earlier
// We're just interested in the error
// (implying a bad signature)
if (err !== null)
{
request.log([], err);
return reject(Boom.unauthorized());
}
return resolve(h.authenticated({
credentials: {
user: {
id: tokenData.sub
}
}
}));
}
);
There's not too much to see here, because I just relied on a third-party tool to do all of the validation for me. jwt.verify(...) and it worked like magic.
Update 2
Assuming that my issue lie in the algorithm being used (PS256 vs RS256) I started searching around and found this StackOverflow post which pointed me at phpseclib
We actually coincidentally already had phpseclib installed via Composer as a dependency of Google's auth SDK, so I bumped it up to a top-level dependency and gave it a try. Unfortunately I still ran into an issue. Here's my code so far:
use phpseclib\Crypt\RSA;
// Setup:
$rsa = new RSA();
$rsa->setHash("sha256");
$rsa->setMGFHash("sha256");
$rsa->setSignatureMode(RSA::SIGNATURE_PSS);
// The variables I'm working with:
$jwt = explode(".", "..."); // [Header, Body, Signature]
$key = "..."; // This is my PEM-encoded string, from above
// Attempting to verify:
$rsa->loadKey($key);
$valid = $rsa->verify($jwt[0] . "." . $jwt[1], base64_decode($jwt[2]));
if ($valid) { die("Valid"); } else { die("Invalid"); }
Neither die() statement is reached as I hit an error on the $rsa->verify() line with the following:
ErrorException: Invalid signature
at
/app/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.php(2693)
Looking at this line in the library, it looks like it's failing at the "length checking" step:
if (strlen($s) != $this->k) {
user_error("Invalid signature");
}
I'm not sure what length it's expecting, though. I passed the raw signature directly from the JWT
After messing with this all day I finally figured out the missing piece.
First, some quick notes on the original question (already touched on in the updates):
To do RSA signatures with PSS padding ("PS256") you will need a third-party library, because the OpenSSL functions in PHP don't support this. A common recommendation is phpseclib
The 32 magic bytes I had to add to the key were only a quirk of PHP's OpenSSL functions and don't need to be utilized with phpseclib
With that said, my final problem (with the signature being "invalid") was:
JWT signatures are base64URL encoded, not base64 encoded
I wasn't even aware there was an RFC specification for base64URL encoding. It turns out you just replace every + with a - and every / with an _. So instead of:
$signature = base64_decode($jwt[2]);
It should be:
$signature = base64_decode(strtr($jwt[2], "-_", "+/"));
This works and my signature finally validates!
Is there a coldfusion alternaitive to this php function: openssl_verify:
openssl_verify() verifies that the signature is correct for the
specified data using the public key associated with pub_key_id. This
must be the public key corresponding to the private key used for
signing.
I've looked all over but there doesn't seem to be any. Thanks in advance for any info?
There are no built in functions, AFAIK. However, java supports signature verification, which you could adapt with a bit of java code.
Convert the data you want to verify into binary. The exact steps depends on what you are verifying, but say it is a physical file:
dataBytes = fileReadBinary( "c:\path\someFile.zip" );
Decode the signature value into binary. Again, the "how" depends on the signature format. If it is a base64 encoded string:
signatureBytes = binaryDecode( base64SignatureString, "base64" );
Load the certificate from your keystore (or from a file) and extract the public key:
// Example: "C:\ColdFusion\jre\lib\security\cacerts"
fis = createObject("java", "java.io.FileInputStream").init( pathToKeyStore );
keyStore = createObject("java", "java.security.KeyStore").getInstance("JKS");
// Default keystore password is "changeit" (do not keep the default. change it)
keyStore.load(fis, keyStorePassword.toCharArray());
publicKey = keyStore.getCertificate( "yourCertAlias" ).getPublicKey();
Create a Signature object to perform the verification. Initialize it with the appropriate algorithm (ie SHA1withRSA, etcetera), public key and the data to verify:
sign = createObject("java", "java.security.Signature").getInstance("SHA1withRSA");
sign.initVerify( publicKey );
sign.update( dataBytes );
Finally, feed in the signature and verify its status:
isVerified = sign.verify(signatureBytes);
writeDump( isVerified );
For more details, see Lesson: Generating and Verifying Signatures and Weaknesses and Alternatives.
You could attempt using cfexecute along with the OpenSSL CLI. https://www.openssl.org/docs/manmaster/apps/verify.html
New question to keep this question specific and to the point.
I have a JWT from Azure and now I need verify the signature in my application.
The public keys from Microsoft can be found here:
https://login.windows.net/common/discovery/keys
How do I use these keys to verify a signature? I can tell these these are the public keys I need as the X5T header in the JWT matches those on this public key list.
I am using the JWT PHP library but everything I enter as the public key seems to fail.
supplied key param cannot be coerced into a public key
So using the link above, that goes from there into the PHP
openssl_verify function as parameter three ($key in the example below)?
$success = openssl_verify($msg, $signature, $key, 'SHA256')
Everything I enter causes an error in one way or another.
Thanks,
Problem solved.
Turns out that the X5C part of the JSON array is the certificate not public key so JSON decoding https://login.windows.net/common/discovery/keys and grabbing the X5C element and using openssl to derive the public key works:
$cert_object = openssl_x509_read($cert);
$pkey_object = openssl_pkey_get_public(cert_object);
$pkey_array = openssl_pkey_get_details($pkey_object);
$publicKey = $pkey_array ['key'];
In this example $cert is the X5C value. However this on its own is not enough as its not encoded to X509. So what I did is create a new file in windows called certificate.cer, open in notepad and put the X5C value in there. Then by double clicking on ther .cer in windows, navigating to the details tab and clicking "copy to file" this opens the certificate export wizard.
Export as X509 and upload to the server.
$cert = file_get_contents('Certificates/Public/public.cer');
Works! There is probably a simpler way but this works.