Thanks to visit codestin.com
Credit goes to github.com

Skip to content

signing JWT's with SSH Agents #2022

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
terrafrost opened this issue Jul 29, 2024 · 10 comments
Closed

signing JWT's with SSH Agents #2022

terrafrost opened this issue Jul 29, 2024 · 10 comments

Comments

@terrafrost
Copy link
Member

terrafrost commented Jul 29, 2024

This is split off from #1735 (comment).

@WarriorXK - I'm curious how you're envisioning this would work.

So with ssh-agent it just tries to login with every key in the agent. When it succeeds you move on.

So if you have multiple keys in your ssh-agent then how would you know which one to use? You could do something like this I guess:

$agent = new \phpseclib3\System\SSH\Agent();
$identities = $agent->requestIdentities();
foreach ($identities as $identity) {
    if ($identity->getPublicKey()->toString() != 'whatever') break;
    if ($identity->getPublicKey()->getFingerprint() != 'whatever') break;
}

(i put two if statements there because either of them can be used; not sure why you'd want to use both in conjunction with one another)

Anyway, assuming you have a satisfactory answer to that then I guess the next thing would be... so you found the specific key you're wanting to use. So I guess a JWT consists of three base64 encoded parts separated by a dot (.):

  • The first part is the header and, when base64-decoded, looks like {"typ":"JWT","alg":"ES256"}.

  • The second part contains the claims and, when base64-decode, looks like this:

    {"iat":1722234232,"nbf":1722234232,"exp":1722234832,"iss":"https://api.my-awesome-app.io","aud":"https://client-app.io"}
    
  • The third part contains the signature, base64-encoded.

So assuming you got the first and second parts down pat (eg. $jwt[0] and $jwt[1]) then what you could do is you could is something like this:

$identity = $identity->withHash('sha256'); // idk if this is necessary
$sig = $identity->sign($jwt[0] . '.' . $jwt[1]); // $jwt[0] and $jwt[1] should retain their base64 encoding
$sig = \phpseclib3\File\ASN1::decodeBER($sig);
$sig = \phpseclib3\File\ASN1::asn1map($sig[0], \phpseclib3\File\ASN1\Maps\EcdsaSigValue::MAP);
$sig = $sig['r']->toBytes() . $sig['s']->toBytes();
$jwt[2] = sodium_bin2base64($sig, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);

That's only for ES256 however. And all that could be greatly simplified if I created a new signature plugin so that you could do this:

$identity = $identity->withHash('sha256'); // idk if this is necessary
$identity = $identity->withSignatureFormat('JWS');
$jwt[2] = $identity->sign($jwt[0] . '.' . $jwt[1]);

Anyway, does that sound to be pretty close what you're looking for? Also, if you're looking for support for a specific algorithm lmk

@terrafrost
Copy link
Member Author

Also, sorry for the delayed response - I was in Japan when you made your request and got back early last week and was then trying to play catch up at work.

@WarriorXK
Copy link

WarriorXK commented Jul 29, 2024

Hi @terrafrost, no worries about the delayed response, I am in no hurry :).

To give a bit of background, we use this to authenticate against an API from CLI context where normally we use OIDC. But since logging in through OIDC in a commandline (and opening a browser) does work quite as smooth we came up with the idea of using SSH keys for authentication since we already deploy those everywhere.

We are currently using the following piece of code to sign the JWT using the local agent:

exec('ssh-keygen -Y sign -n ' . escapeshellarg($namespace) . ' -f ' . escapeshellarg(stream_get_meta_data($pubKeyFile)['uri']) . ' < ' . stream_get_meta_data($srcFile)['uri']);

This requires us to specify with public key we want to use (written to a temp file $pubKeyFile), the ssh-keygen command will use the ssh-agent to find the matching available private key (Including from PIV devices such as YubiKeys).
Currently we use our own implementation code to determine which key to use based on the comment of the public key, if this would be implemented in PHPSecLib I don't think it should be the responsibility of the library to figure out which key to use. But I think specifying the public key as object or the fingerprint as argument to the sign function would be a preferred approach.

We use the following to verify the signature and validity on the server side:

$sshPublicKeys = []; // Read from /home/$jwtSubject/.ssh/authorized keys or LDAP

$mappedSSHPublicKeys = [];
foreach ($sshPublicKeys as $sshPublicKey) {

    $publicKey = PublicKeyLoader::load($sshPublicKey);
    $mappedSSHPublicKeys['SHA256:' . $publicKey->getFingerprint('sha256')] = $publicKey;

}

...

exec('/usr/bin/ssh-keygen -Y check-novalidate -n ' . escapeshellarg($namespace) . ' -s ' . escapeshellarg($tmpSignatureFilePath) . ' < ' . escapeshellarg($tmpBodyFilePath) . ' 2>&1', $output, $resultCode);
if ($resultCode !== 0) {
   return JWTValidationCode::InvalidSignature;
}

$outputLine = reset($output);
preg_match('/ +(?<fingerprint>[^\s]+)$/', $outputLine, $matches);
$usedKeyFingerprint = $matches['fingerprint'];

$logger?->log(LogLevel::DEBUG, 'Signature is valid and signed with key ' . $usedKeyFingerprint);
if (!isset($mappedSSHPublicKeys[$usedKeyFingerprint])) {
    return JWTValidationCode::UnknownKey;
}

So what we are looking for is not a JWT implementation perse, but more something that would be able to create a signature using the SSH agent and be able to verify the signature using a list of public SSH keys. I get the feeling that the second part is already possible using PHPSecLib, I am just unsure how. The challenging one is the first part, communicating with an ssh-agent.

We already have this working, but it would be amazing if we could remove all dependencies on executing commands as it would limit the ease of deployment and is prone to break.

@terrafrost
Copy link
Member Author

Currently we use our own implementation code to determine which key to use based on the comment of the public key, if this would be implemented in PHPSecLib I don't think it should be the responsibility of the library to figure out which key to use. But I think specifying the public key as object or the fingerprint as argument to the sign function would be a preferred approach.

I can implement a findIdentityByPublicKey(PublicKey $key) method in System/SSH/Agent.php and perhaps a findIdentityByFingerprint(string $fingerprint).

So what we are looking for is not a JWT implementation perse, but more something that would be able to create a signature using the SSH agent and be able to verify the signature using a list of public SSH keys. I get the feeling that the second part is already possible using PHPSecLib, I am just unsure how. The challenging one is the first part, communicating with an ssh-agent.

The former - creating signatures - should be possible using the code I posted:

$identity = $identity->withHash('sha256'); // idk if this is necessary
$sig = $identity->sign($jwt[0] . '.' . $jwt[1]); // $jwt[0] and $jwt[1] should retain their base64 encoding
$sig = \phpseclib3\File\ASN1::decodeBER($sig);
$sig = \phpseclib3\File\ASN1::asn1map($sig[0], \phpseclib3\File\ASN1\Maps\EcdsaSigValue::MAP);
$sig = $sig['r']->toBytes() . $sig['s']->toBytes();
$jwt[2] = sodium_bin2base64($sig, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);

That's for ES256 but I can simplify that with some code changes.

I haven't tested it for any of the RSA algorithms but I imagine this would work for RS256:

$identity = $identity->withPadding(RSA::SIGNATURE_PKCS1)->withHash('sha256');
$sig = $identity->sign($jwt[0] . '.' . $jwt[1]); // $jwt[0] and $jwt[1] should retain their base64 encoding
$jwt[2] = sodium_bin2base64($sig, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING);

For verifying you should just be able to do something like this (untested):

$jwt = explode('.', $jwt);
echo $public->verify($jwt[0] . '.' . $jwt[1], $jwt[2]) ? 'good' : 'bad';

Obviously you'd need to set the padding / hash as appropriate, depending on the algorithm being used.

@WarriorXK
Copy link

I just managed to get this to work. I had no idea you had an SSH agent framework in this library, this is amazing.

The only issue that remains for me is that the comment field key is not available to us even though its already being retrieved from the agent (Agent.php line 179). We need it to determine which key to use since YubiKeys expose 2 different keys. Is it possible to add this to the identity or the public key object somehow?

@terrafrost
Copy link
Member Author

The only issue that remains for me is that the comment field key is not available to us even though its already being retrieved from the agent (Agent.php line 179). We need it to determine which key to use since YubiKeys expose 2 different keys. Is it possible to add this to the identity or the public key object somehow?

See 10075ea

Also, https://phpseclib.com/docs/jwt shows how to use PrivateKey objects (or Identity objects) to create various different JWT signatures:

@terrafrost
Copy link
Member Author

Also, https://phpseclib.com/docs/jwt shows how to use PrivateKey objects (or Identity objects) to create various different JWT signatures:

Some of those examples will work better on the next release of phpseclib due to these two commits:

2276cf5
45b98d8

@WarriorXK
Copy link

Great! Thank you so much for your assistance. Just a final question, do you follow a generic release schedule so we know when to expect these commits to be available as a tagged release?

@terrafrost
Copy link
Member Author

terrafrost commented Aug 7, 2024 via email

@terrafrost
Copy link
Member Author

3.0.40 has been released!

@WarriorXK
Copy link

Great, thank you so much for your time!

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

No branches or pull requests

2 participants