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// maxClockSkew bounds how far a signed request's Date header may drift 46// from this server's clock before it's rejected — without this, a 47// captured valid signed request (Follow, reply, ...) could be replayed 48// indefinitely. 49const maxClockSkew = 5 * time.Minute 50 51// VerifyRequest verifies an inbound request's Signature header against 52// pub, re-deriving the signing string from whichever headers the 53// signature itself claims to cover (a remote server may sign a different 54// header set than we do when sending). 55func VerifyRequest(r *http.Request, body []byte, pub *rsa.PublicKey) error { 56 params := parseSignatureHeader(r.Header.Get("Signature")) 57 if params["signature"] == "" { 58 return fmt.Errorf("missing or unparseable Signature header") 59 } 60 61 if dateHeader := r.Header.Get("Date"); dateHeader != "" { 62 signedAt, err := http.ParseTime(dateHeader) 63 if err != nil { 64 return fmt.Errorf("unparseable Date header: %w", err) 65 } 66 if skew := time.Since(signedAt); skew > maxClockSkew || skew < -maxClockSkew { 67 return fmt.Errorf("Date header too far from current time (skew %s), possible replay", skew) 68 } 69 } else { 70 return fmt.Errorf("missing Date header") 71 } 72 73 if digestHeader := r.Header.Get("Digest"); digestHeader != "" { 74 sum := sha256.Sum256(body) 75 want := "SHA-256=" + base64.StdEncoding.EncodeToString(sum[:]) 76 if !strings.EqualFold(digestHeader, want) { 77 return fmt.Errorf("digest does not match body") 78 } 79 } 80 81 headers := strings.Fields(params["headers"]) 82 if len(headers) == 0 { 83 headers = []string{"date"} 84 } 85 host := r.Header.Get("Host") 86 if host == "" { 87 host = r.Host 88 } 89 signingString := buildSigningString(headers, r.Method, r.URL.RequestURI(), host, r.Header) 90 91 sigBytes, err := base64.StdEncoding.DecodeString(params["signature"]) 92 if err != nil { 93 return fmt.Errorf("decode signature: %w", err) 94 } 95 hashed := sha256.Sum256([]byte(signingString)) 96 if err := rsa.VerifyPKCS1v15(pub, crypto.SHA256, hashed[:], sigBytes); err != nil { 97 return fmt.Errorf("signature does not verify: %w", err) 98 } 99 return nil 100} 101 102// KeyID extracts the keyId parameter from an inbound request's Signature 103// header — usually the remote actor's URI with a #main-key fragment, 104// telling us whose public key to fetch and verify against. 105func KeyID(r *http.Request) string { 106 return parseSignatureHeader(r.Header.Get("Signature"))["keyId"] 107} 108 109func buildSigningString(headers []string, method, requestURI, host string, reqHeaders http.Header) string { 110 var lines []string 111 for _, h := range headers { 112 switch strings.ToLower(h) { 113 case "(request-target)": 114 lines = append(lines, fmt.Sprintf("(request-target): %s %s", strings.ToLower(method), requestURI)) 115 case "host": 116 lines = append(lines, "host: "+host) 117 default: 118 lines = append(lines, strings.ToLower(h)+": "+reqHeaders.Get(h)) 119 } 120 } 121 return strings.Join(lines, "\n") 122} 123 124func parseSignatureHeader(s string) map[string]string { 125 out := map[string]string{} 126 for _, part := range strings.Split(s, ",") { 127 part = strings.TrimSpace(part) 128 kv := strings.SplitN(part, "=", 2) 129 if len(kv) != 2 { 130 continue 131 } 132 out[kv[0]] = strings.Trim(kv[1], `"`) 133 } 134 return out 135} 136 137// PostSigned sends a signed POST of an activity+json body, authenticated 138// as keyID using priv. 139func PostSigned(client *http.Client, url, keyID string, priv *rsa.PrivateKey, body []byte) error { 140 req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) 141 if err != nil { 142 return fmt.Errorf("build request: %w", err) 143 } 144 req.Header.Set("Content-Type", activityContentType) 145 if err := SignRequest(req, body, keyID, priv); err != nil { 146 return err 147 } 148 149 resp, err := client.Do(req) 150 if err != nil { 151 return fmt.Errorf("post %s: %w", url, err) 152 } 153 defer resp.Body.Close() 154 if resp.StatusCode >= 300 { 155 respBody, _ := io.ReadAll(io.LimitReader(resp.Body, maxResponseBody)) 156 return fmt.Errorf("post %s: HTTP %d: %s", url, resp.StatusCode, truncateForLog(respBody)) 157 } 158 return nil 159} 160 161// truncateForLog caps how much of a remote error body ends up embedded in 162// an error message (and from there, in logs) — a remote server's response 163// is untrusted content, and shouldn't get an unbounded write into our own 164// logging. 165func truncateForLog(b []byte) string { 166 const max = 500 167 if len(b) <= max { 168 return string(b) 169 } 170 return string(b[:max]) + "... (truncated)" 171}