2.95 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, xs_str **key_id)
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    /* copy the keyId */
186    *key_id = xs_dup(keyId);
187
188    xs *actor = NULL;
189    int status;
190
191    /* does it have ? variables? */
192    if ((p = strchr(keyId, '?')) != NULL) {
193        /* try first with them */
194        if (!valid_status((status = actor_request(NULL, keyId, &actor)))) {
195            *p = '\0';
196            /* retry stripping them */
197            status = actor_request(NULL, keyId, &actor);
198        }
199    }
200    else
201        status = actor_request(NULL, keyId, &actor);
202
203    if (!valid_status(status)) {
204        *err = xs_fmt("actor request error %s %d", keyId, status);
205        return 0;
206    }
207
208    if ((k = xs_dict_get(actor, "publicKey")) == NULL ||
209        ((pubkey = xs_dict_get(k, "publicKeyPem")) == NULL)) {
210        *err = xs_fmt("cannot get pubkey from %s", keyId);
211        return 0;
212    }
213
214    /* now build the string to be signed */
215    xs *sig_str = xs_str_new(NULL);
216
217    {
218        xs *l = xs_split(headers, " ");
219        xs_list *p;
220        const xs_val *v;
221
222        p = l;
223        while (xs_list_iter(&p, &v)) {
224            const char *hc;
225            xs *ss = NULL;
226
227            if (*sig_str != '\0')
228                sig_str = xs_str_cat(sig_str, "\n");
229
230            if (strcmp(v, "(request-target)") == 0) {
231                ss = xs_fmt("%s: post %s", v, xs_dict_get(req, "path"));
232            }
233            else
234            if (strcmp(v, "(created)") == 0) {
235                ss = xs_fmt("%s: %s", v, created);
236            }
237            else
238            if (strcmp(v, "(expires)") == 0) {
239                ss = xs_fmt("%s: %s", v, expires);
240            }
241            else
242            if (strcmp(v, "host") == 0) {
243                hc = xs_dict_get(req, "host");
244
245                /* if there is no host header or some garbage like
246                   address:host has arrived here due to misconfiguration,
247                   signature verify will totally fail, so let's Leroy Jenkins
248                   with the global server hostname instead */
249                if (hc == NULL || xs_str_in(hc, ":") != -1)
250                    hc = xs_dict_get(srv_config, "host");
251
252                ss = xs_fmt("host: %s", hc);
253            }
254            else {
255                /* add the header */
256                if ((hc = xs_dict_get(req, v)) == NULL) {
257                    *err = xs_fmt("cannot find header '%s'", v);
258                    return 0;
259                }
260
261                ss = xs_fmt("%s: %s", v, hc);
262            }
263
264            sig_str = xs_str_cat(sig_str, ss);
265        }
266    }
267
268    if (xs_evp_verify(pubkey, sig_str, strlen(sig_str), signature) != 1) {
269        *err = xs_fmt("RSA verify error %s", keyId);
270        return 0;
271    }
272
273    return 1;
274}