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

go-ap / client / #103

07 Sep 2026 03:33PM UTC coverage: 76.471% (-0.5%) from 76.923%
#103

push

sourcehut

mariusor
Improve handling msg creation from request

7 of 15 new or added lines in 1 file covered. (46.67%)

39 existing lines in 1 file now uncovered.

598 of 782 relevant lines covered (76.47%)

6.28 hits per line

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

63.54
/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
        "io"
13
        "net/http"
14
        "slices"
15

16
        "git.sr.ht/~mariusor/lw"
17
        rfc "github.com/dadrus/httpsig"
18
        vocab "github.com/go-ap/activitypub"
19
        "github.com/go-ap/errors"
20
        draft "github.com/go-fed/httpsig"
21
)
22

23
type Signer struct {
24
        // RFC9421 relevant data
25
        nonceFn           noncer
26
        coveredComponents []string
27
        // tag is an application-specific tag for the signature as a String value.
28
        // This value is used by applications to help identify signatures relevant for specific applications or protocols.
29
        // See: https://www.rfc-editor.org/rfc/rfc9421.html#section-2.3-4.12
30
        tag string
31

32
        Alg   KeyEncoding
33
        Key   crypto.PrivateKey
34
        Actor *vocab.Actor
35

36
        l lw.Logger
37
}
38

39
func (s *Signer) logFn(f string, p ...any) {
40
        if s.l != nil {
2✔
41
                s.l.Infof(f, p...)
2✔
42
                return
2✔
43
        }
2✔
44
}
45

46
type OptionFn func(transport *Signer)
47

48
func WithNonce(nonceFn func() (string, error)) OptionFn {
49
        return func(h *Signer) {
1✔
50
                h.nonceFn = nonceFn
1✔
51
        }
1✔
52
}
53

54
func WithCoveredComponents(comp ...string) OptionFn {
55
        return func(h *Signer) {
1✔
56
                h.coveredComponents = comp
1✔
57
        }
1✔
58
}
59

60
func WithLogger(l lw.Logger) OptionFn {
61
        return func(h *Signer) {
×
62
                h.l = l
×
UNCOV
63
        }
×
64
}
65

66
func WithAlg(alg KeyEncoding) OptionFn {
67
        return func(h *Signer) {
1✔
68
                h.Alg = alg
1✔
69
        }
1✔
70
}
71

72
func WithActor(act *vocab.Actor, prv crypto.PrivateKey) OptionFn {
73
        return func(h *Signer) {
10✔
74
                h.Actor = act
10✔
75
                h.Key = prv
10✔
76
        }
10✔
77
}
78

79
func WithApplicationTag(t string) OptionFn {
80
        return func(h *Signer) {
2✔
81
                h.tag = t
2✔
82
        }
2✔
83
}
84

85
// New initializes the Signer
86
// that might come from the initialization functions.
87
func New(initFns ...OptionFn) *Signer {
88
        h := new(Signer)
4✔
89
        h.l = lw.Nil()
4✔
90
        for _, fn := range initFns {
4✔
91
                fn(h)
5✔
92
        }
5✔
93
        return h
4✔
94
}
95

96
type noncer func() (string, error)
97

98
func (n noncer) GetNonce(_ context.Context) (string, error) {
99
        return n()
1✔
100
}
1✔
101

102
func validateActorPublicKey(key crypto.PrivateKey, actorPubKey crypto.PublicKey) error {
103
        switch pk := key.(type) {
1✔
104
        case *rsa.PrivateKey:
105
                pub := &pk.PublicKey
1✔
106
                if !pub.Equal(actorPubKey) {
1✔
UNCOV
107
                        return keyMismatchErr(pk, actorPubKey)
×
108
                }
×
109
        case *ecdsa.PrivateKey:
110
                pub, ok := pk.Public().(*ecdsa.PublicKey)
×
111
                if !ok || !pub.Equal(actorPubKey) {
×
UNCOV
112
                        return keyMismatchErr(pk, actorPubKey)
×
113
                }
×
114
        case ed25519.PrivateKey:
115
                pub, _ := pk.Public().(ed25519.PublicKey)
×
116
                if !pub.Equal(actorPubKey) {
×
UNCOV
117
                        return keyMismatchErr(pk, actorPubKey)
×
UNCOV
118
                }
×
119
        }
120
        return nil
1✔
121
}
122

123
func rfcAlgorithmFromPrivateKey(key crypto.PrivateKey, typ KeyEncoding) rfc.SignatureAlgorithm {
124
        // NOTE(marius): I'm not sure what purpose it serves to validate the public key of the actor
125
        // against the private key
126
        var alg rfc.SignatureAlgorithm = ""
2✔
127

2✔
128
        switch pk := key.(type) {
2✔
129
        case *rsa.PrivateKey:
130
                switch pk.Size() {
1✔
131
                case 128, 256:
132
                        switch typ {
1✔
133
                        case KeyTypePSS:
UNCOV
134
                                alg = rfc.RsaPssSha256
×
135
                        case KeyTypePKCS:
136
                                fallthrough
1✔
137
                        default:
138
                                alg = rfc.RsaPkcs1v15Sha256
1✔
139
                        }
140
                case 384:
141
                        switch typ {
×
142
                        case KeyTypePSS:
143
                                alg = rfc.RsaPssSha384
×
144
                        case KeyTypePKCS:
145
                                fallthrough
×
146
                        default:
UNCOV
147
                                alg = rfc.RsaPkcs1v15Sha384
×
148
                        }
149
                case 512:
150
                        switch typ {
×
151
                        case KeyTypePSS:
152
                                alg = rfc.RsaPssSha512
×
153
                        case KeyTypePKCS:
154
                                fallthrough
×
155
                        default:
UNCOV
156
                                alg = rfc.RsaPkcs1v15Sha512
×
157
                        }
158
                }
159
        case *ecdsa.PrivateKey:
UNCOV
160
                if p := pk.Params(); p != nil {
×
161
                        switch p.BitSize {
×
162
                        case 128, 256:
163
                                alg = rfc.EcdsaP256Sha256
×
164
                        case 384:
165
                                alg = rfc.EcdsaP384Sha384
×
166
                        case 512:
UNCOV
167
                                alg = rfc.EcdsaP521Sha512
×
168
                        }
169
                }
170
        case ed25519.PrivateKey:
UNCOV
171
                alg = rfc.Ed25519
×
172
        }
173
        return alg
2✔
174
}
175

176
func HTTPSigMsgFromRequest(req *http.Request) *rfc.Message {
177
        msg := rfc.MessageFromRequest(req)
1✔
178
        // NOTE(marius): on incoming requests the req.URL.Host is empty,
1✔
179
        //  but to match the signature base it needs to be completed with the authority
180
        if msg.URL.Host == "" {
1✔
NEW
181
                msg.URL.Host = msg.Authority
×
NEW
182
        }
×
183
        // NOTE(marius): similarly if the protocol is empty we either hardcoded to https,
184
        //  or we load it from the X-Forwarded-Proto if we're behind proxy.
185
        if msg.URL.Scheme == "" {
1✔
NEW
186
                msg.URL.Scheme = "https"
×
NEW
187
                if proto := msg.Header.Get("X-Forwarded-Proto"); proto != "" {
×
NEW
188
                        msg.URL.Scheme = proto
×
NEW
189
                }
×
190
        }
191
        // NOTE(marius): for some fetch requests, we have a non empty fragment
192
        //  I'm not clear if this case is handled correctly on the verifier side.
193
        if msg.URL.Fragment != "" {
1✔
NEW
194
                req.URL.Fragment = ""
×
NEW
195
        }
×
196
        return msg
1✔
197
}
198

199
func (s *Signer) signRequestRFC(coveredComponents []string) func(req *http.Request) error {
200
        return func(req *http.Request) error {
2✔
201
                if s.Actor == nil {
2✔
202
                        return errors.Newf("unable to sign request, Actor is invalid")
1✔
203
                }
1✔
204
                if s.Key == nil {
1✔
UNCOV
205
                        return errors.Newf("unable to sign request, private key is invalid")
×
UNCOV
206
                }
×
207
                s.logFn("Signing RFC request")
1✔
208

1✔
209
                pubKey, err := toCryptoPublicKey(s.Actor.PublicKey)
1✔
210
                if err != nil {
1✔
UNCOV
211
                        return errors.Annotatef(err, "unable to sign request, unable to validate the Actor's public key")
×
212
                }
×
213
                if err = validateActorPublicKey(s.Key, pubKey); err != nil {
1✔
UNCOV
214
                        return errors.Annotatef(err, "unable to sign request, Actor public key does not match it's private key")
×
UNCOV
215
                }
×
216

217
                key := rfc.Key{
1✔
218
                        KeyID:     string(s.Actor.PublicKey.ID),
1✔
219
                        Algorithm: rfcAlgorithmFromPrivateKey(s.Key, s.Alg),
1✔
220
                        Key:       s.Key,
1✔
221
                }
1✔
222

223
                initFns := []rfc.SignerOption{
1✔
224
                        rfc.WithTTL(sigValidDuration),
1✔
225
                }
1✔
226

227
                if s.tag != "" {
1✔
UNCOV
228
                        initFns = append(initFns, rfc.WithTag(s.tag))
×
UNCOV
229
                }
×
230

231
                if coveredComponents != nil {
1✔
232
                        initFns = append(initFns, rfc.WithComponents(coveredComponents...))
1✔
233
                }
1✔
234
                if req.Method == http.MethodPost {
1✔
235
                        initFns = append(initFns, rfc.WithContentDigestAlgorithm(rfc.Sha256))
1✔
236
                }
1✔
237
                if s.nonceFn != nil {
1✔
238
                        initFns = append(initFns, rfc.WithNonce(s.nonceFn))
1✔
239
                }
1✔
240
                signer, err := rfc.NewSigner(key, initFns...)
1✔
241
                if err != nil {
1✔
UNCOV
242
                        return err
×
UNCOV
243
                }
×
244
                msg := HTTPSigMsgFromRequest(req)
1✔
245
                s.l.WithContext(lw.Ctx{"headers": msg.Header, "authority": msg.Authority, "url": msg.URL.String(), "err": err}).Infof("sign msg")
1✔
246
                postSignHeaders, err := signer.Sign(msg)
1✔
247
                if err != nil {
1✔
UNCOV
248
                        return err
×
UNCOV
249
                }
×
250
                req.Header = postSignHeaders
1✔
251
                return nil
1✔
252
        }
253
}
254

255
type KeyEncoding int
256

257
const (
258
        KeyTypeUnknown KeyEncoding = 0
259
        KeyTypePKCS    KeyEncoding = 1
260
        KeyTypePSS     KeyEncoding = 2
261
)
262

263
func toCryptoPublicKey(key vocab.PublicKey) (crypto.PublicKey, error) {
264
        pubBytes, _ := pem.Decode([]byte(key.PublicKeyPem))
1✔
265
        if pubBytes == nil {
1✔
UNCOV
266
                return nil, errors.Newf("unable to decode PEM payload for public key")
×
267
        }
×
268
        pk, _ := x509.ParsePKIXPublicKey(pubBytes.Bytes)
1✔
269
        if pk != nil {
1✔
UNCOV
270
                return pk, nil
×
UNCOV
271
        }
×
272
        pk, err := x509.ParsePKCS1PublicKey(pubBytes.Bytes)
1✔
273
        return pk, err
1✔
274
}
275

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

280
func draftAlgorithmFromPrivateKey(prv crypto.PrivateKey) draft.Algorithm {
281
        switch pk := prv.(type) {
2✔
282
        case *rsa.PrivateKey:
283
                switch pk.Size() {
1✔
284
                case 128, 256:
285
                        return draft.RSA_SHA256
1✔
286
                case 384:
UNCOV
287
                        return draft.RSA_SHA384
×
288
                case 512:
289
                        return draft.RSA_SHA512
×
290
                }
291
        case *ecdsa.PrivateKey:
292
                if p := pk.Params(); p != nil {
×
UNCOV
293
                        switch p.BitSize {
×
294
                        case 128, 256:
UNCOV
295
                                return draft.ECDSA_SHA256
×
296
                        case 384:
UNCOV
297
                                return draft.ECDSA_SHA384
×
298
                        case 512:
UNCOV
299
                                return draft.ECDSA_SHA512
×
300
                        }
301
                }
302
        case ed25519.PrivateKey:
UNCOV
303
                return draft.ED25519
×
304
        }
305
        return ""
1✔
306
}
307

308
func (s *Signer) signRequestDraft(req *http.Request) error {
309
        if s.Actor == nil {
2✔
310
                return errors.Newf("unable to sign request, Actor is invalid")
1✔
311
        }
1✔
312
        if s.Key == nil {
1✔
313
                return errors.Newf("unable to sign request, private key is invalid")
×
314
        }
×
315
        if !s.Actor.PublicKey.ID.IsValid() {
1✔
UNCOV
316
                return errors.Newf("unable to sign request, invalid Actor public key ID")
×
UNCOV
317
        }
×
318
        s.logFn("Signing draft request")
1✔
319

1✔
320
        keyID := s.Actor.PublicKey.ID
1✔
321

1✔
322
        headers := HeadersToSign
1✔
323
        bodyBuf := bytes.Buffer{}
1✔
324
        if req.Body != nil {
1✔
325
                if _, err := io.Copy(&bodyBuf, req.Body); err == nil {
1✔
326
                        req.Body = io.NopCloser(&bodyBuf)
1✔
327
                        if bodyBuf.Len() > 0 {
1✔
UNCOV
328
                                headers = append(HeadersToSign, "digest")
×
UNCOV
329
                        }
×
330
                }
331
        }
332

333
        algo := draftAlgorithmFromPrivateKey(s.Key)
1✔
334
        secToExpiration := int64(sigValidDuration.Seconds())
1✔
335
        // NOTE(marius): The only http-signatures accepted by Mastodon instances is "Signature", not "Authorization"
1✔
336
        sig, _, err := draft.NewSigner([]draft.Algorithm{algo}, draft.DigestSha256, headers, draft.Signature, secToExpiration)
1✔
337
        if err != nil {
1✔
UNCOV
338
                return err
×
UNCOV
339
        }
×
340
        return sig.SignRequest(s.Key, string(keyID), req, bodyBuf.Bytes())
1✔
341
}
342

343
func (s *Signer) SignRFC9421(req *http.Request) error {
344
        coveredComponents := s.coveredComponents
2✔
345
        if coveredComponents == nil {
2✔
346
                // NOTE(marius): ideally the caller knows if we're about to sign a Fetch or not,
347
                //  and provide all necessary covered components at initialization time.
348
                coveredComponents = FetchCoveredComponents
2✔
349
                if !slices.Contains([]string{http.MethodGet, http.MethodHead}, req.Method) {
2✔
350
                        coveredComponents = append(coveredComponents, AdditionalPostCoveredComponents...)
2✔
351
                }
2✔
352
        }
353
        return s.signRequestRFC(coveredComponents)(req)
2✔
354
}
355

356
func (s *Signer) SignDraft(req *http.Request) error {
357
        return s.signRequestDraft(req)
2✔
358
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc