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

go-ap / client / #75

23 Jul 2026 11:50AM UTC coverage: 76.66% (-0.2%) from 76.873%
#75

push

sourcehut

mariusor
Add a logger function to the s2s authorization logic

6 of 11 new or added lines in 1 file covered. (54.55%)

716 of 934 relevant lines covered (76.66%)

6.94 hits per line

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

62.55
/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
        rfc "github.com/dadrus/httpsig"
17
        vocab "github.com/go-ap/activitypub"
18
        "github.com/go-ap/errors"
19
        draft "github.com/go-fed/httpsig"
20
)
21

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

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

35
        lFn func(string, ...any)
36
}
37

38
func (s *Signer) logFn(f string, p ...any) {
2✔
39
        if s.lFn == nil {
4✔
40
                return
2✔
41
        }
2✔
NEW
42
        s.lFn(f, p...)
×
43
}
44

45
type OptionFn func(transport *Signer)
46

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

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

NEW
59
func WithLogFn(fn func(string, ...any)) OptionFn {
×
NEW
60
        return func(h *Signer) {
×
NEW
61
                h.lFn = fn
×
NEW
62
        }
×
63
}
64

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

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

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

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

94
type noncer func() (string, error)
95

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

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

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

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

174
func (s *Signer) signRequestRFC(coveredComponents []string) func(req *http.Request) error {
2✔
175
        return func(req *http.Request) error {
4✔
176
                if s.Actor == nil {
3✔
177
                        return errors.Newf("unable to sign request, Actor is invalid")
1✔
178
                }
1✔
179
                if s.Key == nil {
1✔
180
                        return errors.Newf("unable to sign request, private key is invalid")
×
181
                }
×
182
                s.logFn("Signing request: %s", s.Actor.ID)
1✔
183

1✔
184
                pubKey, err := toCryptoPublicKey(s.Actor.PublicKey)
1✔
185
                if err != nil {
1✔
186
                        return errors.Annotatef(err, "unable to sign request, unable to validate the Actor's public key")
×
187
                }
×
188
                if err = validateActorPublicKey(s.Key, pubKey); err != nil {
1✔
189
                        return errors.Annotatef(err, "unable to sign request, Actor public key does not match it's private key")
×
190
                }
×
191

192
                key := rfc.Key{
1✔
193
                        KeyID:     string(s.Actor.PublicKey.ID),
1✔
194
                        Algorithm: rfcAlgorithmFromPrivateKey(s.Key, s.Alg),
1✔
195
                        Key:       s.Key,
1✔
196
                }
1✔
197

1✔
198
                initFns := []rfc.SignerOption{
1✔
199
                        rfc.WithTTL(sigValidDuration),
1✔
200
                }
1✔
201

1✔
202
                if s.tag != "" {
1✔
203
                        initFns = append(initFns, rfc.WithTag(s.tag))
×
204
                }
×
205

206
                if coveredComponents != nil {
2✔
207
                        initFns = append(initFns, rfc.WithComponents(coveredComponents...))
1✔
208
                }
1✔
209
                if req.Method == http.MethodPost {
2✔
210
                        initFns = append(initFns, rfc.WithContentDigestAlgorithm(rfc.Sha256))
1✔
211
                }
1✔
212
                if s.nonceFn != nil {
2✔
213
                        initFns = append(initFns, rfc.WithNonce(s.nonceFn))
1✔
214
                }
1✔
215
                signer, err := rfc.NewSigner(key, initFns...)
1✔
216
                if err != nil {
1✔
217
                        return err
×
218
                }
×
219
                msg := rfc.MessageFromRequest(req)
1✔
220
                // NOTE(marius): for some fetch requests, we have a non empty fragment
1✔
221
                // I'm not clear if this case is handled correctly on the verifier side.
1✔
222
                //if msg.URL.Fragment != "" {
1✔
223
                //        req.URL.Fragment = ""
1✔
224
                //}
1✔
225
                postSignHeaders, err := signer.Sign(msg)
1✔
226
                if err != nil {
1✔
227
                        return err
×
228
                }
×
229
                req.Header = postSignHeaders
1✔
230
                return nil
1✔
231
        }
232
}
233

234
type KeyEncoding int
235

236
const (
237
        KeyTypeUnknown KeyEncoding = 0
238
        KeyTypePKCS    KeyEncoding = 1
239
        KeyTypePSS     KeyEncoding = 2
240
)
241

242
func toCryptoPublicKey(key vocab.PublicKey) (crypto.PublicKey, error) {
1✔
243
        pubBytes, _ := pem.Decode([]byte(key.PublicKeyPem))
1✔
244
        if pubBytes == nil {
1✔
245
                return nil, errors.Newf("unable to decode PEM payload for public key")
×
246
        }
×
247
        pk, _ := x509.ParsePKIXPublicKey(pubBytes.Bytes)
1✔
248
        if pk != nil {
1✔
249
                return pk, nil
×
250
        }
×
251
        pk, err := x509.ParsePKCS1PublicKey(pubBytes.Bytes)
1✔
252
        return pk, err
1✔
253
}
254

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

259
func draftAlgorithmFromPrivateKey(prv crypto.PrivateKey) draft.Algorithm {
2✔
260
        switch pk := prv.(type) {
2✔
261
        case *rsa.PrivateKey:
1✔
262
                switch pk.Size() {
1✔
263
                case 128, 256:
1✔
264
                        return draft.RSA_SHA256
1✔
265
                case 384:
×
266
                        return draft.RSA_SHA384
×
267
                case 512:
×
268
                        return draft.RSA_SHA512
×
269
                }
270
        case *ecdsa.PrivateKey:
×
271
                if p := pk.Params(); p != nil {
×
272
                        switch p.BitSize {
×
273
                        case 128, 256:
×
274
                                return draft.ECDSA_SHA256
×
275
                        case 384:
×
276
                                return draft.ECDSA_SHA384
×
277
                        case 512:
×
278
                                return draft.ECDSA_SHA512
×
279
                        }
280
                }
281
        case ed25519.PrivateKey:
×
282
                return draft.ED25519
×
283
        }
284
        return ""
1✔
285
}
286

287
func (s *Signer) signRequestDraft(req *http.Request) error {
2✔
288
        if s.Actor == nil {
3✔
289
                return errors.Newf("unable to sign request, Actor is invalid")
1✔
290
        }
1✔
291
        if s.Key == nil {
1✔
292
                return errors.Newf("unable to sign request, private key is invalid")
×
293
        }
×
294
        if !s.Actor.PublicKey.ID.IsValid() {
1✔
295
                return errors.Newf("unable to sign request, invalid Actor public key ID")
×
296
        }
×
297
        s.logFn("Signing request: %s", s.Actor.ID)
1✔
298

1✔
299
        keyID := s.Actor.PublicKey.ID
1✔
300

1✔
301
        headers := HeadersToSign
1✔
302
        bodyBuf := bytes.Buffer{}
1✔
303
        if req.Body != nil {
2✔
304
                if _, err := io.Copy(&bodyBuf, req.Body); err == nil {
2✔
305
                        req.Body = io.NopCloser(&bodyBuf)
1✔
306
                        if bodyBuf.Len() > 0 {
1✔
307
                                headers = append(HeadersToSign, "digest")
×
308
                        }
×
309
                }
310
        }
311

312
        algo := draftAlgorithmFromPrivateKey(s.Key)
1✔
313
        secToExpiration := int64(sigValidDuration.Seconds())
1✔
314
        // NOTE(marius): The only http-signatures accepted by Mastodon instances is "Signature", not "Authorization"
1✔
315
        sig, _, err := draft.NewSigner([]draft.Algorithm{algo}, draft.DigestSha256, headers, draft.Signature, secToExpiration)
1✔
316
        if err != nil {
1✔
317
                return err
×
318
        }
×
319
        return sig.SignRequest(s.Key, string(keyID), req, bodyBuf.Bytes())
1✔
320
}
321

322
func (s *Signer) SignRFC9421(req *http.Request) error {
2✔
323
        coveredComponents := s.coveredComponents
2✔
324
        if coveredComponents == nil {
4✔
325
                // NOTE(marius): ideally the caller knows if we're about to sign a Fetch or not,
2✔
326
                // and provide all necessary covered components at initialization time.
2✔
327
                coveredComponents = FetchCoveredComponents
2✔
328
                if !slices.Contains([]string{http.MethodGet, http.MethodHead}, req.Method) {
4✔
329
                        coveredComponents = append(coveredComponents, AdditionalPostCoveredComponents...)
2✔
330
                }
2✔
331
        }
332
        return s.signRequestRFC(coveredComponents)(req)
2✔
333
}
334

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