Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

1package activitypub 2 3import ( 4 "bytes" 5 "crypto" 6 "crypto/rand" 7 "crypto/rsa" 8 "crypto/sha256" 9 "encoding/base64" 10 "fmt" 11 "io" 12 "net/http" 13 "strings" 14 "time" 15) 16 17// signedHeaders is the fixed set of headers this package signs and 18// verifies — the subset draft-cavage HTTP Signatures needs to bind a 19// request to its actual method, path, host, date, and body, which is what 20// every mainstream ActivityPub implementation (Mastodon included) expects. 21var signedHeaders = []string{"(request-target)", "host", "date", "digest"} 22 23// SignRequest signs req per draft-cavage HTTP Signatures: (request-target), 24// host, date, and a Digest of body are covered. Sets the Digest, Date, and 25// Signature headers on req. 26func SignRequest(req *http.Request, body []byte, keyID string, priv *rsa.PrivateKey) error { 27 digest := sha256.Sum256(body) 28 req.Header.Set("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(digest[:])) 29 req.Header.Set("Date", time.Now().UTC().Format(http.TimeFormat)) 30 31 signingString := buildSigningString(signedHeaders, req.Method, req.URL.RequestURI(), req.URL.Host, req.Header) 32 hashed := sha256.Sum256([]byte(signingString)) 33 sig, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, hashed[:]) 34 if err != nil { 35 return fmt.Errorf("sign request: %w", err) 36 } 37 38 req.Header.Set("Signature", fmt.Sprintf( 39 `keyId="%s",algorithm="rsa-sha256",headers="%s",signature="%s"`, 40 keyID, strings.Join(signedHeaders, " "), base64.StdEncoding.EncodeToString(sig), 41 )) 42 return nil 43} 44 45// VerifyRequest verifies an inbound request's Signature header against 46// pub, re-deriving the signing string from whichever headers the 47// signature itself claims to cover (a remote server may sign a different 48// header set than we do when sending). 49func VerifyRequest(r *http.Request, body []byte, pub *rsa.PublicKey) error { 50 params := parseSignatureHeader(r.Header.Get("Signature")) 51 if params["signature"] == "" { 52 return fmt.Errorf("missing or unparseable Signature header") 53 } 54 55 if digestHeader := r.Header.Get("Digest"); digestHeader != "" { 56 sum := sha256.Sum256(body) 57 want := "SHA-256=" + base64.StdEncoding.EncodeToString(sum[:]) 58 if !strings.EqualFold(digestHeader, want) { 59 return fmt.Errorf("digest does not match body") 60 } 61 } 62 63 headers := strings.Fields(params["headers"]) 64 if len(headers) == 0 { 65 headers = []string{"date"} 66 } 67 host := r.Header.Get("Host") 68 if host == "" { 69 host = r.Host 70 } 71 signingString := buildSigningString(headers, r.Method, r.URL.RequestURI(), host, r.Header) 72 73 sigBytes, err := base64.StdEncoding.DecodeString(params["signature"]) 74 if err != nil { 75 return fmt.Errorf("decode signature: %w", err) 76 } 77 hashed := sha256.Sum256([]byte(signingString)) 78 if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hashed[:], sigBytes); err != nil { 79 return fmt.Errorf("signature does not verify: %w", err) 80 } 81 return nil 82} 83 84// KeyID extracts the keyId parameter from an inbound request's Signature 85// header — usually the remote actor's URI with a #main-key fragment, 86// telling us whose public key to fetch and verify against. 87func KeyID(r *http.Request) string { 88 return parseSignatureHeader(r.Header.Get("Signature"))["keyId"] 89} 90 91func buildSigningString(headers []string, method, requestURI, host string, reqHeaders http.Header) string { 92 var lines []string 93 for _, h := range headers { 94 switch strings.ToLower(h) { 95 case "(request-target)": 96 lines = append(lines, fmt.Sprintf("(request-target): %s %s", strings.ToLower(method), requestURI)) 97 case "host": 98 lines = append(lines, "host: "+host) 99 default: 100 lines = append(lines, strings.ToLower(h)+": "+reqHeaders.Get(h)) 101 } 102 } 103 return strings.Join(lines, "\n") 104} 105 106func parseSignatureHeader(s string) map[string]string { 107 out := map[string]string{} 108 for _, part := range strings.Split(s, ",") { 109 part = strings.TrimSpace(part) 110 kv := strings.SplitN(part, "=", 2) 111 if len(kv) != 2 { 112 continue 113 } 114 out[kv[0]] = strings.Trim(kv[1], `"`) 115 } 116 return out 117} 118 119// PostSigned sends a signed POST of an activity+json body, authenticated 120// as keyID using priv. 121func PostSigned(client *http.Client, url, keyID string, priv *rsa.PrivateKey, body []byte) error { 122 req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) 123 if err != nil { 124 return fmt.Errorf("build request: %w", err) 125 } 126 req.Header.Set("Content-Type", activityContentType) 127 if err := SignRequest(req, body, keyID, priv); err != nil { 128 return err 129 } 130 131 resp, err := client.Do(req) 132 if err != nil { 133 return fmt.Errorf("post %s: %w", url, err) 134 } 135 defer resp.Body.Close() 136 if resp.StatusCode >= 300 { 137 respBody, _ := io.ReadAll(resp.Body) 138 return fmt.Errorf("post %s: HTTP %d: %s", url, resp.StatusCode, string(respBody)) 139 } 140 return nil 141}