sodium_bin2base64( sodium_crypto_sign_publickey( $pair ), SODIUM_BASE64_VARIANT_ORIGINAL ), 'secret' => sodium_bin2base64( sodium_crypto_sign_secretkey( $pair ), SODIUM_BASE64_VARIANT_ORIGINAL ), 'created' => time(), ); openstation_network_option_set( OPENSTATION_NETWORK_KEYPAIR_OPTION, $stored ); return $stored; } /** * This install's public key, base64. * * @return string */ function openstation_network_public_key() { $pair = openstation_network_keypair(); return (string) $pair['public']; } /** * Whether a string is a well-formed base64 Ed25519 public key. * * @param mixed $key Candidate. * @return bool */ function openstation_network_is_public_key( $key ) { if ( ! is_string( $key ) || '' === $key ) { return false; } try { $bin = sodium_base642bin( $key, SODIUM_BASE64_VARIANT_ORIGINAL ); } catch ( SodiumException $e ) { return false; } return SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES === strlen( $bin ); } /** * Sign a message with this install's secret key. * * @param string $message Message. * @return string Base64 detached signature. */ function openstation_network_sign( $message ) { $pair = openstation_network_keypair(); $secret = sodium_base642bin( (string) $pair['secret'], SODIUM_BASE64_VARIANT_ORIGINAL ); return sodium_bin2base64( sodium_crypto_sign_detached( (string) $message, $secret ), SODIUM_BASE64_VARIANT_ORIGINAL ); } /** * Verify a detached signature against a public key. Malformed input of * any kind is a failed verification, never an exception. * * @param string $message Message. * @param string $signature Base64 signature. * @param string $public_key Base64 public key. * @return bool */ function openstation_network_verify( $message, $signature, $public_key ) { if ( ! is_string( $signature ) || ! openstation_network_is_public_key( $public_key ) ) { return false; } try { $sig = sodium_base642bin( $signature, SODIUM_BASE64_VARIANT_ORIGINAL ); $key = sodium_base642bin( $public_key, SODIUM_BASE64_VARIANT_ORIGINAL ); if ( SODIUM_CRYPTO_SIGN_BYTES !== strlen( $sig ) ) { return false; } return sodium_crypto_sign_verify_detached( $sig, (string) $message, $key ); } catch ( SodiumException $e ) { return false; } } /** * The line a signed request signs: method, REST route, timestamp. The * route rather than the full URL, because the URL an install reaches * another by is not always the URL that install knows itself by * (a proxy, an internal hostname, a container). * * @param string $method HTTP method. * @param string $route REST route, `/desktop-mode/v1/network`. * @param int $timestamp Unix time. * @return string */ function openstation_network_request_message( $method, $route, $timestamp ) { return strtoupper( (string) $method ) . "\n" . (string) $route . "\n" . (int) $timestamp; } /** * Headers that sign an outgoing request with this install's key. * * @param string $method HTTP method. * @param string $route REST route. * @return array */ function openstation_network_signed_headers( $method, $route ) { $timestamp = time(); return array( 'X-OpenStation-Key' => openstation_network_public_key(), 'X-OpenStation-Timestamp' => (string) $timestamp, 'X-OpenStation-Signature' => openstation_network_sign( openstation_network_request_message( $method, $route, $timestamp ) ), ); } /** * The public key that signed an incoming REST request, or '' when the * request is unsigned, stale, or its signature does not verify. * * Only says WHO signed; whether that key is trusted is the caller's * question (the hub answers it from its registry). * * @param WP_REST_Request $request Request. * @return string Base64 public key, or ''. */ function openstation_network_request_signer( WP_REST_Request $request ) { $key = (string) $request->get_header( 'X-OpenStation-Key' ); $timestamp = (int) $request->get_header( 'X-OpenStation-Timestamp' ); $signature = (string) $request->get_header( 'X-OpenStation-Signature' ); if ( '' === $key || '' === $signature || 0 === $timestamp ) { return ''; } if ( abs( time() - $timestamp ) > OPENSTATION_NETWORK_REQUEST_SKEW ) { return ''; } $message = openstation_network_request_message( $request->get_method(), $request->get_route(), $timestamp ); return openstation_network_verify( $message, $signature, $key ) ? $key : ''; }