aboutsummaryrefslogtreecommitdiff
path: root/crates/arti-rpcserver/src/connection/auth/cookie.rs
blob: 9bf7cbd443a85d725bfcc6e233c26077508df216 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
//! Authentication where both parties prove the ability to read a "cookie" file from disk.
//!
//! For full documentation of the protocol, see `rpc-cookie-sketch.md`

use std::sync::{Arc, Mutex, Weak};

use derive_deftly::Deftly;
use tor_rpc_connect::auth::{
    RpcAuth,
    cookie::{Cookie, CookieAuthMac, CookieAuthNonce},
};
use tor_rpcbase::{self as rpc, templates::*};

use crate::Connection;

use super::{AuthenticateReply, AuthenticationFailure};

/// Begin authenticating on an RPC connection, using Cookie authentication.
///
/// In cookie authentication, both parties prove knowledge of a
/// shared secret, written to a file on disk.  This method
/// does not prevent MITM attacks on its own.
///
/// When cookie authentication is in use, clients use this method
/// to begin cookie authentication by telling the RPC server
/// a temporary nonce.
///
/// You typically won't need to invoke this method yourself;
/// instead, your RPC library (such as `arti-rpc-client-core`)
/// should handle it for you.
///
/// See `rpc-cookie-sketch.md` for full details of this protocol.
#[derive(Debug, serde::Deserialize, Deftly)]
#[derive_deftly(DynMethod)]
#[deftly(rpc(method_name = "auth:cookie_begin"))]
struct CookieBegin {
    /// A client-selected random nonce.
    ///
    /// Used as input to the `server_mac` calculation
    client_nonce: CookieAuthNonce,
}
impl rpc::RpcMethod for CookieBegin {
    type Output = CookieBeginReply;
    type Update = rpc::NoUpdates;
}

/// An RPC server's response to an `auth:cookie_begin` request.
#[derive(Debug, serde::Serialize)]
struct CookieBeginReply {
    /// Handle to an object to use for the client's subsequent `cookie_continue`.
    cookie_auth: rpc::ObjectId,
    /// The address that the server believes it is listening on.
    ///
    /// The client should verify that this is the exact string encoded
    /// in the connect point that it's trying to use.
    server_addr: String,
    /// A MAC proving that the server knows the secret cookie.
    ///
    /// 32 bytes long, encoded in 64 bytes of hexadecimal.  Case-insensitive.
    server_mac: CookieAuthMac,
    /// A secret nonce chosen by the server.
    ///
    /// 32 bytes long, encoded in 64 bytes of hexadecimal.  Case-insensitive.
    server_nonce: CookieAuthNonce,
}

/// An in-progress cookie authentication attempt.
///
/// This object is returned by `auth:cookie_begin`;
/// it can be used a single time with `auth:cookie_continue` to finish authentication.
#[derive(Deftly)]
#[derive_deftly(rpc::Object)]
struct CookieAuthInProgress {
    /// The cookie we're using to check the client's authentication.
    cookie: Arc<Cookie>,
    /// The RPC connection we'll use, if successful, to create a session.
    conn: Weak<Connection>,
    /// The nonce that the client sent us.
    client_nonce: CookieAuthNonce,
    /// The nonce that we sent to the client.
    ///
    /// If this is None, then the client already authenticated (or failed to authenticate) using
    /// this object once, and this object can no longer be used.
    ///
    /// (It is okay in our protocol for the client to authenticate more than once on the same
    /// connection, but we want to ensure that we use a fresh nonce each time.)
    server_nonce: Mutex<Option<CookieAuthNonce>>,
    /// The address that we believe we're listening on.
    server_addr: String,
}

/// Finish cookie authentication, returning a new RPC Session.
///
/// You typically won't need to invoke this method yourself;
/// instead, your RPC library (such as `arti-rpc-client-core`)
/// should handle it for you.
///
/// After invoking this method, the RPC library should use `rpc:release`
/// to drop its reference to the `CookieAuthInProgress`:
/// remember, as a rule,
/// RPC methods other than `rpc:release` do not consume the objects they are invoked on.
///
/// See `rpc-cookie-sketch.md` for full details of this protocol.
#[derive(Debug, serde::Deserialize, Deftly)]
#[derive_deftly(DynMethod)]
#[deftly(rpc(method_name = "auth:cookie_continue"))]
struct CookieContinue {
    /// MAC to prove knowledge of the secret cookie.
    ///
    /// 32 bytes long, encoded in 64 bytes of hexadecimal.  Case-insensitive.
    client_mac: CookieAuthMac,
}

impl rpc::RpcMethod for CookieContinue {
    type Output = AuthenticateReply;
    type Update = rpc::NoUpdates;
}

/// Invoke the `auth:cookie_begin` method on a connection.
async fn cookie_begin(
    unauth: Arc<Connection>,
    method: Box<CookieBegin>,
    ctx: Arc<dyn rpc::Context>,
) -> Result<CookieBeginReply, rpc::RpcError> {
    // Make sure that we actually want cookie authentication.
    let (cookie, server_addr) = match &unauth.require_auth {
        RpcAuth::Cookie {
            secret,
            server_address,
            ..
        } => (
            secret.load().map_err(|_| {
                // This is an internal error, since server cookies are always preloaded.
                rpc::RpcError::new(
                    "Somehow had an unloadable cookie".into(),
                    rpc::RpcErrorKind::InternalError,
                )
            })?,
            server_address.clone(),
        ),
        _ => return Err(AuthenticationFailure::IncorrectMethod.into()),
    };
    let mut rng = rand::rng();

    let server_nonce = CookieAuthNonce::new(&mut rng);

    let server_mac = cookie.server_mac(&method.client_nonce, &server_nonce, server_addr.as_str());

    let auth_in_progress = Arc::new(CookieAuthInProgress {
        cookie,
        conn: Arc::downgrade(&unauth),
        client_nonce: method.client_nonce,
        server_nonce: Mutex::new(Some(server_nonce.clone())),
        server_addr: server_addr.clone(),
    });
    let cookie_auth = ctx.register_owned(auth_in_progress);

    Ok(CookieBeginReply {
        cookie_auth,
        server_addr,
        server_mac,
        server_nonce,
    })
}

/// Invoke the `auth:cookie_continue` method on a [`CookieAuthInProgress`]
async fn cookie_continue(
    in_progress: Arc<CookieAuthInProgress>,
    method: Box<CookieContinue>,
    ctx: Arc<dyn rpc::Context>,
) -> Result<AuthenticateReply, rpc::RpcError> {
    // Make sure we haven't gotten another one of these.
    let Some(server_nonce) = in_progress
        .server_nonce
        .lock()
        .expect("lock poisoned")
        .take()
    else {
        return Err(AuthenticationFailure::CookieNonceReused.into());
    };

    let expected_client_mac = in_progress.cookie.client_mac(
        &in_progress.client_nonce,
        &server_nonce,
        &in_progress.server_addr,
    );

    if expected_client_mac != method.client_mac {
        return Err(AuthenticationFailure::IncorrectAuthentication.into());
    }
    let conn = in_progress
        .conn
        .upgrade()
        .ok_or(AuthenticationFailure::ShuttingDown)?;
    let auth = &super::RpcAuthentication {};
    let session = conn.create_session(auth)?;
    let session = ctx.register_owned(session);

    Ok(AuthenticateReply { session })
}

rpc::static_rpc_invoke_fn! {
    cookie_begin;
    cookie_continue;
}