Skip to content
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

[RFC]: Add Argon2 to password_* #1997

Merged
merged 18 commits into from
Sep 8, 2016
Merged

[RFC]: Add Argon2 to password_* #1997

merged 18 commits into from
Sep 8, 2016

Conversation

charlesportwoodii
Copy link
Contributor

@nikic
Copy link
Member

nikic commented Jul 10, 2016

Is libargon2 provided by distros? I don't fancy the idea of bundling another library, unless we actually need to patch something in it.

@charlesportwoodii
Copy link
Contributor Author

charlesportwoodii commented Jul 10, 2016

@nikic,

No, not yet - this is something specifically mentioned in the RFC draft I'm working on.

libargon2 is in Debian stretch, but not stable yet. https://packages.debian.org/search?keywords=libargon2-0. I haven't seen it drop in RH/CentOS yet, hence whyy the necessary C files were added directly. I'm not too fond of including it directly either.

As an alternative to directly including the source (at least until a library drops in distros) to to compile the libargon2 reference library as part of PHP's make process. This is the process used by the extension I wrote and extensions for other languages such as Ruby and Go.

The libargon2 reference library compiles with little dependencies, going down that route however would significantly complicate the PHP build process, which I was hoping to avoid.

Let me know your thoughts. There's ways to make it work without including the C source, but I completely understand tabling a feature to wait for the library to become more wildly available.

@jpauli
Copy link
Member

jpauli commented Jul 11, 2016

I'm 👎 to bundle yet another lib, and to have to maintain it.
This is a pain with libgd, libpcre and other libs we bundle, so please, rely on the OS distro.

Also, ext/standard should not depend on libpthread, as this is not (officialy) supported under Windows.
Please, consider Windows specific issues and provide some solutions if possible.

@jpauli jpauli added the RFC label Jul 11, 2016
'/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1');
PHP_INSTALL_HEADERS("", "ext/standard");
if (PHP_MBREGEX != "no") {
CHECK_HEADER_ADD_INCLUDE("oniguruma.h", "CFLAGS_STANDARD", PHP_MBREGEX + ";ext\\mbstring\\oniguruma")
}
PHP_INSTALL_HEADERS("", "ext/standard");
PHP_INSTALL_HEADERS([ext/standard/argon2lib])
Copy link
Member

Choose a reason for hiding this comment

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

This should be PHP_INSTALL_HEADERS("", "ext/standard/argon2lib")

@KalleZ
Copy link
Member

KalleZ commented Jul 11, 2016

I agree with @jpauli here, bundling yet another library and the added Windows issues it causes is a huge -1, especially in ext/standard

@nikic
Copy link
Member

nikic commented Jul 11, 2016

On Jul 11, 2016 00:19, "Charles R. Portwood II" [email protected]
wrote:

@nikic,

No, not yet - this is something specifically mentioned in the RFC draft
I'm working on.

libargon2 is in Debian stretch, but not stable yet.
https://packages.debian.org/search?keywords=libargon2-0. I haven't seen it
drop in RH/CentOS yet, hence by the necessary C files were added directly.
I'm not too fond of including it directly either.

As an alternative to directly including the source (at least until a
library drops in distros) to to compile the libargon2 reference library as
part of PHP's make process. This is the process used by the extension I
wrote and extensions for other languages such as Ruby and Go.

The libargon2 reference library compiles with little dependencies, going
down that route however would significantly complicate the PHP build
process, which I was hoping to avoid.

Let me know your thoughts. There's ways to make it work without including
the C source, but I completely understand tabling a feature to wait for the
library to become more wildly available.

I would suggest to add libargon2 as an optional dependency for now, which
we link against if something like --with-argon2 is configured. This will
make the feature available to the people who need it in a convenient way.
If libargon2 becomes widely available, we can make it a default dependency,
though that is probably some years off.

From my limited understanding, there is no pressing need to replace bcrypt
with argon2 right now, argon2 is just slightly preferable. As such I thing
it's fine to only provide it optionally to start with.

@jpauli
Copy link
Member

jpauli commented Jul 11, 2016

I totally agree with @nikic here.

@charlesportwoodii
Copy link
Contributor Author

Great, thanks for your feedback everyone.

I can definitely change this up to use a config flag and check for the library that way. Dynamic linking should resolve the issues surrounding pthread.

From my limited understanding, there is no pressing need to replace bcrypt
with argon2 right now, argon2 is just slightly preferable. As such I thing
it's fine to only provide it optionally to start with.

You're correct. Bcrypt is still a perfectly valid and secure hashing algorithm. Argon2 is just another option.

- Configure flag now accepts --with-argon2 for dynamic linking with
  libargon2. Argon2 will be enabled in password_* only if this
  flag is passed.
- --with-argon2 config flag allows user passed directory for linking
- Added Argon2 specific tests to ensure existing tests do not fail
  when argon2 is disable
- Added "Skipped:" flag so argon2 tests would be skipped when
  PHP is compiled without Argon2 support
if (CHECK_LIB("Argon2Ref.lib", "argon2", PHP_ARGON2)
&& CHECK_HEADER_ADD_INCLUDE("argon2.h", "CFLAGS_ARGON2")) {
AC_DEFINE('HAVE_ARGON2LIB', 1);
EXTENSION("argon2", null, false);
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't look right, simply drop the call to EXTENSION() here and leave the AC_DEFINE().

Copy link
Contributor Author

@charlesportwoodii charlesportwoodii Jul 12, 2016

Choose a reason for hiding this comment

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

I was getting linker errors without the call to EXTENSION. Is there another macro I can use to get Argon2Ref.lib to be added to the linker? I wasn't seeing any other function for handling that.

Copy link
Member

Choose a reason for hiding this comment

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

Hi! It's been a while since I was tinkering around with this, but from vague memory this is how it works:

CHECK_LIB':

  • The second parameter is the target of where to link the Argon2Ref.lib to (as per extension), this can be set to empty ("") -- See win32\build\confutils.js
  • The third parameter is fine (as it uses PHP_ARGON2, which is the one from the --with-argon2=XXX)

CHECK_HEADER_ADD_INCLUDE

  • This sets the compiler flags for an extension (CFLAGS_ARGON2), and should simply be CFLAGS (we use this value for core things, like determining IPv6 support in win32\build\config.js).

TL;DR:
Line 7 should be: if (CHECK_LIB("Argon2Ref.lib", "", PHP_ARGON2)
Line 8 should be: && CHECK_HEADER_ADD_INCLUDE("argon2.h", "CFLAGS")) {

which should allow you to drop the EXTENSION() call.

(bear in mind I'm rusty, but else poke around the sources in win32\build)!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi! Thanks for pointing me in the right direction! I really appreciate it.

It looks like the trick is to set the second arguement of CHECK_LIB (target) to null instead of "". An empty string creates LIBS_ in the Makefile, whereas null correctly assigns it to LIBS.

Thanks again!

charlesportwoodii added a commit to charlesportwoodii/php-argon2-ext that referenced this pull request Jul 13, 2016
- Added Windows config script for building on Windows
- Updated tests
- Renamed constants from PASSWORD_ARGON* to ARGON*_PASSWORD
  to avoid potential conflicts with php/php-src#1997
- Bumping version for API change
@@ -61,7 +81,14 @@ static php_password_algo php_password_determine_algo(const char *hash, const siz
{
if (len > 3 && hash[0] == '$' && hash[1] == '2' && hash[2] == 'y' && len == 60) {
return PHP_PASSWORD_BCRYPT;
}
#if HAVE_ARGON2LIB
if (hash[0] == '$' && strstr(hash, "argon2i")) {
Copy link
Member

Choose a reason for hiding this comment

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

This will check whether argon2i is contained anywhere in the string. Maybe doing something more precise like memcmp(hash + 1, "argon2i", sizeof("argon2i") - 1), with appropriate length check beforehand, would be preferable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because of the memory, time, and parallelism factors, Argon2 hashes can be of variable length. Using the default constants listed in php_password.h, this string will be 99 characters long, however the change of constants would result in different sized hashes.

The other alternative is to try to parse out all the available Argon2 options on the spot - there's still a chance that the string isn't an Argon2 hash though even verifying those options. At the end of the day the hash, will be password to password_verify. If the string isn't a valid Argon2 hash it'll be detected there and the password_verify will return false.

Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

The comment about the length check was only to avoid out of bounds memcmp, not ensure a particular value. I.e. something along the lines of:

if (hash_len >= sizeof("$argon2i$")-1 && !memcmp(hash, "$argon2i$", sizeof("$argon2i$")-1)) {
    return PHP_PASSWORD_ARGON2I;
}
// ...

You're right that it probably doesn't matter anyway. The only problem I can imagine is the string "argon2i" occurring in the salt of a non-bcrypt non-argon hash (iirc password_verify also accepts crypt hashes that password_hash cannot generate). But this is probably pretty far fetched ^^

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 don't think it matters since bcrypt and crypt hashes are checked before hand, but I went ahead and converted it to your suggestion anyways. Your solution makes this a little more portable if new algorithms are added.

@nikic
Copy link
Member

nikic commented Jul 18, 2016

The implementation looks good to me (at least the C part, no idea about autoconf), nice work!

Using zend_string_alloc instead of char* for out and encoded
variables
@charlesportwoodii
Copy link
Contributor Author

@nikic Thanks, and thanks for the code review!

Argon2d is not suitable for password_hashing. To ensure best practices
within password_*, Argon2d was removed.

--with-argon2 implies the full feature set of Argon2, whereas this
feature only implements Argon2i within password_*. Consequently
the feature flag was renamed to --with-password-argon2
- Updating tests
- Adjusting cost factors:
 - memory_cost = 1 MiB
 - time_cost = 2
 - threads = 2
Test normal operation of password_verify() with argon2
--SKIPIF--
<?php
if (!defined('PASSWORD_ARGON2I')) die('Skipped: password_get_info not built with Argon2');
Copy link

Choose a reason for hiding this comment

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

password_get_info not built with Argon2 in password_verify test

@php-pulls php-pulls merged commit 35a74b9 into php:master Sep 8, 2016
@cmb69
Copy link
Member

cmb69 commented Sep 8, 2016

@charlesportwoodii ext/standard/tests/password/password_needs_rehash_argon2.phpt is failing for me on Windows x64 (I haven't checked other platforms yet) with the following diff:

001+ bool(true)
001- bool(false)

The other tests are running fine.

@charlesportwoodii
Copy link
Contributor Author

@cmb69,

I'm pretty sure the test file is the problem. Just let me verify that is the case and I'll have a patch up for you momentarily. I'll also provide a separate PR for the upgrading docs since this was forked from 7.1 instead of 7.2.

@charlesportwoodii
Copy link
Contributor Author

charlesportwoodii commented Sep 8, 2016

@cmb69,

Yeah, it's just the test file that needs to be updated after the constants were changed. The following diff should fix it. Git must've not picked up my last change set for this. Sorry for the trouble.

$ TEST_PHP_EXECUTABLE=./sapi/cli/php ./run-tests.php ext/standard/tests/password
=====================================================================
PHP         : ./sapi/cli/php 
PHP_SAPI    : cli
PHP_VERSION : 7.1.0-dev
ZEND_VERSION: 3.1.0-dev
PHP_OS      : Linux - Linux 4.4.0-36-generic #55-Ubuntu SMP Thu Aug 11 18:01:55 UTC 2016 x86_64
INI actual  : /home//Public/php-src
More .INIs  :   
CWD         : /home//Public/php-src
Extra dirs  : 
VALGRIND    : Not used
=====================================================================
Running selected tests.
PASS Test error operation of password_hash() with bcrypt hashing [ext/standard/tests/password/password_bcrypt_errors.phpt] 
PASS Test deprecated operation of password_hash() [ext/standard/tests/password/password_deprecated_salts.phpt] 
PASS Test normal operation of password_get_info() [ext/standard/tests/password/password_get_info.phpt] 
PASS Test normal operation of password_get_info() with Argon2 [ext/standard/tests/password/password_get_info_argon2.phpt] 
PASS Test error operation of password_get_info() [ext/standard/tests/password/password_get_info_error.phpt] 
PASS Test normal operation of password_hash() [ext/standard/tests/password/password_hash.phpt] 
PASS Test normal operation of password_hash() with argon2 [ext/standard/tests/password/password_hash_argon2.phpt] 
PASS Test error operation of password_hash() [ext/standard/tests/password/password_hash_error.phpt] 
PASS Test error operation of password_hash() with argon2 [ext/standard/tests/password/password_hash_error_argon2.phpt] 
PASS Test normal operation of password_needs_rehash() [ext/standard/tests/password/password_needs_rehash.phpt] 
PASS Test normal operation of password_needs_rehash() with argon2 [ext/standard/tests/password/password_needs_rehash_argon2.phpt] 
PASS Test error operation of password_needs_rehash() [ext/standard/tests/password/password_needs_rehash_error.phpt] 
PASS Test normal operation of password_verify) [ext/standard/tests/password/password_verify.phpt] 
PASS Test normal operation of password_verify() with argon2 [ext/standard/tests/password/password_verify_argon2.phpt] 
PASS Test error operation of password_verify() [ext/standard/tests/password/password_verify_error.phpt] 
=====================================================================
Number of tests :   15                15
Tests skipped   :    0 (  0.0%) --------
Tests warned    :    0 (  0.0%) (  0.0%)
Tests failed    :    0 (  0.0%) (  0.0%)
Expected fail   :    0 (  0.0%) (  0.0%)
Tests passed    :   15 (100.0%) (100.0%)
---------------------------------------------------------------------
Time taken      :    2 seconds
=====================================================================

The patch is listed below.

diff --git a/ext/standard/tests/password/password_needs_rehash_argon2.phpt b/ext/standard/tests/password/password_needs_rehash_argon2.phpt
index 1fec77d..88b7c4b 100644
--- a/ext/standard/tests/password/password_needs_rehash_argon2.phpt
+++ b/ext/standard/tests/password/password_needs_rehash_argon2.phpt
@@ -7,11 +7,11 @@ if (!defined('PASSWORD_ARGON2I')) die('Skipped: password_needs_rehash not built
 --FILE--
 <?php

-$hash = '$argon2i$v=19$m=65536,t=3,p=1$YkprUktYN0lHQTd2bWRFeA$79aA+6IvgclpDAJVoezProlqzIPy7do/P0sBDXS9Nn0';
+$hash = password_hash('test', PASSWORD_ARGON2I);
 var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I));
 var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['memory_cost' => 1<<17]));
-var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['time_cost' => 2]));
-var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['threads' => 2]));
+var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['time_cost' => 4]));
+var_dump(password_needs_rehash($hash, PASSWORD_ARGON2I, ['threads' => 4]));
 echo "OK!";
 ?>
 --EXPECT--

For the UPGRADING I'll just provide an extra diff here instead of a new PR.

@charlesportwoodii
Copy link
Contributor Author

charlesportwoodii commented Sep 8, 2016

@cmb69,

And a patch for the UPGRADING docs. Let me know if you're looking for more, or if I am missing anything.

diff --git a/UPGRADING b/UPGRADING
index d61280a..d15c4c9 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -32,6 +32,10 @@ PHP 7.2 UPGRADE NOTES
 - PCRE:
   . Added `J` modifier for setting PCRE_DUPNAMES.

+- Standard:
+  . Simplified password hashing API updated to support Argon2i hashes when PHP is compiled with libargon2
+    (https://wiki.php.net/rfc/argon2_password_hash).
+
 ========================================
 3. Changes in SAPI modules
 ========================================
@@ -44,6 +48,14 @@ PHP 7.2 UPGRADE NOTES
 5. Changed Functions
 ========================================

+- Standard:
+  . password_hash() can generate Argon2i hashes when the algorithm is set to PASSWORD_ARGON2I. 
+    When using PASSWORD_ARGON2I, the following cost factors may be set: 'memory_cost', 'time_cost', 
+    and 'threads'. These cost factors will default to 'PASSWORD_ARGON2_DEFAULT_MEMORY_COST', 
+    'PASSWORD_ARGON2_DEFAULT_TIME_COST', and 'PASSWORD_ARGON2_DEFAULT_THREADS' respectively if not set.
+  . password_verify() can verify Argon2i hashes.
+  . password_get_info() and password_needs_rehash() can accept Argon2i hashes.
+
 ========================================
 6. New Functions
 ========================================
@@ -82,6 +94,12 @@ PHP 7.2 UPGRADE NOTES
 10. New Global Constants
 ========================================

+- Standard:
+  . PASSWORD_ARGON2_DEFAULT_MEMORY_COST
+  . PASSWORD_ARGON2_DEFAULT_TIME_COST
+  . PASSWORD_ARGON2_DEFAULT_THREADS
+  . PASSWORD_ARGON2I
+
 ========================================
 11. Changes to INI File Handling
 ========================================

@cmb69
Copy link
Member

cmb69 commented Sep 8, 2016

Thanks, that looks fine! I'm going to commit.

@koenhoeijmakers
Copy link

koenhoeijmakers commented Jul 25, 2017

So i know i'm late to the party, But couldn't libsodium (which will be core) handle the Argon2 hashing? (in which case the argument of "not willing to bundle another lib" becomes obsolete), Or does libsodium also rely on libargon2?

Just curious and speculating ofcourse, but couldn't really find an answer to this, enlight me! :)

@denji
Copy link

denji commented Jul 25, 2017

Hi, what about support for the Argon2id (Argon2i + Argon2d)? https://github.com/P-H-C/phc-winner-argon2#argon2

@charlesportwoodii
Copy link
Contributor Author

charlesportwoodii commented Jul 25, 2017

@koenhoeijmakers

The bundling issue was resolved by using the libargon2 shared library 20161029, so that's no longer a concern. While sodium-ext is now in core, it's still dependent upon the libsodium library.

This was merged in nearly a year before sodium-ext was able to be merged in. The driving factors at the time were: not wanting to link this RFC to the Sodium RFC and have a bunch of last minute changes going on, and wanting clear separation of concerns between what Sodium does and the functionality available in password_hash/verify().

While I agree there could be some efficiencies gained by using crypto_pwhash() function from libsodium instead of directly calling the libargon2 functions, that would link password_hash/verify() to sodium-ext, and obscure what --with-sodium and --with-password-argon2 does. While I think most people would agree having these two items completely separated is a good idea, this is honestly a larger mailing list discussion.

@denji

I believe 7.2 is feature frozen, and the implemented version of libargon2 (20161029) didn't include Argon2id at the time. While #2582 enables this as a future possibility, it would require a mailing list discussion and a RFC to add a PASSWORD_ARGON2ID constant and implement that as another algorithm choice within password_hash/verify().

@denji
Copy link

denji commented Jul 29, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants