2.53 http.c
  1/* snac - A simple, minimalistic ActivityPub instance */
  2/* copyright (c) 2022 - 2024 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                            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    char *host;
 28    char *target;
 29    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                            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    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(xs_dict *req, xs_str **err)
126/* check the signature */
127{
128    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 *pubkey;
135    char *p;
136
137    if (xs_is_null(sig_hdr)) {
138        *err = xs_fmt("missing 'signature' header");
139        return 0;
140    }
141
142    {
143        /* extract the values */
144        xs *l = xs_split(sig_hdr, ",");
145        xs_list *p = l;
146        xs_val *v;
147
148        while (xs_list_iter(&p, &v)) {
149            xs *kv = xs_split_n(v, "=", 1);
150
151            if (xs_list_len(kv) != 2)
152                continue;
153
154            xs *k1 = xs_strip_i(xs_dup(xs_list_get(kv, 0)));
155            xs *v1 = xs_strip_chars_i(xs_dup(xs_list_get(kv, 1)), " \"");
156
157            if (!strcmp(k1, "keyId"))
158                keyId = xs_dup(v1);
159            else
160            if (!strcmp(k1, "headers"))
161                headers = xs_dup(v1);
162            else
163            if (!strcmp(k1, "signature"))
164                signature = xs_dup(v1);
165            else
166            if (!strcmp(k1, "created"))
167                created = xs_dup(v1);
168            else
169            if (!strcmp(k1, "expires"))
170                expires = xs_dup(v1);
171        }
172    }
173
174    if (keyId == NULL || headers == NULL || signature == NULL) {
175        *err = xs_fmt("bad signature header");
176        return 0;
177    }
178
179    /* strip the # from the keyId */
180    if ((p = strchr(keyId, '#')) != NULL)
181        *p = '\0';
182
183    /* also strip cgi variables */
184    if ((p = strchr(keyId, '?')) != NULL)
185        *p = '\0';
186
187    xs *actor = NULL;
188    int status;
189
190    if (!valid_status((status = actor_request(NULL, keyId, &actor)))) {
191        *err = xs_fmt("actor request error %s %d", keyId, status);
192        return 0;
193    }
194
195    if ((p = xs_dict_get(actor, "publicKey")) == NULL ||
196        ((pubkey = xs_dict_get(p, "publicKeyPem")) == NULL)) {
197        *err = xs_fmt("cannot get pubkey from %s", keyId);
198        return 0;
199    }
200
201    /* now build the string to be signed */
202    xs *sig_str = xs_str_new(NULL);
203
204    {
205        xs *l = xs_split(headers, " ");
206        xs_list *p;
207        xs_val *v;
208
209        p = l;
210        while (xs_list_iter(&p, &v)) {
211            char *hc;
212            xs *ss = NULL;
213
214            if (*sig_str != '\0')
215                sig_str = xs_str_cat(sig_str, "\n");
216
217            if (strcmp(v, "(request-target)") == 0) {
218                ss = xs_fmt("%s: post %s", v, xs_dict_get(req, "path"));
219            }
220            else
221            if (strcmp(v, "(created)") == 0) {
222                ss = xs_fmt("%s: %s", v, created);
223            }
224            else
225            if (strcmp(v, "(expires)") == 0) {
226                ss = xs_fmt("%s: %s", v, expires);
227            }
228            else
229            if (strcmp(v, "host") == 0) {
230                hc = xs_dict_get(req, "host");
231
232                /* if there is no host header or some garbage like
233                   address:host has arrived here due to misconfiguration,
234                   signature verify will totally fail, so let's Leroy Jenkins
235                   with the global server hostname instead */
236                if (hc == NULL || xs_str_in(hc, ":") != -1)
237                    hc = xs_dict_get(srv_config, "host");
238
239                ss = xs_fmt("host: %s", hc);
240            }
241            else {
242                /* add the header */
243                if ((hc = xs_dict_get(req, v)) == NULL) {
244                    *err = xs_fmt("cannot find header '%s'", v);
245                    return 0;
246                }
247
248                ss = xs_fmt("%s: %s", v, hc);
249            }
250
251            sig_str = xs_str_cat(sig_str, ss);
252        }
253    }
254
255    if (xs_evp_verify(pubkey, sig_str, strlen(sig_str), signature) != 1) {
256        *err = xs_fmt("RSA verify error %s", keyId);
257        return 0;
258    }
259
260    return 1;
261}