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

go-ap / client / #129

05 Nov 2026 07:48AM UTC coverage: 76.593% (+0.5%) from 76.056%
#129

push

sourcehut

mariusor
Remove unused interface

589 of 769 relevant lines covered (76.59%)

6.36 hits per line

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

63.13
/s2s/httpsignatures.go
1
package s2s
2

3
import (
4
        "bytes"
5
        "crypto"
6
        "crypto/ecdsa"
7
        "crypto/ed25519"
8
        "crypto/rsa"
9
        "crypto/x509"
10
        "encoding/pem"
11
        "io"
12
        "net/http"
13
        "slices"
14

15
        "git.sr.ht/~mariusor/lw"
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
        Key   crypto.PrivateKey
32
        Actor *vocab.Actor
33

34
        l lw.Logger
35
}
36

37
type OptionFn func(transport *Signer)
38

39
func WithNonce(nonceFn func() string) OptionFn {
40
        return func(h *Signer) {
1 ✔
41
                h.nonceFn = nonceFn
1 ✔
42
        }
1 ✔
43
}
44

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

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

57
func WithActor(act *vocab.Actor, prv crypto.PrivateKey) OptionFn {
58
        return func(h *Signer) {
10 ✔
59
                h.Actor = act
10 ✔
60
                h.Key = prv
10 ✔
61
        }
10 ✔
62
}
63

64
func WithApplicationTag(t string) OptionFn {
65
        return func(h *Signer) {
2 ✔
66
                h.tag = t
2 ✔
67
        }
2 ✔
68
}
69

70
// New initializes the Signer
71
// that might come from the initialization functions.
72
func New(initFns ...OptionFn) *Signer {
73
        h := new(Signer)
4 ✔
74
        h.l = lw.Nil()
4 ✔
75
        for _, fn := range initFns {
4 ✔
76
                fn(h)
4 ✔
77
        }
4 ✔
78
        return h
4 ✔
79
}
80

81
type noncer func() string
82

83
func validateActorPublicKey(key crypto.PrivateKey, actorPubKey crypto.PublicKey) error {
84
        switch pk := key.(type) {
1 ✔
85
        case *rsa.PrivateKey:
86
                pub := &pk.PublicKey
1 ✔
87
                if !pub.Equal(actorPubKey) {
1 ✔
88
                        return keyMismatchErr(pk, actorPubKey)
×
89
                }
×
90
        case *ecdsa.PrivateKey:
91
                pub, ok := pk.Public().(*ecdsa.PublicKey)
×
92
                if !ok || !pub.Equal(actorPubKey) {
×
93
                        return keyMismatchErr(pk, actorPubKey)
×
94
                }
×
95
        case ed25519.PrivateKey:
96
                pub, _ := pk.Public().(ed25519.PublicKey)
×
97
                if !pub.Equal(actorPubKey) {
×
98
                        return keyMismatchErr(pk, actorPubKey)
×
99
                }
×
100
        }
101
        return nil
1 ✔
102
}
103

104
func rfcAlgorithmFromPrivateKey(key crypto.PrivateKey) rfc.SignatureAlgorithm {
105
        // NOTE(marius): I'm not sure what purpose it serves to validate the public key of the actor
106
        // against the private key
107
        switch pk := key.(type) {
2 ✔
108
        case *rsa.PrivateKey:
109
                switch pk.Size() {
1 ✔
110
                case 128, 256:
111
                        return rfc.RsaPkcs1v15Sha256
1 ✔
112
                case 384:
113
                        return rfc.RsaPkcs1v15Sha384
×
114
                case 512:
115
                        return rfc.RsaPkcs1v15Sha512
×
116
                }
117
        case *ecdsa.PrivateKey:
118
                if p := pk.Params(); p != nil {
×
119
                        switch p.BitSize {
×
120
                        case 128, 256:
121
                                return rfc.EcdsaP256Sha256
×
122
                        case 384:
123
                                return rfc.EcdsaP384Sha384
×
124
                        case 512:
125
                                return rfc.EcdsaP521Sha512
×
126
                        }
127
                }
128
        case ed25519.PrivateKey:
129
                return rfc.Ed25519
×
130
        default:
1 ✔
131
        }
132
        return "unknown"
1 ✔
133
}
134

135
func HTTPSigMsgFromRequest(req *http.Request) *rfc.Message {
136
        msg := rfc.MessageFromRequest(req)
1 ✔
137
        // NOTE(marius): on incoming requests the req.URL.Host is empty,
1 ✔
138
        //  but to match the signature base it needs to be completed with the authority
139
        if msg.URL.Host == "" {
1 ✔
140
                msg.URL.Host = msg.Authority
×
141
        }
×
142
        // NOTE(marius): similarly if the protocol is empty we either hardcoded to https,
143
        //  or we load it from the X-Forwarded-Proto if we're behind proxy.
144
        if msg.URL.Scheme == "" {
1 ✔
145
                msg.URL.Scheme = "https"
×
146
                if proto := msg.Header.Get("X-Forwarded-Proto"); proto != "" {
×
147
                        msg.URL.Scheme = proto
×
148
                }
×
149
        }
150
        // NOTE(marius): for some fetch requests, we have a non empty fragment
151
        //  I'm not clear if this case is handled correctly on the verifier side.
152
        if msg.URL.Fragment != "" {
1 ✔
153
                req.URL.Fragment = ""
×
154
        }
×
155
        return msg
1 ✔
156
}
157

158
func (s *Signer) signRequestRFC(coveredComponents []string) func(req *http.Request) error {
159
        return func(req *http.Request) error {
2 ✔
160
                if s.Actor == nil {
2 ✔
161
                        return errors.Newf("unable to sign request, Actor is invalid")
1 ✔
162
                }
1 ✔
163
                if s.Key == nil {
1 ✔
164
                        return errors.Newf("unable to sign request, private key is invalid")
×
165
                }
×
166
                s.l.Tracef("Signing RFC request")
1 ✔
167

1 ✔
168
                pubKey, err := toCryptoPublicKey(s.Actor.PublicKey)
1 ✔
169
                if err != nil {
1 ✔
170
                        return errors.Annotatef(err, "unable to sign request, unable to validate the Actor's public key")
×
171
                }
×
172
                if err = validateActorPublicKey(s.Key, pubKey); err != nil {
1 ✔
173
                        return errors.Annotatef(err, "unable to sign request, Actor public key does not match it's private key")
×
174
                }
×
175

176
                key := rfc.Key{
1 ✔
177
                        Algorithm: rfcAlgorithmFromPrivateKey(s.Key),
1 ✔
178
                        Material:  s.Key,
1 ✔
179
                }
1 ✔
180

181
                initFns := []rfc.SignerOption{
1 ✔
182
                        rfc.WithKeyID(string(s.Actor.PublicKey.ID)),
1 ✔
183
                        rfc.WithAlg(),
1 ✔
184
                        rfc.WithTTL(sigValidDuration),
1 ✔
185
                }
1 ✔
186

187
                if s.tag != "" {
1 ✔
188
                        initFns = append(initFns, rfc.WithTag(s.tag))
×
189
                }
×
190

191
                if req.Method == http.MethodPost {
1 ✔
192
                        coveredComponents = append(coveredComponents, "content-digest")
1 ✔
193
                        initFns = append(initFns, rfc.WithContentDigestAlgorithm(rfc.Sha256))
1 ✔
194
                }
1 ✔
195
                if coveredComponents != nil {
1 ✔
196
                        initFns = append(initFns, rfc.WithComponents(coveredComponents...))
1 ✔
197
                }
1 ✔
198
                signer, err := rfc.NewSigner(key, initFns...)
1 ✔
199
                if err != nil {
1 ✔
200
                        s.l.WithContext(lw.Ctx{"err": err}).Warnf("RFC signer initialization failed")
×
201
                        return err
×
202
                }
×
203
                signFns := make([]rfc.SignOption, 0)
1 ✔
204
                if s.nonceFn != nil {
1 ✔
205
                        if nonceVal := s.nonceFn(); nonceVal != "" {
1 ✔
206
                                signFns = append(signFns, rfc.WithParameter(rfc.Nonce, nonceVal))
1 ✔
207
                        }
1 ✔
208
                }
209
                msg := HTTPSigMsgFromRequest(req)
1 ✔
210
                headersWithSignature, err := signer.Sign(msg, signFns...)
1 ✔
211
                if err != nil {
1 ✔
212
                        s.l.WithContext(lw.Ctx{"err": err}).Warnf("RFC signature failed")
×
213
                        return err
×
214
                }
×
215
                req.Header = headersWithSignature
1 ✔
216
                return nil
1 ✔
217
        }
218
}
219

220
func toCryptoPublicKey(key vocab.PublicKey) (crypto.PublicKey, error) {
221
        pubBytes, _ := pem.Decode([]byte(key.PublicKeyPem))
1 ✔
222
        if pubBytes == nil {
1 ✔
223
                return nil, errors.Newf("unable to decode PEM payload for public key")
×
224
        }
×
225
        pk, _ := x509.ParsePKIXPublicKey(pubBytes.Bytes)
1 ✔
226
        if pk != nil {
1 ✔
227
                return pk, nil
×
228
        }
×
229
        pk, err := x509.ParsePKCS1PublicKey(pubBytes.Bytes)
1 ✔
230
        return pk, err
1 ✔
231
}
232

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

237
func draftAlgorithmFromPrivateKey(prv crypto.PrivateKey) draft.Algorithm {
238
        switch pk := prv.(type) {
2 ✔
239
        case *rsa.PrivateKey:
240
                switch pk.Size() {
1 ✔
241
                case 128, 256:
242
                        return draft.RSA_SHA256
1 ✔
243
                case 384:
244
                        return draft.RSA_SHA384
×
245
                case 512:
246
                        return draft.RSA_SHA512
×
247
                }
248
        case *ecdsa.PrivateKey:
249
                if p := pk.Params(); p != nil {
×
250
                        switch p.BitSize {
×
251
                        case 128, 256:
252
                                return draft.ECDSA_SHA256
×
253
                        case 384:
254
                                return draft.ECDSA_SHA384
×
255
                        case 512:
256
                                return draft.ECDSA_SHA512
×
257
                        }
258
                }
259
        case ed25519.PrivateKey:
260
                return draft.ED25519
×
261
        }
262
        return ""
1 ✔
263
}
264

265
func (s *Signer) signRequestDraft(req *http.Request) error {
266
        if s.Actor == nil {
2 ✔
267
                return errors.Newf("unable to sign request, Actor is invalid")
1 ✔
268
        }
1 ✔
269
        if s.Key == nil {
1 ✔
270
                return errors.Newf("unable to sign request, private key is invalid")
×
271
        }
×
272
        if !s.Actor.PublicKey.ID.IsValid() {
1 ✔
273
                return errors.Newf("unable to sign request, invalid Actor public key ID")
×
274
        }
×
275
        s.l.Tracef("Signing draft request")
1 ✔
276

1 ✔
277
        keyID := s.Actor.PublicKey.ID
1 ✔
278

1 ✔
279
        headers := HeadersToSign
1 ✔
280
        bodyBuf := bytes.Buffer{}
1 ✔
281
        if req.Body != nil {
1 ✔
282
                if _, err := io.Copy(&bodyBuf, req.Body); err == nil {
1 ✔
283
                        req.Body = io.NopCloser(&bodyBuf)
1 ✔
284
                        if bodyBuf.Len() > 0 {
1 ✔
285
                                headers = append(HeadersToSign, "digest")
×
286
                        }
×
287
                }
288
        }
289

290
        algo := draftAlgorithmFromPrivateKey(s.Key)
1 ✔
291
        secToExpiration := int64(sigValidDuration.Seconds())
1 ✔
292
        // NOTE(marius): The only http-signatures accepted by Mastodon instances is "Signature", not "Authorization"
1 ✔
293
        sig, _, err := draft.NewSigner([]draft.Algorithm{algo}, draft.DigestSha256, headers, draft.Signature, secToExpiration)
1 ✔
294
        if err != nil {
1 ✔
295
                s.l.WithContext(lw.Ctx{"err": err}).Warnf("Draft signature failed")
×
296
                return err
×
297
        }
×
298
        return sig.SignRequest(s.Key, string(keyID), req, bodyBuf.Bytes())
1 ✔
299
}
300

301
func (s *Signer) SignRFC9421(req *http.Request) error {
302
        coveredComponents := s.coveredComponents
2 ✔
303
        if coveredComponents == nil {
2 ✔
304
                // NOTE(marius): ideally the caller knows if we're about to sign a Fetch or not,
305
                //  and provide all necessary covered components at initialization time.
306
                coveredComponents = FetchCoveredComponents
2 ✔
307
                if !slices.Contains([]string{http.MethodGet, http.MethodHead}, req.Method) {
2 ✔
308
                        coveredComponents = append(coveredComponents, AdditionalPostCoveredComponents...)
2 ✔
309
                }
2 ✔
310
        }
311
        return s.signRequestRFC(coveredComponents)(req)
2 ✔
312
}
313

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