PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / trunk
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin vtrunk
1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 0.8.6 All 33 releases
desktop-mode / includes / network / keys.php

keys.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin trunk, at includes/network/keys.php

203 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Network identity: one Ed25519 keypair per install.
4 *
5 * On a network of separate installs nothing is shared: no database, no
6 * users, no salts. What one install can do is sign, and what another
7 * can do is verify a signature against a public key it pinned when the
8 * two were paired. Every install therefore owns one keypair, generated
9 * on first use and kept in an option (a network option on a multisite,
10 * where every site is the same install). The public half is published
11 * on `GET /desktop-mode/v1/network/identity`; the secret half never
12 * leaves the database.
13 *
14 * Two things are signed with it: a member's requests to its hub (a
15 * timestamped line, so the hub knows which member is asking) and, in
16 * the hop token, the identity handed to another install. Both sides
17 * verify with `sodium_crypto_sign_verify_detached()`, which WordPress
18 * guarantees through its bundled sodium polyfill.
19 *
20 * @package OpenStation
21 */
22
23 defined( 'ABSPATH' ) || exit;
24
25 /** Where the keypair lives: a network option on a multisite, a site option elsewhere. */
26 const OPENSTATION_NETWORK_KEYPAIR_OPTION = 'openstation_network_keypair';
27
28 /** How far a signed request's timestamp may drift from the receiver's clock, in seconds. */
29 const OPENSTATION_NETWORK_REQUEST_SKEW = 300;
30
31 /**
32 * Read an install-wide option: the network's on a multisite, the site's
33 * elsewhere. Network identity belongs to the install, not to a site.
34 *
35 * @param string $key Option name.
36 * @param mixed $default Value when unset.
37 * @return mixed
38 */
39 function openstation_network_option_get( $key, $default = false ) {
40 return is_multisite() ? get_network_option( null, $key, $default ) : get_option( $key, $default );
41 }
42
43 /**
44 * Write an install-wide option. See {@see openstation_network_option_get()}.
45 *
46 * @param string $key Option name.
47 * @param mixed $value Value.
48 * @return bool
49 */
50 function openstation_network_option_set( $key, $value ) {
51 return is_multisite() ? (bool) update_network_option( null, $key, $value ) : (bool) update_option( $key, $value, false );
52 }
53
54 /**
55 * Delete an install-wide option. See {@see openstation_network_option_get()}.
56 *
57 * @param string $key Option name.
58 * @return bool
59 */
60 function openstation_network_option_delete( $key ) {
61 return is_multisite() ? (bool) delete_network_option( null, $key ) : (bool) delete_option( $key );
62 }
63
64 /**
65 * This install's keypair, generated on first use.
66 *
67 * @return array{public:string,secret:string,created:int} Base64 keys.
68 */
69 function openstation_network_keypair() {
70 $stored = openstation_network_option_get( OPENSTATION_NETWORK_KEYPAIR_OPTION );
71 if ( is_array( $stored ) && ! empty( $stored['public'] ) && ! empty( $stored['secret'] ) ) {
72 return $stored;
73 }
74 $pair = sodium_crypto_sign_keypair();
75 $stored = array(
76 'public' => sodium_bin2base64( sodium_crypto_sign_publickey( $pair ), SODIUM_BASE64_VARIANT_ORIGINAL ),
77 'secret' => sodium_bin2base64( sodium_crypto_sign_secretkey( $pair ), SODIUM_BASE64_VARIANT_ORIGINAL ),
78 'created' => time(),
79 );
80 openstation_network_option_set( OPENSTATION_NETWORK_KEYPAIR_OPTION, $stored );
81 return $stored;
82 }
83
84 /**
85 * This install's public key, base64.
86 *
87 * @return string
88 */
89 function openstation_network_public_key() {
90 $pair = openstation_network_keypair();
91 return (string) $pair['public'];
92 }
93
94 /**
95 * Whether a string is a well-formed base64 Ed25519 public key.
96 *
97 * @param mixed $key Candidate.
98 * @return bool
99 */
100 function openstation_network_is_public_key( $key ) {
101 if ( ! is_string( $key ) || '' === $key ) {
102 return false;
103 }
104 try {
105 $bin = sodium_base642bin( $key, SODIUM_BASE64_VARIANT_ORIGINAL );
106 } catch ( SodiumException $e ) {
107 return false;
108 }
109 return SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES === strlen( $bin );
110 }
111
112 /**
113 * Sign a message with this install's secret key.
114 *
115 * @param string $message Message.
116 * @return string Base64 detached signature.
117 */
118 function openstation_network_sign( $message ) {
119 $pair = openstation_network_keypair();
120 $secret = sodium_base642bin( (string) $pair['secret'], SODIUM_BASE64_VARIANT_ORIGINAL );
121 return sodium_bin2base64( sodium_crypto_sign_detached( (string) $message, $secret ), SODIUM_BASE64_VARIANT_ORIGINAL );
122 }
123
124 /**
125 * Verify a detached signature against a public key. Malformed input of
126 * any kind is a failed verification, never an exception.
127 *
128 * @param string $message Message.
129 * @param string $signature Base64 signature.
130 * @param string $public_key Base64 public key.
131 * @return bool
132 */
133 function openstation_network_verify( $message, $signature, $public_key ) {
134 if ( ! is_string( $signature ) || ! openstation_network_is_public_key( $public_key ) ) {
135 return false;
136 }
137 try {
138 $sig = sodium_base642bin( $signature, SODIUM_BASE64_VARIANT_ORIGINAL );
139 $key = sodium_base642bin( $public_key, SODIUM_BASE64_VARIANT_ORIGINAL );
140 if ( SODIUM_CRYPTO_SIGN_BYTES !== strlen( $sig ) ) {
141 return false;
142 }
143 return sodium_crypto_sign_verify_detached( $sig, (string) $message, $key );
144 } catch ( SodiumException $e ) {
145 return false;
146 }
147 }
148
149 /**
150 * The line a signed request signs: method, REST route, timestamp. The
151 * route rather than the full URL, because the URL an install reaches
152 * another by is not always the URL that install knows itself by
153 * (a proxy, an internal hostname, a container).
154 *
155 * @param string $method HTTP method.
156 * @param string $route REST route, `/desktop-mode/v1/network`.
157 * @param int $timestamp Unix time.
158 * @return string
159 */
160 function openstation_network_request_message( $method, $route, $timestamp ) {
161 return strtoupper( (string) $method ) . "\n" . (string) $route . "\n" . (int) $timestamp;
162 }
163
164 /**
165 * Headers that sign an outgoing request with this install's key.
166 *
167 * @param string $method HTTP method.
168 * @param string $route REST route.
169 * @return array<string,string>
170 */
171 function openstation_network_signed_headers( $method, $route ) {
172 $timestamp = time();
173 return array(
174 'X-OpenStation-Key' => openstation_network_public_key(),
175 'X-OpenStation-Timestamp' => (string) $timestamp,
176 'X-OpenStation-Signature' => openstation_network_sign( openstation_network_request_message( $method, $route, $timestamp ) ),
177 );
178 }
179
180 /**
181 * The public key that signed an incoming REST request, or '' when the
182 * request is unsigned, stale, or its signature does not verify.
183 *
184 * Only says WHO signed; whether that key is trusted is the caller's
185 * question (the hub answers it from its registry).
186 *
187 * @param WP_REST_Request $request Request.
188 * @return string Base64 public key, or ''.
189 */
190 function openstation_network_request_signer( WP_REST_Request $request ) {
191 $key = (string) $request->get_header( 'X-OpenStation-Key' );
192 $timestamp = (int) $request->get_header( 'X-OpenStation-Timestamp' );
193 $signature = (string) $request->get_header( 'X-OpenStation-Signature' );
194 if ( '' === $key || '' === $signature || 0 === $timestamp ) {
195 return '';
196 }
197 if ( abs( time() - $timestamp ) > OPENSTATION_NETWORK_REQUEST_SKEW ) {
198 return '';
199 }
200 $message = openstation_network_request_message( $request->get_method(), $request->get_route(), $timestamp );
201 return openstation_network_verify( $message, $signature, $key ) ? $key : '';
202 }
203