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

go-ap / client / #108

10 Sep 2026 09:26AM UTC coverage: 76.056% (-0.4%) from 76.471%
#108

push

sourcehut

mariusor
Removing the logFn from signer

1 of 4 new or added lines in 1 file covered. (25.0%)

594 of 781 relevant lines covered (76.06%)

6.28 hits per line

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

61.78
/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
type OptionFn func(transport *Signer)
40

41
func WithNonce(nonceFn func() (string, error)) OptionFn {
42
        return func(h *Signer) {
1 ✔
43
                h.nonceFn = nonceFn
1 ✔
44
        }
1 ✔
45
}
46

47
func WithCoveredComponents(comp ...string) OptionFn {
48
        return func(h *Signer) {
1 ✔
49
                h.coveredComponents = comp
1 ✔
50
        }
1 ✔
51
}
52

53
func WithLogger(l lw.Logger) OptionFn {
54
        return func(h *Signer) {
×
55
                h.l = l
×
56
        }
×
57
}
58

59
func WithAlg(alg KeyEncoding) OptionFn {
60
        return func(h *Signer) {
1 ✔
61
                h.Alg = alg
1 ✔
62
        }
1 ✔
63
}
64

65
func WithActor(act *vocab.Actor, prv crypto.PrivateKey) OptionFn {
66
        return func(h *Signer) {
10 ✔
67
                h.Actor = act
10 ✔
68
                h.Key = prv
10 ✔
69
        }
10 ✔
70
}
71

72
func WithApplicationTag(t string) OptionFn {
73
        return func(h *Signer) {
2 ✔
74
                h.tag = t
2 ✔
75
        }
2 ✔
76
}
77

78
// New initializes the Signer
79
// that might come from the initialization functions.
80
func New(initFns ...OptionFn) *Signer {
81
        h := new(Signer)
4 ✔
82
        h.l = lw.Nil()
4 ✔
83
        for _, fn := range initFns {
4 ✔
84
                fn(h)
5 ✔
85
        }
5 ✔
86
        return h
4 ✔
87
}
88

89
type noncer func() (string, error)
90

91
func (n noncer) GetNonce(_ context.Context) (string, error) {
92
        return n()
1 ✔
93
}
1 ✔
94

95
func validateActorPublicKey(key crypto.PrivateKey, actorPubKey crypto.PublicKey) error {
96
        switch pk := key.(type) {
1 ✔
97
        case *rsa.PrivateKey:
98
                pub := &pk.PublicKey
1 ✔
99
                if !pub.Equal(actorPubKey) {
1 ✔
100
                        return keyMismatchErr(pk, actorPubKey)
×
101
                }
×
102
        case *ecdsa.PrivateKey:
103
                pub, ok := pk.Public().(*ecdsa.PublicKey)
×
104
                if !ok || !pub.Equal(actorPubKey) {
×
105
                        return keyMismatchErr(pk, actorPubKey)
×
106
                }
×
107
        case ed25519.PrivateKey:
108
                pub, _ := pk.Public().(ed25519.PublicKey)
×
109
                if !pub.Equal(actorPubKey) {
×
110
                        return keyMismatchErr(pk, actorPubKey)
×
111
                }
×
112
        }
113
        return nil
1 ✔
114
}
115

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

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

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

192
func (s *Signer) signRequestRFC(coveredComponents []string) func(req *http.Request) error {
193
        return func(req *http.Request) error {
2 ✔
194
                if s.Actor == nil {
2 ✔
195
                        return errors.Newf("unable to sign request, Actor is invalid")
1 ✔
196
                }
1 ✔
197
                if s.Key == nil {
1 ✔
198
                        return errors.Newf("unable to sign request, private key is invalid")
×
199
                }
×
200
                s.l.Tracef("Signing RFC request")
1 ✔
201

1 ✔
202
                pubKey, err := toCryptoPublicKey(s.Actor.PublicKey)
1 ✔
203
                if err != nil {
1 ✔
204
                        return errors.Annotatef(err, "unable to sign request, unable to validate the Actor's public key")
×
205
                }
×
206
                if err = validateActorPublicKey(s.Key, pubKey); err != nil {
1 ✔
207
                        return errors.Annotatef(err, "unable to sign request, Actor public key does not match it's private key")
×
208
                }
×
209

210
                key := rfc.Key{
1 ✔
211
                        KeyID:     string(s.Actor.PublicKey.ID),
1 ✔
212
                        Algorithm: rfcAlgorithmFromPrivateKey(s.Key, s.Alg),
1 ✔
213
                        Key:       s.Key,
1 ✔
214
                }
1 ✔
215

216
                initFns := []rfc.SignerOption{
1 ✔
217
                        rfc.WithTTL(sigValidDuration),
1 ✔
218
                }
1 ✔
219

220
                if s.tag != "" {
1 ✔
221
                        initFns = append(initFns, rfc.WithTag(s.tag))
×
222
                }
×
223

224
                if req.Method == http.MethodPost {
1 ✔
225
                        coveredComponents = append(coveredComponents, "content-digest")
1 ✔
226
                        initFns = append(initFns, rfc.WithContentDigestAlgorithm(rfc.Sha256))
1 ✔
227
                }
1 ✔
228
                if coveredComponents != nil {
1 ✔
229
                        initFns = append(initFns, rfc.WithComponents(coveredComponents...))
1 ✔
230
                }
1 ✔
231
                if s.nonceFn != nil {
1 ✔
232
                        initFns = append(initFns, rfc.WithNonce(s.nonceFn))
1 ✔
233
                }
1 ✔
234
                signer, err := rfc.NewSigner(key, initFns...)
1 ✔
235
                if err != nil {
1 ✔
NEW
236
                        s.l.WithContext(lw.Ctx{"err": err}).Warnf("RFC signer initialization failed")
×
237
                        return err
×
238
                }
×
239
                msg := HTTPSigMsgFromRequest(req)
1 ✔
240
                headersWithSignature, err := signer.Sign(msg)
1 ✔
241
                if err != nil {
1 ✔
NEW
242
                        s.l.WithContext(lw.Ctx{"err": err}).Warnf("RFC signature failed")
×
243
                        return err
×
244
                }
×
245
                req.Header = headersWithSignature
1 ✔
246
                return nil
1 ✔
247
        }
248
}
249

250
type KeyEncoding int
251

252
const (
253
        KeyTypeUnknown KeyEncoding = 0
254
        KeyTypePKCS    KeyEncoding = 1
255
        KeyTypePSS     KeyEncoding = 2
256
)
257

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

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

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

303
func (s *Signer) signRequestDraft(req *http.Request) error {
304
        if s.Actor == nil {
2 ✔
305
                return errors.Newf("unable to sign request, Actor is invalid")
1 ✔
306
        }
1 ✔
307
        if s.Key == nil {
1 ✔
308
                return errors.Newf("unable to sign request, private key is invalid")
×
309
        }
×
310
        if !s.Actor.PublicKey.ID.IsValid() {
1 ✔
311
                return errors.Newf("unable to sign request, invalid Actor public key ID")
×
312
        }
×
313
        s.l.Tracef("Signing draft request")
1 ✔
314

1 ✔
315
        keyID := s.Actor.PublicKey.ID
1 ✔
316

1 ✔
317
        headers := HeadersToSign
1 ✔
318
        bodyBuf := bytes.Buffer{}
1 ✔
319
        if req.Body != nil {
1 ✔
320
                if _, err := io.Copy(&bodyBuf, req.Body); err == nil {
1 ✔
321
                        req.Body = io.NopCloser(&bodyBuf)
1 ✔
322
                        if bodyBuf.Len() > 0 {
1 ✔
323
                                headers = append(HeadersToSign, "digest")
×
324
                        }
×
325
                }
326
        }
327

328
        algo := draftAlgorithmFromPrivateKey(s.Key)
1 ✔
329
        secToExpiration := int64(sigValidDuration.Seconds())
1 ✔
330
        // NOTE(marius): The only http-signatures accepted by Mastodon instances is "Signature", not "Authorization"
1 ✔
331
        sig, _, err := draft.NewSigner([]draft.Algorithm{algo}, draft.DigestSha256, headers, draft.Signature, secToExpiration)
1 ✔
332
        if err != nil {
1 ✔
NEW
333
                s.l.WithContext(lw.Ctx{"err": err}).Warnf("Draft signature failed")
×
334
                return err
×
335
        }
×
336
        return sig.SignRequest(s.Key, string(keyID), req, bodyBuf.Bytes())
1 ✔
337
}
338

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

352
func (s *Signer) SignDraft(req *http.Request) error {
353
        return s.signRequestDraft(req)
2 ✔
354
}
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