• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

go-ap / client / #5

11 Apr 2026 04:03PM UTC coverage: 35.658% (-0.3%) from 35.992%
#5

push

sourcehut

mariusor
More tests, but still quite far from covering the important bits

18 of 84 new or added lines in 5 files covered. (21.43%)

10 existing lines in 1 file now uncovered.

363 of 1018 relevant lines covered (35.66%)

1.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.84
/s2s/httpsignatures.go
1
package s2s
2

3
import (
4
        "bytes"
5
        "context"
6
        "crypto"
7
        "crypto/ecdsa"
8
        "crypto/ed25519"
9
        "crypto/rsa"
10
        "crypto/x509"
11
        "encoding/pem"
12
        "fmt"
13
        "io"
14
        "net/http"
15
        "strings"
16
        "sync"
17
        "time"
18

19
        "git.sr.ht/~mariusor/lw"
20
        e "github.com/common-fate/httpsig/alg_ecdsa"
21
        ed "github.com/common-fate/httpsig/alg_ed25519"
22
        r "github.com/common-fate/httpsig/alg_rsa"
23
        "github.com/common-fate/httpsig/signer"
24
        vocab "github.com/go-ap/activitypub"
25
        "github.com/go-ap/errors"
26
        "github.com/go-fed/httpsig"
27
)
28

29
var (
30
        nilLogger = lw.Dev(lw.SetOutput(io.Discard))
31

32
        digestAlgorithm     = httpsig.DigestSha256
33
        signatureExpiration = int64(time.Hour.Seconds())
34
)
35

36
type HTTPSignatureTransport struct {
37
        signer.Transport
38

39
        // m is needed for ensuring the signer.Transport CoveredComponents writes and reads
40
        // don't cause a race condition.
41
        m     sync.RWMutex
42
        Key   crypto.PrivateKey
43
        Actor *vocab.Actor
44

45
        l lw.Logger
46
}
47

48
type OptionFn func(transport *HTTPSignatureTransport) error
49

50
func WithTransport(tr http.RoundTripper) OptionFn {
1✔
51
        return func(h *HTTPSignatureTransport) error {
2✔
52
                h.Transport.BaseTransport = tr
1✔
53
                return nil
1✔
54
        }
1✔
55
}
56

57
func NoRFC9421(h *HTTPSignatureTransport) error {
1✔
58
        h.Transport.Alg = nil
1✔
59
        return nil
1✔
60
}
1✔
61

NEW
62
func WithCoveredComponents(s ...string) OptionFn {
×
NEW
63
        return func(h *HTTPSignatureTransport) error {
×
NEW
64
                h.Transport.CoveredComponents = s
×
NEW
65
                return nil
×
NEW
66
        }
×
67
}
68

69
func WithActor(act *vocab.Actor, prv crypto.PrivateKey) OptionFn {
10✔
70
        return func(h *HTTPSignatureTransport) error {
20✔
71
                h.Actor = act
10✔
72
                h.Key = prv
10✔
73

10✔
74
                if act == nil {
12✔
75
                        return nil
2✔
76
                }
2✔
77

78
                actorPubKey, err := toCryptoPublicKey(act.PublicKey)
8✔
79
                if err != nil {
9✔
80
                        return errors.Annotatef(err, "unable to sign request, Actor public key type %T is invalid", actorPubKey)
1✔
81
                }
1✔
82

83
                h.Transport.KeyID = string(act.PublicKey.ID)
7✔
84
                if prv == nil {
8✔
85
                        return nil
1✔
86
                }
1✔
87
                switch pk := prv.(type) {
6✔
88
                case *rsa.PrivateKey:
2✔
89
                        pub, _ := pk.Public().(*rsa.PublicKey)
2✔
90
                        if !pub.Equal(actorPubKey) {
2✔
91
                                return keyMismatchErr(pk, actorPubKey)
×
92
                        }
×
93
                        h.Transport.Alg = r.NewRSAPKCS256Signer(pk)
2✔
94
                case *ecdsa.PrivateKey:
1✔
95
                        pub, _ := pk.Public().(*ecdsa.PublicKey)
1✔
96
                        if !pub.Equal(actorPubKey) {
1✔
97
                                return keyMismatchErr(pk, actorPubKey)
×
98
                        }
×
99
                        h.Transport.Alg = e.NewP384Signer(pk)
1✔
100
                case ed25519.PrivateKey:
3✔
101
                        pub, _ := pk.Public().(ed25519.PublicKey)
3✔
102
                        if !pub.Equal(actorPubKey) {
3✔
103
                                return keyMismatchErr(pk, actorPubKey)
×
104
                        }
×
105
                        h.Transport.Alg = &ed.Ed25519{PrivateKey: pk, PublicKey: pub}
3✔
106
                }
107
                return nil
6✔
108
        }
109
}
110

111
func WithLogger(l lw.Logger) OptionFn {
2✔
112
        return func(h *HTTPSignatureTransport) error {
4✔
113
                h.l = l
2✔
114
                if l != nil {
3✔
115
                        h.Transport.OnDeriveSigningString = func(ctx context.Context, stringToSign string) {
2✔
116
                                l.Debugf("String to sign: %s", stringToSign)
1✔
117
                        }
1✔
118
                }
119
                return nil
2✔
120
        }
121
}
122

123
func WithApplicationTag(t string) OptionFn {
2✔
124
        return func(h *HTTPSignatureTransport) error {
4✔
125
                h.Transport.Tag = t
2✔
126
                return nil
2✔
127
        }
2✔
128
}
129

130
// New initializes the HTTPSignatureTransport
131
// TODO(marius): we need to add to the return values the errors
132
//  that might come from the initialization functions.
133
func New(initFns ...OptionFn) *HTTPSignatureTransport {
6✔
134
        h := new(HTTPSignatureTransport)
6✔
135
        h.Transport.CoveredComponents = FetchCoveredComponents
6✔
136
        h.l = nilLogger
6✔
137
        for _, fn := range initFns {
11✔
138
                if err := fn(h); err != nil {
5✔
139
                        h.l.Errorf("unable to initialize HTTP Signature transport: %s", err)
×
140
                        return h /*, err*/
×
141
                }
×
142
        }
143
        return h /*, nil*/
6✔
144
}
145

146
type privateKey interface {
147
        Public() crypto.PublicKey
148
}
149

150
func pemEncodePublicKey(prvKey crypto.PrivateKey) string {
1✔
151
        prv, ok := prvKey.(privateKey)
1✔
152
        if !ok {
1✔
153
                return fmt.Sprintf("invalid private key: %T", prvKey)
×
154
        }
×
155
        pubEnc, err := x509.MarshalPKIXPublicKey(prv.Public())
1✔
156
        if err != nil {
1✔
157
                return "invalid public key: %s" + err.Error()
×
158
        }
×
159
        p := pem.Block{
1✔
160
                Type:  "PUBLIC KEY",
1✔
161
                Bytes: pubEnc,
1✔
162
        }
1✔
163
        return strings.ReplaceAll(string(pem.EncodeToMemory(&p)), "\n", "")
1✔
164
}
165

166
// RoundTrip dispatches the received request after signing it
167
func (s *HTTPSignatureTransport) RoundTrip(req *http.Request) (*http.Response, error) {
4✔
168
        if s.BaseTransport == nil {
7✔
169
                s.BaseTransport = http.DefaultTransport
3✔
170
        }
3✔
171
        or := *req
4✔
172
        isFetchRequest := req.Method == http.MethodGet || req.Method == http.MethodHead
4✔
173

4✔
174
        if req.URL != nil && req.URL.Path == "" {
8✔
175
                req.URL.Path = "/"
4✔
176
        }
4✔
177
        if s.Actor != nil {
6✔
178
                req = cloneRequest(&or) // per RoundTripper contract
2✔
179
                if s.Transport.Alg != nil {
3✔
180
                        res, err := s.Transport.RoundTrip(req)
1✔
181
                        if err == nil && res.StatusCode < http.StatusBadRequest {
2✔
182
                                return res, nil
1✔
183
                        }
1✔
184
                }
185

186
                // NOTE(marius): if the RFC9421 signed request has failed (possibly due to the server not supporting it)
187
                // we fall back to signing with the draft 6 compatible algorithm.
188
                lctx := lw.Ctx{"key": pemEncodePublicKey(s.Key), "actor": s.Actor.ID}
1✔
189
                if err := s.signRequest(req); err != nil && s.l != nil {
1✔
190
                        s.l.WithContext(lctx, lw.Ctx{"err": err.Error()}).Errorf("unable to sign request")
×
191
                } else {
1✔
192
                        s.l.WithContext(lctx).Debugf("signed request")
1✔
193
                }
1✔
194
        }
195

196
        res1, err := s.BaseTransport.RoundTrip(req)
3✔
197
        // NOTE(marius): if the RoundTrip fails to produce a response and this is a fetch request,
3✔
198
        // we can try again with the original request which doesn't have the Signature header.
3✔
199
        //
3✔
200
        // For the other types of requests that have succeeded, we return now.
3✔
201
        if !isFetchRequest || err == nil {
6✔
202
                return res1, err
3✔
203
        }
3✔
204

205
        // NOTE(marius): if we received an actual error we try the request again, but unsigned.
206
        //
207
        // This is a pretty hacky mitigation for loading Public Keys for Actors on other instances, which
208
        // sometimes triggers a feedback loop if the instance tries to authorize the signing actor in its turn.
209
        //
210
        // I detailed that faulty behaviour in ticket:
211
        //  https://todo.sr.ht/~mariusor/go-activitypub/301
212
        return s.BaseTransport.RoundTrip(&or)
×
213
}
214

215
func toCryptoPublicKey(key vocab.PublicKey) (crypto.PublicKey, error) {
8✔
216
        block, _ := pem.Decode([]byte(key.PublicKeyPem))
8✔
217
        if block == nil {
9✔
218
                return nil, errors.Errorf("invalid PEM decode on public key")
1✔
219
        }
1✔
220
        return x509.ParsePKIXPublicKey(block.Bytes)
7✔
221
}
222

223
func keyMismatchErr(pk crypto.PrivateKey, pub crypto.PublicKey) error {
×
224
        return errors.Newf("unable to sign request, mismatch between the Actor's public and private key: %T : %T", pub, pk)
×
225
}
×
226

227
func (s *HTTPSignatureTransport) signRequest(req *http.Request) error {
1✔
228
        if !s.Actor.PublicKey.ID.IsValid() {
1✔
229
                return errors.Newf("unable to sign request, invalid Actor public key ID")
×
230
        }
×
231

232
        keyID := s.Actor.PublicKey.ID
1✔
233

1✔
234
        headers := HeadersToSign
1✔
235
        bodyBuf := bytes.Buffer{}
1✔
236
        if req.Body != nil {
1✔
237
                if _, err := io.Copy(&bodyBuf, req.Body); err == nil {
×
238
                        req.Body = io.NopCloser(&bodyBuf)
×
239
                        headers = append(HeadersToSign, "digest")
×
240
                }
×
241
        }
242

243
        algos := make([]httpsig.Algorithm, 0)
1✔
244
        switch s.Key.(type) {
1✔
245
        case *rsa.PrivateKey:
1✔
246
                algos = append(algos, httpsig.RSA_SHA256, httpsig.RSA_SHA512)
1✔
247
        case *ecdsa.PrivateKey:
×
248
                algos = append(algos, httpsig.ECDSA_SHA512, httpsig.ECDSA_SHA256)
×
249
        case ed25519.PrivateKey:
×
250
                algos = append(algos, httpsig.ED25519)
×
251
        }
252

253
        // NOTE(marius): The only http-signatures accepted by Mastodon instances is "Signature", not "Authorization"
254
        sig, _, err := httpsig.NewSigner(algos, digestAlgorithm, headers, httpsig.Signature, signatureExpiration)
1✔
255
        if err != nil {
1✔
256
                return err
×
257
        }
×
258
        if err = sig.SignRequest(s.Key, string(keyID), req, bodyBuf.Bytes()); err != nil {
1✔
259
                return err
×
260
        }
×
261
        return nil
1✔
262
}
263

264
var _ http.RoundTripper = new(HTTPSignatureTransport)
265

266
// cloneRequest returns a clone of the provided *http.Request.
267
// The clone is a shallow copy of the struct and its Header map.
268
func cloneRequest(r *http.Request) *http.Request {
2✔
269
        // shallow copy of the struct
2✔
270
        r2 := new(http.Request)
2✔
271
        *r2 = *r
2✔
272
        // deep copy of the Header
2✔
273
        r2.Header = make(http.Header, len(r.Header))
2✔
274
        for k, s := range r.Header {
5✔
275
                r2.Header[k] = append([]string(nil), s...)
3✔
276
        }
3✔
277
        return r2
2✔
278
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc