I tried to set up app attestation between my app and php but I rarely find any other source of explaination than Apple's own documentation, which let me stuck quite at an early state. So far I got the following steps:
On the client side, following https://developer.apple.com/documentation/devicecheck/establishing_your_app_s_integrity, I creted my attestation as a base64 encoded string:
attestation.base64EncodedString()
I then send that string to the server, following https://developer.apple.com/documentation/devicecheck/validating_apps_that_connect_to_your_server from now on.
The documentation says, that the attestation is in the CBOR format. I therefor first decode the base64 encoded string and parse it using (https://github.com/Spomky-Labs/cbor-php).
<?php
use CBOR\Decoder;
use CBOR\OtherObject;
use CBOR\Tag;
use CBOR\StringStream;
$otherObjectManager = new OtherObject\OtherObjectManager();
$tagManager = new Tag\TagObjectManager();
$decoder = new Decoder($tagManager, $otherObjectManager);
$data = base64_decode(/* .. base64 encoded attestation string as send from the client (see swift snippet above) */);
$stream = new StringStream($data);
$object = $decoder->decode($stream);
$norm = $object->getNormalizedData();
$fmt = $norm['fmt'];
$x5c = $norm['attStmt']['x5c'];
From the documentation, the normalized object should have the following format:
{
fmt: 'apple-appattest',
attStmt: {
x5c: [
<Buffer 30 82 02 cc ... >,
<Buffer 30 82 02 36 ... >
],
receipt: <Buffer 30 80 06 09 ... >
},
authData: <Buffer 21 c9 9e 00 ... >
}
which it does:
$fmt == "apple-appattest" // true
Then the next according to the documentation is described as:
Verify that the x5c array contains the intermediate and leaf certificates for App Attest, starting from the credential certificate in the first data buffer in the array (credcert). Verify the validity of the certificates using Apple’s App Attest root certificate.
However, I don't know how to proceed further on this. The content of e.g. $norm['attStmt']['x5c'][0] is a mix of readable chars and glyphs. To give you an idea, this is a random substring from the content of $norm['attStmt']['x5c'][0]: "Certification Authority10U Apple Inc.10 UUS0Y0*�H�=*�H�=B��c�}�". That's why I'm not really sure wheather I have to perform any further encodeing/decoding steps.
I tried parsing the certificate but without any luck (both var_dump return false):
$cert = openssl_x509_read($x5c[0]);
var_dump($cert); // false - indicating that reading the cert failed
$parsedCert = openssl_x509_parse($cert, false);
var_dump($parsedCert); // false - of course, since the prior step did not succeed
Any ideas, guidance or alternative ressources are highly appreciated. Thank you!
After a while I came up with the following solution. The $x5c field contains a list of certificates, all in binary form. I wrote the folowing converter to create a ready-to-use certificate in PEM format, which does the following:
base64 encode the binary data
break lines after 64 bytes
add BEGIN and END markers (also note the trailing line-break on the end certificate line)
function makeCert($bindata) {
$beginpem = "-----BEGIN CERTIFICATE-----\n";
$endpem = "-----END CERTIFICATE-----\n";
$pem = $beginpem;
$cbenc = base64_encode($bindata);
for($i = 0; $i < strlen($cbenc); $i++) {
$pem .= $cbenc[$i];
if (($i + 1) % 64 == 0)
$pem .= "\n";
}
$pem .= "\n".$endpem;
return $pem;
}
the following then works:
openssl_x509_read(makeCert($x5c[0]))
We run node.js CLI script from PHP with Symfony Process.
The script always print whole response as JSON in one line.
The response is somehow truncated on 512 characters.
I only found that xdebug.var_display_max_data => 512 => 512 in php.ini but don't see how this is related.
Adapter > Symfony Process > node script.js
A) Test Node script
from terminal node script $ node user-update.js parameters returns full result in all cases - like 629 chars.
from Symfony Process node script response is truncated to 512 chars.
B) Test Symfony Process
$process = new Process($cmd);
try {
$process->mustRun();
$response = $process->getOutput();
} catch (ProcessFailedException $e) {
$response = $e->getMessage();
}
echo $response;
echo PHP_EOL;
echo strlen($response);
$cmd = 'node user-update.js parameters'; - truncated to 512.
$cmd = 'php -r \'for($i=0; $i<520; $i++){ echo "."; }\''; - does not truncate.
$cmd = 'cat long_one_line.txt'; - print full file. 1650 chars in one line.
C) Try with PHP shell functions
$response = shell_exec($cmd); // response is truncated to 512
system($cmd, $returnVal); // print directly to STDOut, truncated to 512
What could be the cause and solution?
node v7.6.0
PHP 7.1.2
I suspect your process is ending before the buffer can be read by PHP.
As a work-around you can add something like this:
// The `| cat` at the end of this line means we wait for
// cat's process to end instead of node's process.
$process = new Process('node user-update.js parameters | cat');
I have this algorithm in PHP:
$encoded_key = 'WHllcnRGYTY3eWpUNjQ';
$decoded_key = base64_decode($encoded_key);
// XyertFa67yjT64
$params_string = implode('', $params);
//U215250.00121715620http://partner.domain.ru/order/U215/successhttp://partner.domain.ru/order/U215/fail
$raw_signature = hash_hmac('sha1', $params_string, $decoded_key, true);
// Byte-encoded, hex: c6881d8665afbb46a93a16b34bd152878a19ab3a
$encoded_signature = base64_encode($raw_signature);
// xogdhmWvu0apOhazS9FSh4oZqzo=
I'm trying to port this code to Ruby and get the same result but Base64 and OpenSSL can't help me. Does any one know whats wrong?
One problem is that you are using HMAC.hexdigest instead of HMAC.digest. Your PHP code is generating a raw HMAC and then encoding it in base 64. Therefore, you need to do the same thing in Ruby.
The other problem is the base 64 decoding step of the key. The key you entered is not padded correctly and will therefore be truncated by Ruby's base 64 library. For example:
encoded_key = "WHllcnRGYTY3eWpUNjQ"
Base64.decode64(encoded_key)
#=> "XyertFa67yjT"
# incomplete!
Base64.decode64("#{encoded_key}=\n")
#=> "XyertFa67yjT64"
# this is what you actually want
The padding and the final newline are there to ensure that the base 64 encoded data is complete, since it marks the end. However, it is possible to manually add the padding and just assume that the data is complete:
require 'base64'
require 'openssl'
def base64_pad(unpadded_str)
padding = case unpadded_str.size % 3
when 1 then "=="
when 2 then "="
end
"#{unpadded_str}#{padding}\n"
end
encoded_key = "WHllcnRGYTY3eWpUNjQ"
key = Base64.decode64(base64_pad(encoded_key))
#=> "XyertFa67yjT64"
string = "U215250.00121715620http://partner.domain.ru/order/U215/successhttp://partner.domain.ru/order/U215/fail"
Base64.encode64(OpenSSL::HMAC.digest('SHA1', key, string))
#=> "xogdhmWvu0apOhazS9FSh4oZqzo=\n"
I have a set of ZLIB compressed / base64 encoded strings (done in a C program) that are stored in a database. I have written a small PHP page that should retrieve these values and plot them (the string originally was a list of floats).
Chunk of C program that compresses/encodes:
error=compress2(comp_buffer, &comp_length,(const Bytef*)data.mz ,(uLongf)length,Z_DEFAULT_COMPRESSION); /* compression */
if (error != Z_OK) {fprintf(stderr,"zlib error..exiting"); exit(EXIT_FAILURE);}
mz_binary=g_base64_encode (comp_buffer,comp_length); /* encoding */
(Example) of original input format:
292.1149 8379.5928
366.1519 101313.3906
367.3778 20361.8105
369.1290 17033.3223
375.4355 1159.1841
467.3191 8445.3926
Each column was compressed/encoded as a single string. To reconstruct the original data i am using the following code:
//$row[4] is retrieved from the DB and contains the compressed/encoded string
$mz = base64_decode($row[4]);
$unc_mz = gzuncompress($mz);
echo $unc_mz;
Yet this gives me the following output:
f6jEÍ„]EšiSE#IEfŽ
Could anyone give me a tip/hint about what I might be missing?
------ Added Information -----
I feel that the problem comes from the fact that currently php views $unc_mz as a single string while in reality i would have to re-construct an array containing X lines (this output was from a 9 line file) but... no idea how to do that assignment.
The C program that did that went roughly like this:
uncompress( pUncompr , &uncomprLen , (const Bytef*)pDecoded , decodedSize );
pToBeCorrected = (char *)pUncompr;
for (n = 0; n < (2 * peaksCount); n++) {
pPeaks[n] = (RAMPREAL) ((float *) pToBeCorrected)[n];
}
where peaksCount would be the amount of 'lines' in the input file.
EDIT (15-2-2012): The problem with my code was that I was not reconstructing the array, the fixed code is as follows (might be handy if someone needs a similar snippet):
while ($row = mysql_fetch_array($result, MYSQL_NUM)) {
$m< = base64_decode($row[4]);
$mz_int = gzuncompress($int);
$max = strlen($unc_mz);
$counter = 0;
for ($i = 0; $i < $max; $i = $i + 4) {
$temp= substr($unc_mz,$i,4);
$temp = unpack("f",$temp);
$mz_array[$counter] = $temp[1];
$counter++;
}
The uncompressed string has to be chopped into chunks corresponding to the length of a float, unpack() then reconstructs the float data from teh binary 'chunk'. That's the simplest description that I can give for the above snippet.
compress2() produces the zlib format (RFC 1950). I would have to guess that something called gzuncompress() is expecting the gzip format (RFC 1952). So gzuncompress() would immediately fail upon not finding a gzip header.
You would need to use deflateInit2() in zlib to request that deflate() produce gzip-formatted output, or find or provide a different function in PHP that expects the zlib format.
For some reason, the zlib.deflate filter doesn't seem to be working with socket pairs generated by stream_socket_pair(). All that can be read from the second socket is the two-byte zlib header, and everything after that is NULL.
Example:
<?php
list($in, $out) = stream_socket_pair(STREAM_PF_UNIX,
STREAM_SOCK_STREAM,
STREAM_IPPROTO_IP);
$params = array('level' => 6, 'window' => 15, 'memory' => 9);
stream_filter_append($in, 'zlib.deflate', STREAM_FILTER_WRITE, $params);
stream_set_blocking($in, 0);
stream_set_blocking($out, 0);
fwrite($in, 'Some big long string.');
$compressed = fread($out, 1024);
var_dump($compressed);
fwrite($in, 'Some big long string, take two.');
$compressed = fread($out, 1024);
var_dump($compressed);
fwrite($in, 'Some big long string - third time is the charm?');
$compressed = fread($out, 1024);
var_dump($compressed);
Output:
string(2) "x�"
string(0) ""
string(0) ""
If I comment out the call to stream_filter_append(), the stream writing/reading functions correctly, with the data being dumped in its entirety all three times, and if I direct the zlib filtered stream into a file instead of through the socket pair, the compressed data is written correctly. So both parts function correctly separately, but not together. Is this a PHP bug that I should report, or an error on my part?
This question is branched from a solution to this related question.
I had worked on the PHP source code and found a fix.
To understand what happens I had traced the code during a
....
for ($i = 0 ; $i < 3 ; $i++) {
fwrite($s[0], ...);
fread($s[1], ...);
fflush($s[0], ...);
fread($s[1], ...);
}
loop and I found that the deflate function is never called with the Z_SYNC_FLUSH flag set because no new data are present into the backets_in brigade.
My fix is to manage the (PSFS_FLAG_FLUSH_INC flag is set AND no iterations are performed on deflate function case) extending the
if (flags & PSFS_FLAG_FLUSH_CLOSE) {
managing FLUSH_INC too:
if (flags & PSFS_FLAG_FLUSH_CLOSE || (flags & PSFS_FLAG_FLUSH_INC && to_be_flushed)) {
This downloadable patch is for debian squeeze version of PHP but the current git version of the file is closer to it so I suppose to port the fix is simply (few lines).
If some side effect arises please contact me.
Looking through the C source code, the problem is that the filter always lets zlib's deflate() function decide how much data to accumulate before producing compressed output. The deflate filter does not create a new data bucket to pass on unless deflate() outputs some data (see line 235) or the PSFS_FLAG_FLUSH_CLOSE flag bit is set (line 250). That's why you only see the header bytes until you close $in; the first call to deflate() outputs the two header bytes, so data->strm.avail_out is 2 and a new bucket is created for these two bytes to pass on.
Note that fflush() does not work because of a known issue with the zlib filter. See: Bug #48725 Support for flushing in zlib stream.
Unfortunately, there does not appear to be a nice work-around to this. I started writing a filter in PHP by extending php_user_filter, but quickly ran into the problem that php_user_filter does not expose the flag bits, only whether flags & PSFS_FLAG_FLUSH_CLOSE (the fourth parameter to the filter() method, a boolean argument commonly named $closing). You would need to modify the C sources yourself to fix Bug #48725. Alternatively, re-write it.
Personally I would consider re-writing it because there seems to be a few eyebrow-raising issues with the code:
status = deflate(&(data->strm), flags & PSFS_FLAG_FLUSH_CLOSE ? Z_FULL_FLUSH : (flags & PSFS_FLAG_FLUSH_INC ? Z_SYNC_FLUSH : Z_NO_FLUSH)); seems odd because when writing, I don't know why flags would be anything other than PSFS_FLAG_NORMAL. Is it possible to write & flush at the same time? In any case, handling the flags should be done outside of the while loop through the "in" bucket brigade, like how PSFS_FLAG_FLUSH_CLOSE is handled outside of this loop.
Line 221, the memcpy to data->strm.next_in seems to ignore the fact that data->strm.avail_in may be non-zero, so the compressed output might skip some data of a write. See, for example, the following text from the zlib manual:
If not all input can be processed (because there is not enough room in the output buffer), next_in and avail_in are updated and processing will resume at this point for the next call of deflate().
In other words, it is possible that avail_in is non-zero.
The if statement on line 235, if (data->strm.avail_out < data->outbuf_len) should probably be if (data->strm.avail_out) or perhaps if (data->strm.avail_out > 2).
I'm not sure why *bytes_consumed = consumed; isn't *bytes_consumed += consumed;. The example streams at http://www.php.net/manual/en/function.stream-filter-register.php all use += to update $consumed.
EDIT: *bytes_consumed = consumed; is correct. The standard filter implementations all use = rather than += to update the size_t value pointed to by the fifth parameter. Also, even though $consumed += ... on the PHP side effectively translates to += on the size_t (see lines 206 and 231 of ext/standard/user_filters.c), the native filter function is called with either a NULL pointer or a pointer to a size_t set to 0 for the fifth argument (see lines 361 and 452 of main/streams/filter.c).
You need to close the stream after the write to flush it before the data will come in from the read.
list($in, $out) = stream_socket_pair(STREAM_PF_UNIX,
STREAM_SOCK_STREAM,
STREAM_IPPROTO_IP);
$params = array('level' => 6, 'window' => 15, 'memory' => 9);
stream_filter_append($out, 'zlib.deflate', STREAM_FILTER_WRITE, $params);
stream_set_blocking($out, 0);
stream_set_blocking($in, 0);
fwrite($out, 'Some big long string.');
fclose($out);
$compressed = fread($in, 1024);
echo "Compressed:" . bin2hex($compressed) . "<br>\n";
list($in, $out) = stream_socket_pair(STREAM_PF_UNIX,
STREAM_SOCK_STREAM,
STREAM_IPPROTO_IP);
$params = array('level' => 6, 'window' => 15, 'memory' => 9);
stream_filter_append($out, 'zlib.deflate', STREAM_FILTER_WRITE, $params);
stream_set_blocking($out, 0);
stream_set_blocking($in, 0);
fwrite($out, 'Some big long string, take two.');
fclose($out);
$compressed = fread($in, 1024);
echo "Compressed:" . bin2hex($compressed) . "<br>\n";
list($in, $out) = stream_socket_pair(STREAM_PF_UNIX,
STREAM_SOCK_STREAM,
STREAM_IPPROTO_IP);
$params = array('level' => 6, 'window' => 15, 'memory' => 9);
stream_filter_append($out, 'zlib.deflate', STREAM_FILTER_WRITE, $params);
stream_set_blocking($out, 0);
stream_set_blocking($in, 0);
fwrite($out, 'Some big long string - third time is the charm?');
fclose($out);
$compressed = fread($in, 1024);
echo "Compressed:" . bin2hex($compressed) . "<br>\n";
That produces:
Compressed:789c0bcecf4d5548ca4c57c8c9cf4b57282e29cacc4bd70300532b079c
Compressed:789c0bcecf4d5548ca4c57c8c9cf4b57282e29cacc4bd7512849cc4e552829cfd70300b1b50b07
Compressed:789c0bcecf4d5548ca4c57c8c9cf4b57282e29ca0452ba0a25199945290a259940c9cc62202f55213923b128d71e008e4c108c
Also I switched the $in and $out because writing to $in confused me.