Skip to content

Commit 5647622

Browse files
committed
demo: dns txt validation
1 parent aa7d7ff commit 5647622

File tree

12 files changed

+191
-10
lines changed

12 files changed

+191
-10
lines changed

Readme.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Then the receiver confirms that the signature used one of the public
4444
keys available for a given sender, and that the signature matches
4545
the message payload. No passwords.
4646

47-
See also [example][demo] for a simple proof of concept.
47+
See also [example/web-demo][web-demo] for a simple proof of concept.
4848

4949
## Installation
5050

@@ -166,8 +166,16 @@ ssh-keygen -p -N "" -f ssh-signkey.rsa
166166
jq -Mcn .valid=true | ssh-keygen -Y sign -n example -f ssh-signkey.rsa
167167
```
168168

169+
### Extending key sources
170+
171+
Basically, you need to implement `KeyRepository` to return your own `KeyCollection`
172+
based on the `string $sender`. You can decorate the existing `StandardKeyRepository`
173+
to keep the built-in features and only add yours on top. An example how this could
174+
be done using DNS TXT entries is in [examples/dns-txt][dns-demo]
175+
169176
[ssh-sign]: bin/ssh-sign
170-
[demo]: ./example/
177+
[web-demo]: ./examples/web-demo
178+
[dns-demo]: ./examples/dns-txt-demo
171179
[http-client]: https://packagist.org/providers/psr/http-client-implementation
172180
[simple-cache]: https://packagist.org/providers/psr/simple-cache-implementation
173181
[http-factory]: https://packagist.org/providers/psr/http-factory-implementation

cli/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
BUILD_PATH := $(shell mktemp -d)
55

66
../bin/ssh-verify: tools/box ${BUILD_PATH}/vendor
7-
@echo "${BUILD_PATH}"
87
@tools/box/vendor/bin/box compile -d "${BUILD_PATH}"
98
@mv "${BUILD_PATH}/index.phar" ../bin/ssh-verify
9+
@rm -fr "${BUILD_PATH}"
1010

1111
${BUILD_PATH}/vendor:
1212
@cp composer.json composer.lock "${BUILD_PATH}"

examples/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.txt
2+
*.sig
3+
*.json

examples/dns-txt-demo/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
id_ecdsa
2+
id_ecdsa.pub
3+
message.txt
4+
message.txt.sig

examples/dns-txt-demo/demo.sh

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env bash
2+
3+
if [[ -z "$DO_TOKEN" ]]; then
4+
echo "This example uses DigitalOcean API key to set DNS entries, DO_TOKEN is required" >&2
5+
exit 2
6+
fi
7+
8+
if [[ ! -f "../../bin/ssh-verify" ]]; then
9+
{
10+
echo "The ssh-verify utility has not been built."
11+
echo "Use make -c cli from the root directory to build it."
12+
} >&2
13+
exit 3
14+
fi
15+
16+
set -euo pipefail
17+
18+
cleanup() {
19+
echo "Cleaning up"
20+
while read -r file; do rm -fr "$file" 2>/dev/null; done < .gitignore
21+
}
22+
23+
usage() {
24+
echo "usage: $0 <domain>" >&2
25+
exit 1
26+
}
27+
28+
set-dns-txt-record() {
29+
declare domain="$1" name="$2"
30+
31+
local payload
32+
payload="$(
33+
jq -n \
34+
--arg type TXT \
35+
--arg name "$name" \
36+
--arg data "$(cat "id_ecdsa.pub")" \
37+
'{ $type, $name, $data }'
38+
)"
39+
40+
curl --silent -X POST "https://api.digitalocean.com/v2/domains/$domain/records" \
41+
--header "Authorization: Bearer $DO_TOKEN" \
42+
--header "Content-Type: application/json" \
43+
--data "$payload" >/dev/null
44+
}
45+
46+
main() {
47+
declare domain="${1:-}"
48+
if [[ -z "$domain" ]]; then
49+
usage
50+
fi
51+
local namespace="dns-txt-demo"
52+
53+
trap 'cleanup' EXIT
54+
55+
# generate a random ecdsa key, because DO has a limit of 512 characters for TXT records
56+
ssh-keygen -q -t ecdsa -f ./id_ecdsa -N ""
57+
58+
local host_name
59+
host_name="dns-txt-demo-"$(openssl rand -base64 8 | tr -dc A-Za-z0-9 | head -c 10)
60+
61+
set-dns-txt-record "$domain" "$host_name"
62+
63+
# generate a message
64+
date > message.txt
65+
66+
# sign
67+
ssh-keygen -Y sign -n "$namespace" -f "id_ecdsa" message.txt
68+
69+
php verify.php "dns://$domain/$host_name" "$namespace" message.txt
70+
}
71+
72+
main "$@"

examples/dns-txt-demo/verify.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
use WonderNetwork\SshPubkeyPayloadVerification\Key\Key;
5+
use WonderNetwork\SshPubkeyPayloadVerification\Key\KeyCollection;
6+
use WonderNetwork\SshPubkeyPayloadVerification\Key\KeyRepository;
7+
use WonderNetwork\SshPubkeyPayloadVerification\KeyMismatchException;
8+
use WonderNetwork\SshPubkeyPayloadVerification\ValidatorBuilder;
9+
10+
require_once __DIR__ . '/../../vendor/autoload.php';
11+
12+
[, $sender, $namespace, $messageFile] = $argv;
13+
14+
$message = file_get_contents($messageFile);
15+
$signature = file_get_contents($messageFile.'.sig');
16+
17+
function get_keys_from_dns(string $domain, string $hostName): KeyCollection {
18+
$records = dns_get_record(sprintf('%s.%s', $hostName, $domain), DNS_TXT);
19+
if (false === $records) {
20+
return KeyCollection::empty();
21+
}
22+
23+
$keys = array_map(
24+
/** @param array{txt:string} $record */
25+
static function (array $record) {
26+
[$type, $key] = explode(' ', $record['txt']);
27+
return new Key(type: $type, publicKey: $key);
28+
},
29+
$records,
30+
);
31+
32+
return KeyCollection::of(...$keys);
33+
}
34+
35+
$dnsTxtRepository = new class (ValidatorBuilder::start()->standardKeyRepository()) implements KeyRepository {
36+
public function __construct(private KeyRepository $standardKeyRepository) {
37+
}
38+
39+
public function all(string $sender): KeyCollection {
40+
$url = parse_url($sender);
41+
return match ($url['scheme'] ?? null) {
42+
'dns' => get_keys_from_dns(
43+
$url['host'] ?? throw new RuntimeException('Host is required'),
44+
ltrim($url['path'] ?? throw new RuntimeException('Path is required'), '/'),
45+
),
46+
default => $this->standardKeyRepository->all($sender),
47+
};
48+
}
49+
};
50+
51+
$validator = ValidatorBuilder::start()
52+
->withCustomKeyRepository($dnsTxtRepository)
53+
->build();
54+
55+
try {
56+
$badSender = $sender . '-404';
57+
$validator->validate($badSender, $namespace, $message, $signature);
58+
} catch (KeyMismatchException $e) {
59+
if ($e->allowed->isEmpty()) {
60+
echo "Dummy validation failed as expected, because there are no TXT records ";
61+
echo "under the $badSender hostname\n";
62+
} else {
63+
echo "Unexpected error: {$e->getMessage()}\n";
64+
}
65+
} catch (\Throwable $e) {
66+
echo "Unexpected error: {$e->getMessage()}\n";
67+
}
68+
69+
try {
70+
$validator->validate($sender, $namespace, $message, $signature);
71+
} catch (\Throwable $e) {
72+
echo "Unexpected error: {$e->getMessage()}\n";
73+
exit(1);
74+
}
75+
echo "Validation successful\n";
File renamed without changes.
File renamed without changes.

example/index.php renamed to examples/web-demo/index.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
use WonderNetwork\SshPubkeyPayloadVerification\ValidatorBuilder;
55
use WonderNetwork\SshPubkeyPayloadVerification\ValidatorException;
66

7-
require __DIR__.'/../vendor/autoload.php';
7+
require __DIR__.'/../../vendor/autoload.php';
88

99
$sender = sprintf("ssh://%s", $_SERVER['REMOTE_ADDR']);
1010
$namespace = $_POST['namespace'];
File renamed without changes.

0 commit comments

Comments
 (0)