Skip to content

OpenSSL output P7B structure in open_pkcs7_verify and add a openssl_pkcs7_read function #2563

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

jelly
Copy link
Contributor

@jelly jelly commented Jun 7, 2017

This PR adds support for retrieving the P7B structure which may exists in a signed email, this structure usually contains intermediate certificates which are needed for OCSP validation.
Therefore openssl_pkcs7_verify has an extra parameter added to save this structure to a file.

Since PHP has no support to read PKCS#7 structure, add a new function openss_pkcs7_read, which is based on openssl_pkcs12_read.
There has been a request for reading a PKCS#7 structure for a while.

Background information, with openssl's command line utlity this can be achieved as following:

openssl smime -verify -pk7out  -in signed_email.eml  > foo.pkcs7
openssl pkcs7 -print_certs -in foo.pkcs7

There is more information available in my mail to php-internals http://news.php.net/php.internals/99293

int i;

if (zend_parse_parameters(ZEND_NUM_ARGS(), "sz/", &p7b, &p7b_len,
&zout) == FAILURE)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use braces after if (it's PHP core CS)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjusted.


if (0 >= BIO_write(bio_in, p7b, (int)p7b_len)) {
php_openssl_store_errors();
goto clean_exit;
Copy link
Member

@bukka bukka Jun 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't that mean that you try to free p7 that wasn't allocated yet. I don't think we should be doing that even it's NULL and some platform accept that (not sure how portable it is)...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm C89 denotes:

Quoting the C standard, 7.20.3.2/2 from [ISO-IEC 9899](http://www.open-std.org/JTC1/SC22/wg14/www/docs/n1124.pdf)

void free(void *ptr);
If ptr is a null pointer, no action occurs.

But I'm fine with changing it, since the rest of the code follows the same style guide.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep but some old compilers were failing on that. I don't actually thing we support any such system but it's still better to follow this rule...


PHP_OPENSSL_CHECK_SIZE_T_TO_INT(p7b_len, p7b);

bio_in = BIO_new(BIO_s_mem());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should probably check if it's not NULL..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be actually caught below as BIO_write will check that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, then I won't add a check.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I say add the check anyway. We can't guarantee that OpenSSL won't suddenly start assuming bio_in is valid. Compared to the file access/crypto cost, one int branch won't be the perf killer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree with @sgolemon. It's better to add checks than relay on library.

p7 = PEM_read_bio_PKCS7(bio_in, NULL, NULL, NULL);
if (p7 == NULL) {
php_openssl_store_errors();
goto clean_exit;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, it will free NULL...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjusted, by checking the value in the exit label

@@ -0,0 +1,28 @@
--TEST--
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be good to rename to openssl_pkcs7_read_basic.phpt to keep it consistent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense will do.

@@ -36,6 +42,11 @@ if (file_exists($contentfile)) {
echo "true\n";
unlink($contentfile);
}

if (file_exists($pkcsfile)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pls move it to the --CLEAN-- section (see openssl_x509_export_to_file_basic.phpt for example...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, maybe the others should be moved there too. But I would rather see that in a separate PR.

@@ -14,6 +14,11 @@ if ($contentfile === false) {
die("failed to get a temporary filename!");
}

$pkcsfile = tempnam(sys_get_temp_dir(), "ssl");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's better to not use temp for test files to keep it consistent (see openssl_x509_export_to_file_basic.phpt for example of the usual naming )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

if (certs != NULL) {
for (i = 0; i < sk_X509_num(certs); i++) {
X509* aCA = sk_X509_pop(certs);
if (!aCA) break;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

braces should be here and a new line...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

X509_CRL *crl;
for (i = 0; i < sk_X509_CRL_num(crls); i++) {
X509_CRL* crl = sk_X509_CRL_pop(crls);
if (!crl) break;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

braces here as well ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

add_index_zval(zout, i, &zcert);
}

BIO_free(bio_out);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if BIO_new fails, then it would be NULL...

Copy link
Contributor Author

@jelly jelly Jun 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the free into the BIO_write if statement as it can't be NULL then.

@bukka
Copy link
Member

bukka commented Jun 11, 2017

@jelly It looks ok from the quick look now. Please squash it and will test it next week. If no one objects till then and all is fine, I will try to merge as well.

}

zval_dtor(zout);
array_init(zout);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems wierd to return by ref through an arg when you're essentially not using the return value at all.

Why not write your structure to return_value instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've based the function on openssl_pkcs12_read() which does the same, and I think it's nice to keep the functions in php-openssl consistent. One exception is openssl_x509_read which returns a resource or false which makes my argument a bit weaker.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually how it works for most openssl ext function (this one is actually based on openssl_pkcs12_read). I'm not saying that I like it but I think that the consistency is much more important in here so I would prefer to leave it as it is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m'kay. Local consistency is preferable, just struck me as odd at first glance. Freely ignore this review comment. :D

Copy link
Contributor Author

@jelly jelly Jun 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I will drop the commits. @sgolemon thanks for the review btw 👍

Copy link
Contributor Author

@jelly jelly Jun 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bukka reverted those changes, if everything looks fine I will squash the commits.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jelly Looks good. Please squash the commits and I will test and possibly merge.

PKCS7 * p7 = NULL;
int i;

if (zend_parse_parameters(ZEND_NUM_ARGS(), "sz/", &p7b, &p7b_len,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit; New code should prefer "S" (zend_string*) for string args. It makes it easier to share refs and pass data around to other APIs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please elaborate how it makes things easier in this particular case?


PHP_OPENSSL_CHECK_SIZE_T_TO_INT(p7b_len, p7b);

bio_in = BIO_new(BIO_s_mem());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I say add the check anyway. We can't guarantee that OpenSSL won't suddenly start assuming bio_in is valid. Compared to the file access/crypto cost, one int branch won't be the perf killer.


if (certs != NULL) {
for (i = 0; i < sk_X509_num(certs); i++) {
X509* aCA = sk_X509_pop(certs);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know the API here, but pop would imply that it's removing the cert from the certs vector. If that's the case, then on the next loop sk_X509_num will be one less while i continues to increase and you'll end up with only the first half of your entries.

Have you tried this with more than one cert?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sgolemon Good catch! pop decreases the num so the num should be first saved to the variable and the variable used in the loop. Alternatively sk_X509_value could be used IIRC.

}

bio_out = BIO_new(BIO_s_mem());
if (PEM_write_bio_X509(bio_out, aCA)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with above, please code defensively.
if (bio_out && PEM_writre_bio_X509(...)) {

var_dump(openssl_pkcs7_read("", $result));
var_dump(openssl_pkcs7_read($certfile, $result));
var_dump(openssl_pkcs7_read($infile, $result));
var_dump(empty($result));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe dump the actual certificate contents? If it contains unpredictable components, you can use an EXPECTF

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm it doesn't contain unpredictable components and should match openssl pkcs7 -in cert.p7b -print_certs (but without the subject= and issuer=)

@@ -26,6 +28,7 @@ var_dump(openssl_pkcs7_verify($eml, 0));
var_dump(openssl_pkcs7_verify($eml, 0, $empty));
var_dump(openssl_pkcs7_verify($eml, PKCS7_NOVERIFY, $outfile));
var_dump(openssl_pkcs7_verify($eml, PKCS7_NOVERIFY, $outfile, $cainfo, $outfile, $contentfile));
var_dump(openssl_pkcs7_verify($eml, PKCS7_NOVERIFY, $outfile, $cainfo, $outfile, $contentfile, $pkcsfile));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, let's verify something about the written contents. Whether that's a readfile or even just a filesize

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a check to verify the contents of $pkcsfile matches the expected pk7b output.

ZEND_ARG_INFO(0, infilename)
ZEND_ARG_INFO(1, certs)
ZEND_END_ARG_INFO()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to add the new param to arginfo_openssl_pkcs7_verify

&flags, &signersfilename, &signersfilename_len, &cainfo,
&extracerts, &extracerts_len, &datafilename, &datafilename_len) == FAILURE) {
&extracerts, &extracerts_len, &datafilename, &datafilename_len, &p7bfilename, &p7bfilename_len) == FAILURE) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit; As with below, prefer P for pathnames as zend_string rather than separate pieces. No need to change the existing args in the same commit, but let's do new stuff right.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see much point in here as we need just an actual string to pass to the library. I see the point if it's in internal API but if it's just passed to the library, then I don't think we should be changing that. It will just bring addition of extra macro wrapping around zend_string values when calling the library func.


if (p7bout) {
PEM_write_bio_PKCS7(p7bout, p7);
BIO_free(p7bout);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like p7out can leak if the PKCS7_verify call fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems right, thanks! I'll move it to the exit label.

goto clean_exit;
}

if (0 >= BIO_write(bio_in, ZSTR_VAL(p7b), (int)ZSTR_LEN(p7b))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly what I don't like. What's the point of using zend_string if the result is just extra macros. Please can you change it back to s unless there is some specific reason why we should use zend_string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll drop the two commits.

goto clean_exit;
}

p7bout = BIO_new_file(ZSTR_VAL(p7bfilename), "w");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd really prefer not to use ZSTR_VAL and rather use p

@krakjoe
Copy link
Member

krakjoe commented Jun 13, 2017

@bukka I think we will wait for your review of this ... please do merge if okay ...

@jelly jelly force-pushed the openssl_pkcs7_read branch 2 times, most recently from 92742b2 to 4041dee Compare June 15, 2017 08:14
@jelly
Copy link
Contributor Author

jelly commented Jun 15, 2017

@bukka squashed the commits

@bukka
Copy link
Member

bukka commented Jun 18, 2017

@jelly It actually segfaults for me with OpenSSL 1.0.2 when running ext/openssl/tests/openssl_pkcs7_read_basic.phpt. I did a quick debug and it fails when freeing p7. It might be because you are freeing ca using X509_free which is actually part of p7. Are you sure that it should be freed? I haven't had time to look into it properly but will take a look if you are not able to recreate it.

@bukka
Copy link
Member

bukka commented Jun 18, 2017

Well it even fails on travis so you should be able to recreate it:

https://travis-ci.org/php/php-src/jobs/243162346#L1331

@jelly
Copy link
Contributor Author

jelly commented Jun 19, 2017

Hmm it works for me with OpenSSL 1.1, but indeed on Debian Jessie fails with OpenSSL 1.0 so I will take a look at it.

@jelly jelly force-pushed the openssl_pkcs7_read branch 2 times, most recently from b23135d to 3aa1ee6 Compare June 19, 2017 11:26
Add an optional argument to openssl_pkcs7_verify to save the P7B
structure which can contain extra CA intermediate certificates send
along with an S/MIME signed email.

Introduce a new function called openssl_pkcs7_read, which can read a
PKCS#7 structure passed as a string and returns by reference an array
with PEM certificates formatted as a string.
@jelly jelly force-pushed the openssl_pkcs7_read branch from 3aa1ee6 to 3b7dc1f Compare June 22, 2017 09:20
@bukka
Copy link
Member

bukka commented Jun 22, 2017

Thanks for the fix. I have tested on all supported version and all is good. So it's merged now!

@php-pulls
Copy link

Comment on behalf of bukka at php.net:

Merged via 787a18a

@php-pulls php-pulls closed this Jun 22, 2017
@jelly jelly deleted the openssl_pkcs7_read branch July 4, 2017 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants