laminas / laminas-crypt

Strong cryptography tools and password hashing
https://docs.laminas.dev/laminas-crypt/
BSD 3-Clause "New" or "Revised" License
39 stars 24 forks source link

Feature request: Use SSH key from ssh-agent #28

Closed Luc45 closed 1 year ago

Luc45 commented 1 year ago

Feature Request

Q A
New Feature yes
RFC no
BC Break no

Summary

The feature request is to leverage the existing ssh-agent from the operating system to provide a password-protected SSH key for laminas-crypt encryption. This will reduce the need for the user to enter the password multiple times, as the ssh-agent will only prompt for the password once and hold the unencrypted key in memory for the duration of the boot.

Background

My understanding is that if laminas-crypt is using a password-protected SSH key, we need to pass the password to \Laminas\Crypt\PublicKey\Rsa\PrivateKey. So on every interaction with my CLI tool that uses encryption, the password is requested and then used to decrypt the config. This can be inconvenient and increase the risk of human error.

Example: On every interaction with the CLI tool, the password is requested in STDIN and used to decrypt:

$private_key = new PrivateKey( '~/.ssh/foo.pem', $password );

try {
    return ( new Hybrid() )->decrypt( $cipher_text, $private_key );
} catch ( \Exception $e ) {
    throw EncryptionException::decrypt_error( $e );
}

Proposed Solution

By leveraging the ssh-agent, the user can add the password-protected key to the local ssh-agent and use the private key provided by the ssh-agent to decrypt the data in the CLI. The ssh-agent holds the unencrypted key in memory, eliminating the need to enter the password multiple times.

Example

Here's an example of how the process would work:

Add the password-protected key to the local ssh-agent:

ssh-add -k ~/.ssh/id_rsa
Enter passphrase for /home/foo/.ssh/id_rsa: 
Identity added: /home/foo/.ssh/id_rsa
  1. Use the private key provided by the ssh-agent to decrypt the data in the CLI. The ssh-agent holds the unencrypted key in memory, so it won't prompt the user again for the decryption password.
github-actions[bot] commented 1 year ago

This package is considered feature-complete, and is now in security-only maintenance mode, following a decision by the Technical Steering Committee. If you have a security issue, please follow our security reporting guidelines. If you wish to take on the role of maintainer, please nominate yourself

Luc45 commented 1 year ago

I have come up with a workaround for this in the meantime. It's a sort of PHP-based ssh-agent.

Request A:

Request B:

This is very similar to how ssh-agent works in Unix, as it asks for the decryption password once and store the unencrypted key in memory for the duration of that boot.

Code:

/**
 * @return string|null The password stored in the shared memory, if any.
 */
protected function get_encryption_password_from_shared_memory() {
    // Early bail: If "shmop" is not available, simply do not persist the password in memory.
    if ( ! extension_loaded( 'shmop' ) ) {
        return null;
    }

    // Create shared memory space if it doesn't exist.
    $shared_memory = @shmop_open( ftok( __FILE__, 't' ), "c", 0600, 1000 );

    if ( $shared_memory === false ) {
        // Open it with "read-only" if it exists.
        $shared_memory = shmop_open( ftok( __FILE__, 't' ), "a", 0600, 1000 );
    }

    $password = shmop_read( $shared_memory, 0, 1000 );

    shmop_close( $shared_memory );

    if ( empty( trim( $password ) ) ) {
        return null;
    }

    return trim( $password );
}

/**
 * @param string $password The password to store in the shared memory.
 *
 * @return void
 */
protected function save_encryption_password_to_shared_memory( string $password ): void {
    // Early bail: If "shmop" is not available, simply do not persist the password in memory.
    if ( ! extension_loaded( 'shmop' ) ) {
        return;
    }

    // Create shared memory space if it doesn't exist.
    $shared_memory = @shmop_open( ftok( __FILE__, 't' ), "c", 0600, 1000 );

    if ( $shared_memory === false ) {
        // Open it with "write-only" if it exists.
        $shared_memory = shmop_open( ftok( __FILE__, 't' ), "w", 0600, 1000 );
    }

    $written = shmop_write( $shared_memory, str_pad( $password, 1000 ), 0 );

    if ( $written === false ) {
        throw EncryptionException::password_persist_exception();
    }
}
Luc45 commented 1 year ago

The code example sets Unix permissions 0600 for reading/writing the password used to unencrypt the private key. This ensures only the owner has access.

However, it is important to note that the shared memory used to store the password could be read by another program running under the same user.

For example, a malware could read the password from the shared memory and use it to decrypt the private key. If this risk is unacceptable, you may want to consider alternative solutions using native ssh-agent/Keychain from the OS.

By the way, if someone can explain to me how the SSH Agent protocol stores the unencrypted private key in memory without the risk above, I'd love to know more.

Luc45 commented 1 year ago

if someone can explain to me how the SSH Agent protocol stores the unencrypted private key in memory without the risk above, I'd love to know more

It seems the SSH Agent protocol stores unencrypted private keys in memory, making them vulnerable to being read. (Google "dump memory ssh agent")

This has been demonstrated through examples such as using memory dumps to extract private SSH keys from OpenSSH (see: https://research.nccgroup.com/2020/11/11/decrypting-openssh-sessions-for-fun-and-profit/) and through the use of machine learning to extract keys from memory dumps (see: https://arxiv.org/pdf/2209.05243.pdf).

This exploit requires root access, though, at which point the attacker could insert a keylogger and steal your SSH key password anyway.

That makes me feel better about the shmop approach.

Luc45 commented 1 year ago

And lastly, for those that land on this thread later, we can use a bash script to encrypt/decrypt using the local ssh-agent, leveraging the stored private keys: https://gist.github.com/wmertens/c4f2c4186c04dc5442bbe3396f2c12f6

Usage:

  1. Create a sample file with text: echo foo > foo
  2. Copy your public key (You can see the ones that are added to your ssh agent with ssh-add -L)
  3. Encrypt the file using your password-protected private key with your ssh-agent: ./ssh-crypt.bash -e 123 ssh-rsa {PUBLIC_KEY_HERE} < foo > bar
  4. Decrypt the file back to plain text: ./ssh-crypt.bash -d 123 ssh-rsa {PUBLIC_KEY_HERE} < bar > baz

Result:

➜ cat foo
foo
➜ cat bar
Salted__Æ+Ö5^_[4i<9c><85>^FR^K´lÙ^M^BK^Z>èH<9c>ìw^Ymµÿ&V<88><8e>ÚO½^@M¿
➜ cat baz
foo
Bash script, in case the link goes down ```bash #!/usr/bin/env bash # # ssh-crypt # # Bash function to encrypt/decrypt with your ssh-agent private key. # Requires the commands gzip, ssh-add, ssh-keygen and openssl. # # Uses bash-specific extensions like <<<$var to securely pass data. # # Wout.Mertens@gmail.com 2021-11-11 - MIT Licensed # # Copyright 2021 Wout Mertens # # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and # associated documentation files (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, publish, distribute, # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all copies or substantial # portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT # LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ssh-crypt() { if [ "$1" != -e ] && [ "$1" != -d ]; then echo "Usage: ssh-crypt - [seed] [pubkey-match] < infile > outfile" >&2 echo >&2 echo "* -e for encrypt, -d for decrypt" >&2 echo "* seed is used to generate the secret, recommended so you don't use the same secret everywhere" >&2 echo "* pubkey-match is used to select the first matching pubkey in the ssh-agent" >&2 echo "* define CRYPT_PUBKEY to provide your own" >&2 return 2 fi # === Select pubkey local pk if [ -n "$CRYPT_PUBKEY" ]; then pk="$CRYPT_PUBKEY" else # we can't use ecdsa, it always gives different signatures local keys=$(ssh-add -L | grep -v ecdsa) if [ -n "$3" ]; then keys=$(grep -- "$3" <<<"$keys") fi read pk <<<"$keys" fi if [ -z "$pk" ]; then echo "!!! Could not select a public key to use - verify ssh-add -L" return 1 fi # === Generate secret # We pass the pubkey as a file so ssh-keygen will look up the private key in the agent local secretText=$(ssh-keygen -Y sign -n hi -q -f /dev/fd/4 4<<<"$pk" <<<"$2") if [ $? -ne 0 ] || [ -z "$secretText" ]; then echo "!!! Cannot generate secret, is ssh-agent available?" >&2 return 1 fi # Get it on one line local secret=$(openssl dgst -sha512 -r <<<"$secretText") if [ $? -ne 0 ] || [ -z "$secret" ]; then echo "!!! Cannot generate secret, is openssl available?" >&2 return 1 fi # === Encrypt/decrypt # specify all settings so openssl upgrades don't change encryption local opts="-aes-256-cbc -md sha512 -pbkdf2 -iter 239823 -pass fd:4" # we use gzip both for compression and detecting bad secrets early if [ "$1" = -e ]; then gzip | openssl enc -e $opts 4<<<"$secret" else openssl enc -d $opts 4<<<"$secret" | gzip -d fi } # When sourcing this file for use in other scripts, use `LOAD_ONLY=true source ssh-crypt.bash` if [ -z "$LOAD_ONLY" ]; then ssh-crypt "$@" fi ```