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