signaturecheck.go (4439B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package middleware 19 20 import ( 21 "context" 22 "net/http" 23 "net/url" 24 25 "github.com/superseriousbusiness/gotosocial/internal/db" 26 "github.com/superseriousbusiness/gotosocial/internal/gtscontext" 27 "github.com/superseriousbusiness/gotosocial/internal/log" 28 29 "github.com/gin-gonic/gin" 30 "github.com/go-fed/httpsig" 31 ) 32 33 const ( 34 sigHeader = string(httpsig.Signature) 35 authHeader = string(httpsig.Authorization) 36 // untyped error returned by httpsig when no signature is present 37 noSigError = "neither \"" + sigHeader + "\" nor \"" + authHeader + "\" have signature parameters" 38 ) 39 40 // SignatureCheck returns a gin middleware for checking http signatures. 41 // 42 // The middleware first checks whether an incoming http request has been 43 // http-signed with a well-formed signature. If so, it will check if the 44 // domain that signed the request is permitted to access the server, using 45 // the provided uriBlocked function. If the domain is blocked, the middleware 46 // will abort the request chain with http code 403 forbidden. If it is not 47 // blocked, the handler will set the key verifier and the signature in the 48 // context for use down the line. 49 // 50 // In case of an error, the request will be aborted with http code 500. 51 func SignatureCheck(uriBlocked func(context.Context, *url.URL) (bool, db.Error)) func(*gin.Context) { 52 return func(c *gin.Context) { 53 ctx := c.Request.Context() 54 55 // Create the signature verifier from the request; 56 // this will error if the request wasn't signed. 57 verifier, err := httpsig.NewVerifier(c.Request) 58 if err != nil { 59 // Only actually *abort* the request with 401 60 // if a signature was present but malformed. 61 // Otherwise proceed with an unsigned request; 62 // it's up to other functions to reject this. 63 if err.Error() != noSigError { 64 log.Debugf(ctx, "http signature was present but invalid: %s", err) 65 c.AbortWithStatus(http.StatusUnauthorized) 66 } 67 68 return 69 } 70 71 // The request was signed! The key ID should be given 72 // in the signature so that we know where to fetch it 73 // from the remote server. This will be something like: 74 // https://example.org/users/some_remote_user#main-key 75 pubKeyIDStr := verifier.KeyId() 76 77 // Key can sometimes be nil, according to url parse 78 // func: 'Trying to parse a hostname and path without 79 // a scheme is invalid but may not necessarily return 80 // an error, due to parsing ambiguities'. Catch this. 81 pubKeyID, err := url.Parse(pubKeyIDStr) 82 if err != nil || pubKeyID == nil { 83 log.Warnf(ctx, "pubkey id %s could not be parsed as a url", pubKeyIDStr) 84 c.AbortWithStatus(http.StatusUnauthorized) 85 return 86 } 87 88 // If the domain is blocked we want to bail as fast as 89 // possible without the request proceeding further. 90 blocked, err := uriBlocked(ctx, pubKeyID) 91 if err != nil { 92 log.Errorf(ctx, "error checking block for domain %s: %s", pubKeyID.Host, err) 93 c.AbortWithStatus(http.StatusInternalServerError) 94 return 95 } 96 97 if blocked { 98 log.Infof(ctx, "domain %s is blocked", pubKeyID.Host) 99 c.AbortWithStatus(http.StatusForbidden) 100 return 101 } 102 103 // Assume signature was set on Signature header, 104 // but fall back to Authorization header if necessary. 105 signature := c.GetHeader(sigHeader) 106 if signature == "" { 107 signature = c.GetHeader(authHeader) 108 } 109 110 // Set relevant values on the request context 111 // to save some work further down the line. 112 ctx = gtscontext.SetHTTPSignatureVerifier(ctx, verifier) 113 ctx = gtscontext.SetHTTPSignature(ctx, signature) 114 ctx = gtscontext.SetHTTPSignaturePubKeyID(ctx, pubKeyID) 115 116 // Replace request with a shallow 117 // copy with the new context. 118 c.Request = c.Request.WithContext(ctx) 119 } 120 }