2.82 http.c
  1/* snac - A simple, minimalistic ActivityPub instance */
  2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
  3
  4#include "xs.h"
  5#include "xs_io.h"
  6#include "xs_openssl.h"
  7#include "xs_curl.h"
  8#include "xs_time.h"
  9#include "xs_json.h"
 10
 11#include "snac.h"
 12
 13xs_dict *http_signed_request_raw(const char *keyid, const char *seckey,
 14                            const char *method, const char *url,
 15                            const xs_dict *headers,
 16                            const char *body, int b_size,
 17                            int *status, xs_str **payload, int *p_size,
 18                            int timeout)
 19/* does a signed HTTP request */
 20{
 21    xs *l1 = NULL;
 22    xs *date = NULL;
 23    xs *digest = NULL;
 24    xs *s64 = NULL;
 25    xs *signature = NULL;
 26    xs *hdrs = NULL;
 27    const char *host;
 28    const char *target;
 29    const char *k, *v;
 30    xs_dict *response;
 31
 32    date = xs_str_utctime(0, "%a, %d %b %Y %H:%M:%S GMT");
 33
 34    {
 35        xs *s1 = xs_replace_n(url, "http:/" "/", "", 1);
 36        xs *s = xs_replace_n(s1, "https:/" "/", "", 1);
 37        l1 = xs_split_n(s, "/", 1);
 38    }
 39
 40    /* strip the url to get host and target */
 41    host = xs_list_get(l1, 0);
 42
 43    if (xs_list_len(l1) == 2)
 44        target = xs_list_get(l1, 1);
 45    else
 46        target = "";
 47
 48    /* digest */
 49    {
 50        xs *s;
 51
 52        if (body != NULL)
 53            s = xs_sha256_base64(body, b_size);
 54        else
 55            s = xs_sha256_base64("", 0);
 56
 57        digest = xs_fmt("SHA-256=%s", s);
 58    }
 59
 60    {
 61        /* build the string to be signed */
 62        xs *s = xs_fmt("(request-target): %s /%s\n"
 63                       "host: %s\n"
 64                       "digest: %s\n"
 65                       "date: %s",
 66                    strcmp(method, "POST") == 0 ? "post" : "get",
 67                    target, host, digest, date);
 68
 69        s64 = xs_evp_sign(seckey, s, strlen(s));
 70    }
 71
 72    /* build now the signature header */
 73    signature = xs_fmt("keyId=\"%s#main-key\","
 74                       "algorithm=\"rsa-sha256\","
 75                       "headers=\"(request-target) host digest date\","
 76                       "signature=\"%s\"",
 77                        keyid, s64);
 78
 79    /* transfer the original headers */
 80    hdrs = xs_dict_new();
 81    int c = 0;
 82    while (xs_dict_next(headers, &k, &v, &c))
 83        hdrs = xs_dict_append(hdrs, k, v);
 84
 85    /* add the new headers */
 86    if (strcmp(method, "POST") == 0)
 87        hdrs = xs_dict_append(hdrs, "content-type", "application/activity+json");
 88    else
 89        hdrs = xs_dict_append(hdrs, "accept",       "application/activity+json");
 90
 91    xs *user_agent = xs_fmt("%s; +%s/", USER_AGENT, srv_baseurl);
 92
 93    hdrs = xs_dict_append(hdrs, "date",         date);
 94    hdrs = xs_dict_append(hdrs, "signature",    signature);
 95    hdrs = xs_dict_append(hdrs, "digest",       digest);
 96    hdrs = xs_dict_append(hdrs, "host",         host);
 97    hdrs = xs_dict_append(hdrs, "user-agent",   user_agent);
 98
 99    response = xs_http_request(method, url, hdrs,
100                           body, b_size, status, payload, p_size, timeout);
101
102    srv_archive("SEND", url, hdrs, body, b_size, *status, response, *payload, *p_size);
103
104    return response;
105}
106
107
108xs_dict *http_signed_request(snac *snac, const char *method, const char *url,
109                            const xs_dict *headers,
110                            const char *body, int b_size,
111                            int *status, xs_str **payload, int *p_size,
112                            int timeout)
113/* does a signed HTTP request */
114{
115    const char *seckey = xs_dict_get(snac->key, "secret");
116    xs_dict *response;
117
118    response = http_signed_request_raw(snac->actor, seckey, method, url,
119                headers, body, b_size, status, payload, p_size, timeout);
120
121    return response;
122}
123
124
125int check_signature(const xs_dict *req, xs_str **err)
126/* check the signature */
127{
128    const char *sig_hdr = xs_dict_get(req, "signature");
129    xs *keyId = NULL;
130    xs *headers = NULL;
131    xs *signature = NULL;
132    xs *created = NULL;
133    xs *expires = NULL;
134    char *p;
135    const char *pubkey;
136    const char *k;
137
138    if (xs_is_null(sig_hdr)) {
139        *err = xs_fmt("missing 'signature' header");
140        return 0;
141    }
142
143    {
144        /* extract the values */
145        xs *l = xs_split(sig_hdr, ",");
146        int c = 0;
147        const xs_val *v;
148
149        while (xs_list_next(l, &v, &c)) {
150            xs *kv = xs_split_n(v, "=", 1);
151
152            if (xs_list_len(kv) != 2)
153                continue;
154
155            xs *k1 = xs_strip_i(xs_dup(xs_list_get(kv, 0)));
156            xs *v1 = xs_strip_chars_i(xs_dup(xs_list_get(kv, 1)), " \"");
157
158            if (!strcmp(k1, "keyId"))
159                keyId = xs_dup(v1);
160            else
161            if (!strcmp(k1, "headers"))
162                headers = xs_dup(v1);
163            else
164            if (!strcmp(k1, "signature"))
165                signature = xs_dup(v1);
166            else
167            if (!strcmp(k1, "created"))
168                created = xs_dup(v1);
169            else
170            if (!strcmp(k1, "expires"))
171                expires = xs_dup(v1);
172        }
173    }
174
175    if (keyId == NULL || headers == NULL || signature == NULL) {
176        *err = xs_fmt("bad signature header");
177        return 0;
178    }
179
180    /* strip the # from the keyId */
181    if ((p = strchr(keyId, '#')) != NULL)
182        *p = '\0';
183
184    /* also strip cgi variables */
185    if ((p = strchr(keyId, '?')) != NULL)
186        *p = '\0';
187
188    xs *actor = NULL;
189    int status;
190
191    if (!valid_status((status = actor_request(NULL, keyId, &actor)))) {
192        *err = xs_fmt("actor request error %s %d", keyId, status);
193        return 0;
194    }
195
196    if ((k = xs_dict_get(actor, "publicKey")) == NULL ||
197        ((pubkey = xs_dict_get(k, "publicKeyPem")) == NULL)) {
198        *err = xs_fmt("cannot get pubkey from %s", keyId);
199        return 0;
200    }
201
202    /* now build the string to be signed */
203    xs *sig_str = xs_str_new(NULL);
204
205    {
206        xs *l = xs_split(headers, " ");
207        xs_list *p;
208        const xs_val *v;
209
210        p = l;
211        while (xs_list_iter(&p, &v)) {
212            const char *hc;
213            xs *ss = NULL;
214
215            if (*sig_str != '\0')
216                sig_str = xs_str_cat(sig_str, "\n");
217
218            if (strcmp(v, "(request-target)") == 0) {
219                ss = xs_fmt("%s: post %s", v, xs_dict_get(req, "path"));
220            }
221            else
222            if (strcmp(v, "(created)") == 0) {
223                ss = xs_fmt("%s: %s", v, created);
224            }
225            else
226            if (strcmp(v, "(expires)") == 0) {
227                ss = xs_fmt("%s: %s", v, expires);
228            }
229            else
230            if (strcmp(v, "host") == 0) {
231                hc = xs_dict_get(req, "host");
232
233                /* if there is no host header or some garbage like
234                   address:host has arrived here due to misconfiguration,
235                   signature verify will totally fail, so let's Leroy Jenkins
236                   with the global server hostname instead */
237                if (hc == NULL || xs_str_in(hc, ":") != -1)
238                    hc = xs_dict_get(srv_config, "host");
239
240                ss = xs_fmt("host: %s", hc);
241            }
242            else {
243                /* add the header */
244                if ((hc = xs_dict_get(req, v)) == NULL) {
245                    *err = xs_fmt("cannot find header '%s'", v);
246                    return 0;
247                }
248
249                ss = xs_fmt("%s: %s", v, hc);
250            }
251
252            sig_str = xs_str_cat(sig_str, ss);
253        }
254    }
255
256    if (xs_evp_verify(pubkey, sig_str, strlen(sig_str), signature) != 1) {
257        *err = xs_fmt("RSA verify error %s", keyId);
258        return 0;
259    }
260
261    return 1;
262}