Skip to content

crypt UPDATE replace libcrypt with internal password hashing based on OpenSSL/mbedTLS - #646

Closed
HanzlikPetr wants to merge 2 commits into
CESNET:develfrom
HanzlikPetr:pr_crypt_replace
Closed

HanzlikPetr wants to merge 2 commits into
CESNET:develfrom
HanzlikPetr:pr_crypt_replace

Conversation

@HanzlikPetr

Copy link
Copy Markdown
Contributor

Motivation

Password hashing for SSH authentication (storing clear-text passwords and verifying
stored hashes) was dependency on the system crypt_r(3) implementation.
This caused several problems:

  • it required linking and detecting libcrypt (with special-casing for QNX),
    complicating the build on platforms without it (e.g. musl, Windows);
  • a crypt_r() fallback had to be maintained in compat, which serialized
    crypt(3) behind a global mutex;

Since the library already depends on OpenSSL or mbedTLS for SSH/TLS, the hashing
is now implemented internally on top of these crypto backends.

Implementation

New internal nc_crypt() API:

  • src/crypt.c - algorithm core implementing the md5-crypt ("$1$"),
    SHA-256-crypt ("$5$") and SHA-512-crypt ("$6$") password hashing schemes,
    including "rounds=" support and crypt base64 encoding;
  • src/crypt_openssl.c / src/crypt_mbedtls.c - digest backends selected by
    the same CMake option as the SSH/TLS backend;
  • src/crypt_p.h - internal API with full documentation of the supported
    setting format and the deliberate deviations from crypt(3).

The algorithm output is byte-identical to the reference glibc implementation,
so hashes generated by crypt(3) (in /etc/shadow, etc.) verify unchanged.

Improvements over crypt_r():

  • no additional library dependency (uses the already required crypto backend);
  • thread-safe, no global state;
  • constant-time comparison of the computed and stored digests;
  • all intermediate values derived from the password are zeroed on all paths;
  • output buffer size is validated before the (expensive) hash is computed.

Behavior changes

  • Supported hash ids are now exactly those allowed by the iana-crypt-hash YANG
    type ("$1$", "$5$", "$6$"). Hashes in other system-specific formats (e.g.
    bcrypt "$2b$", yescrypt "$y$", DES) now fail to verify with a logged error.
  • Malformed settings (salt/digest characters outside the crypt base64 alphabet,
    "rounds=" outside the format range [1000, 999999999]) are rejected instead of
    being interpreted leniently. glibc-generated hashes are not affected.

Build system

  • libcrypt detection and the QNX login-library special case removed from
    CMakeLists.txt and UseCompat.cmake;
  • the crypt_r() compat fallback and crypt.h compat handling removed;
  • crypt(3) removed from the README requirements.

Tests

  • new tests/test_crypt.c cmocka test with reference vectors for all three
    schemes, generated by the reference glibc crypt_r(3). Covers salt
    truncation, "rounds=", empty salt, '$' in the password, non-ASCII passwords,
    wrong-password verification, corrupted stored hashes and invalid settings;

Comment thread src/crypt.c
Comment thread src/crypt.c
Comment thread src/crypt_p.h
Comment thread src/crypt.c
Comment on lines +46 to +56
/**
* @brief Parsed setting string.
*/
struct nc_crypt_setting {
enum nc_crypt_alg alg; /**< digest algorithm */
unsigned long rounds; /**< SHA-crypt rounds */
int rounds_custom; /**< "rounds=" was present in the setting */
char salt[NC_CRYPT_SHA_SALT_MAX + 1]; /**< salt, truncated and NUL-terminated */
size_t salt_len; /**< salt length */
const char *stored; /**< stored digest part of the setting, NULL if absent */
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Move to crypt_p.h.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just to clarify, is there a specific reason to move nc_crypt_setting to crypt_p.h? It's only used locally within crypt.c, and moving it would also require moving the NC_CRYPT_SHA_SALT_MAX macro. Should I move both of them to crypt_p.h?

Comment thread src/crypt.c
Comment thread src/crypt.c
Comment thread src/crypt.c
Comment on lines +104 to +107
* @return 0 on success.
* @return -2 on unsupported hash id.
* @return -3 on malformed setting.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pointless to have two different retvals when there is only 1 caller of this that doesn't differentiate them anyways, noticed it just because I found the -1 missing quite odd. See comment in crypt_p.h

Comment thread src/crypt.c
Comment on lines +274 to +280
/**
* @brief One operand of a stretching round.
*/
struct nc_crypt_feed {
const void *data; /**< operand bytes */
size_t len; /**< operand length */
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Possibly also move to crypt_p.h

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same question as above.

Comment thread src/crypt.c
Comment thread src/crypt_p.h
Comment on lines +63 to +68
* @return 0 on success.
* @return -1 on invalid arguments.
* @return -2 on unsupported hash id.
* @return -3 on malformed setting (including invalid salt/digest characters).
* @return -4 if the output buffer is too small.
* @return -5 on internal (digest backend) failure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just return -1 on error and don't deal with the other errors, just make sure to log them as errors properly - I don't think the callers care about the specific return values anyways.

Comment thread src/crypt.c
Comment thread src/crypt_p.h
Comment thread src/session_server_ssh.c
Comment on lines +1495 to +1497
ret = nc_crypt(received_pw, stored_pw, &cdata);
if (ret) {
ERR(NULL, "Verifying the password failed (%s).", nc_crypt_strerr(ret));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Probably the biggest problem with the current state of the PR:

This function also verifies hashes from /etc/shadow, when local-users-supported is disabled. Those aren't iana-crypt-hash values. Their format comes from the system libcrypt.

I don't think that adding more algorithms to nc_crypt is realistic, because OpenSSL/MbedTLS may not implement them at all.

I see two options:

  1. Split by source: config passwords use nc_crypt, and /etc/shadow keeps crypt_r() under HAVE_SHADOW only. Easy, but libcrypt stays for those builds.
  2. PAM only for system users: drop direct shadow verification and run password and kbdint through PAM. This removes libcrypt completely, but there's no system password auth without PAM.

Need to discuss and decide which way to go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants