|
| 1 | +# Verify message payloads using publicly available ssh keys |
| 2 | + |
| 3 | +Most of the servers have a public/private ssh key pair available, |
| 4 | +so do most of the developers. Machines have their public parts |
| 5 | +available on port 22 (via `ssh-keyscan`) and devs upload theirs |
| 6 | +to Github and similar places. How about instead of authorizing |
| 7 | +our messages using passwords and tokens, we could just sign them |
| 8 | +using rsa/ecdsa/ed25519 keys we already use for SSH communication, |
| 9 | +and let the receiver verify that against publicly available records? |
| 10 | + |
| 11 | +## How it works? |
| 12 | + |
| 13 | +The sender identifies a private key they want to use. This might |
| 14 | +be an `/etc/ssh/ssh_host_ecdsa_key`, for which the public counterpart |
| 15 | +is available via `ssh-keyscan` to the receiver. For people, their |
| 16 | +personal ssh key uploaded to github could be used, as the public |
| 17 | +keys of each user are available at `github.com/username.keys`. |
| 18 | + |
| 19 | +> [!NOTE] |
| 20 | +> Most of the servers exposing something over HTTP also have |
| 21 | +> a HTTPS certificate issued. While this is not strictly in scope |
| 22 | +> of this package because that is not a SSH private key, it can |
| 23 | +> be easily converted to one, and since the cert is exposed on |
| 24 | +> port 443, it is easy to verify by the receivers. |
| 25 | +
|
| 26 | +`ssh-keygen` can be used to sign a message (see also |
| 27 | +[`bin/ssh-sign` script][ssh-sign]). Then they can send the payload |
| 28 | +with a corresponding `.sig` file to the receiver. |
| 29 | + |
| 30 | +```sh |
| 31 | +ssh-keygen -Y sign -f ~/.ssh/id_rsa -n "com.acme.namespace" payload.json |
| 32 | +curl --silent \ |
| 33 | + |
| 34 | + |
| 35 | + --form "username=mlebkowski" \ |
| 36 | + https://example.org |
| 37 | +``` |
| 38 | + |
| 39 | +The receiver in turn needs to determine the sender’s identity. |
| 40 | +This could be done explicitly (e.g. by passing their github username |
| 41 | +along with the message), or implicitly: by the sender’s IP address. |
| 42 | + |
| 43 | +Then the receiver confirms that the signature used one of the public |
| 44 | +keys available for a given sender, and that the signature matches |
| 45 | +the message payload. No passwords. |
| 46 | + |
| 47 | +See also [example][demo] for a simple proof of concept. |
| 48 | + |
| 49 | +## Installation |
| 50 | + |
| 51 | +```sh |
| 52 | +composer require wondernetwork/ssh-pubkey-payload-verification |
| 53 | +``` |
| 54 | + |
| 55 | +You will need a `psr/http-client` and `psr/simple-cache` implementations. |
| 56 | +Most frameworks will have this for you, but in case you don’t have them, |
| 57 | +you can just pick one from the top: |
| 58 | + |
| 59 | + * [packagist `psr/http-client` implementations][http-client] |
| 60 | + * [packagist `psr/http-factory` implementations][http-factory] |
| 61 | + * [packagist `psr/http-message` implementations][http-message] |
| 62 | + * [packagist `psr/simple-cache` implementations][simple-cache] |
| 63 | + |
| 64 | +## Usage |
| 65 | + |
| 66 | +```php |
| 67 | +use WonderNetwork\SshPubkeyPayloadVerification\ValidatorBuilder; |
| 68 | + |
| 69 | +// simples use case: |
| 70 | +$validator = ValidatorBuilder::start()->build(); |
| 71 | + |
| 72 | +// all available configuration options: |
| 73 | +$validator = ValidatorBuilder::start() |
| 74 | + // cache the fetched ssh-keyscans |
| 75 | + ->withCache($simpleCacheAdapter) |
| 76 | + |
| 77 | + // provide your own httpClient instead of relying on autodiscovery |
| 78 | + // if you’d like to cache calls to github, pass your own caching client |
| 79 | + ->withHttpClient($httpClient) |
| 80 | + ->withHttpMessageFactory($requestFactory) |
| 81 | + |
| 82 | + // when a request comes in, just execute ssh-keyscan |
| 83 | + // to get all their public keys. This is the default |
| 84 | + ->useRealtimeSshKeyscan() |
| 85 | + // instead of doing keyscan for each sender |
| 86 | + // pass a pre-determined known-hosts contents or filename |
| 87 | + ->withKnownHosts($knownHostsContent) |
| 88 | + ->withKnownHostsFile($knownHostsFilename) |
| 89 | + // replace the whole host keyscan implementation with your own |
| 90 | + ->withSshKeyscan($myFancyKeyscan) |
| 91 | + |
| 92 | + // replace the `KeyRepository` entirely and provide you own |
| 93 | + // way for getting list of keys of any given sender |
| 94 | + // this allows you to create custom sender types and sources |
| 95 | + // of their public keys |
| 96 | + ->withCustomKeyRepository() |
| 97 | + ->build(); |
| 98 | +``` |
| 99 | + |
| 100 | +Having the validator, we can now proceed to checking payloads: |
| 101 | + |
| 102 | +```php |
| 103 | +use WonderNetwork\SshPubkeyPayloadVerification\Validator; |
| 104 | +use WonderNetwork\SshPubkeyPayloadVerification\ValidatorException; |
| 105 | + |
| 106 | +/** @var Validator $validator */ |
| 107 | +try { |
| 108 | + $validator->validate( |
| 109 | + sender: sprintf("ssh://%s:%d", $_SERVER['REMOTE_ADDR'], 22), |
| 110 | + // alternatives: |
| 111 | + // sender: sprintf("https://%s:%d", $_SERVER['REMOTE_ADDR'], 443), |
| 112 | + // sender: "github://mlebkowski", |
| 113 | + namespace: "something you just need to agree on", |
| 114 | + // deliver it any way you like it |
| 115 | + message: $_POST['message'], |
| 116 | + // deliver it any way you like it: |
| 117 | + signature: $_POST['signature'], |
| 118 | + ); |
| 119 | + // good, we can act as if this sender/message are authenticated! |
| 120 | +} catch (ValidatorException $e) { |
| 121 | + // bad cookie, we discard the message |
| 122 | + // depending on $e, there might be some interesting context why it failed |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +## Security |
| 127 | + |
| 128 | +This solution is based on the same PKI infrastructure and |
| 129 | +mathematics behind RSA/ECC as the connection to your bank does. |
| 130 | +There are some caveats: |
| 131 | + |
| 132 | + * The `ssh-keyscan` is over an insecure connection. There are no |
| 133 | + equivalent of HTTPS certificates for SSH connections, so an |
| 134 | + attacker in position to alter your network traffic is able to |
| 135 | + spoof this. Similarly, if the target sender is taken over before |
| 136 | + the receiver had the chance to receive their public key list. |
| 137 | + |
| 138 | + Consider using a static known hosts file instead, or think about |
| 139 | + implementing a solution that uses HTTPS certificates if that’s |
| 140 | + something your senders have at hand. |
| 141 | + |
| 142 | + * This does not server as _authorization_ of the sender’s message, |
| 143 | + so you need to do this separately. Nor this secures the |
| 144 | + communication in any way, so think about transport layer security |
| 145 | + separately (delivering over HTTPS should be enough). This is |
| 146 | + stateless, so it doesn’t prevent replay attacks in any way. |
| 147 | + |
| 148 | + * There is no revocation mechanism other than manually evicting |
| 149 | + a key from your cache. |
| 150 | + |
| 151 | +## Cookbook |
| 152 | + |
| 153 | +### Using HTTPS certificates |
| 154 | + |
| 155 | +In order to use a certificate, you need to first convert it’s PEM private |
| 156 | +key into a SSH private key. |
| 157 | + |
| 158 | +```sh |
| 159 | +# copy from the place you keep HTTPS certificates |
| 160 | +cp /etc/letsencrypt/live/acme.example.org/privkey.pem ssh-signkey.rsa |
| 161 | +# limit permissions, or ssh-keyscan will refuse to work with that file |
| 162 | +chmod 0600 ssh-signkey.rsa |
| 163 | +# rewrite the key in-place (here: without using a passphrase) in OpenSSH format |
| 164 | +ssh-keygen -p -N "" -f ssh-signkey.rsa |
| 165 | +# use as any other SSH private key |
| 166 | +jq -Mcn .valid=true | ssh-keygen -Y sign -n example -f ssh-signkey.rsa |
| 167 | +``` |
| 168 | + |
| 169 | +[ssh-sign]: bin/ssh-sign |
| 170 | +[demo]: ./example/ |
| 171 | +[http-client]: https://packagist.org/providers/psr/http-client-implementation |
| 172 | +[simple-cache]: https://packagist.org/providers/psr/simple-cache-implementation |
| 173 | +[http-factory]: https://packagist.org/providers/psr/http-factory-implementation |
| 174 | +[http-message]: https://packagist.org/providers/psr/http-message-implementation |
0 commit comments