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

go-ap / client / #64

05 Jul 2026 04:06PM UTC coverage: 75.904% (-3.9%) from 79.787%
#64

push

sourcehut

mariusor
Adding placeholder tests for detecting the draft and RFC signature algorithms

9 of 24 new or added lines in 1 file covered. (37.5%)

130 existing lines in 3 files now uncovered.

945 of 1245 relevant lines covered (75.9%)

6.26 hits per line

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

63.04
/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
        "time"
16

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
var (
24
        digestAlgorithm  = draft.DigestSha256
25
        sigValidDuration = time.Hour
26
)
27

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

37
        Alg   KeyEncoding
38
        Key   crypto.PrivateKey
39
        Actor *vocab.Actor
40
}
41

42
type OptionFn func(transport *Signer) error
43

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

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

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

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

10✔
70
                return nil
10✔
71
        }
10✔
72
}
73

74
func WithApplicationTag(t string) OptionFn {
2✔
75
        return func(h *Signer) error {
4✔
76
                h.tag = t
2✔
77
                return nil
2✔
78
        }
2✔
79
}
80

81
// New initializes the Signer
82
// that might come from the initialization functions.
83
func New(initFns ...OptionFn) (*Signer, error) {
4✔
84
        // TODO(marius): we need to add the errors to the return values
4✔
85
        h := new(Signer)
4✔
86
        for _, fn := range initFns {
9✔
87
                if err := fn(h); err != nil {
5✔
UNCOV
88
                        return h, err
×
UNCOV
89
                }
×
90
        }
91
        return h, nil
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✔
UNCOV
105
                        return keyMismatchErr(pk, actorPubKey)
×
UNCOV
106
                }
×
UNCOV
107
        case *ecdsa.PrivateKey:
×
UNCOV
108
                pub, ok := pk.Public().(*ecdsa.PublicKey)
×
UNCOV
109
                if !ok || !pub.Equal(actorPubKey) {
×
UNCOV
110
                        return keyMismatchErr(pk, actorPubKey)
×
UNCOV
111
                }
×
UNCOV
112
        case ed25519.PrivateKey:
×
UNCOV
113
                pub, _ := pk.Public().(ed25519.PublicKey)
×
UNCOV
114
                if !pub.Equal(actorPubKey) {
×
UNCOV
115
                        return keyMismatchErr(pk, actorPubKey)
×
UNCOV
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✔
UNCOV
131
                        case KeyTypePSS:
×
UNCOV
132
                                alg = rfc.RsaPssSha256
×
133
                        case KeyTypePKCS:
1✔
134
                                fallthrough
1✔
135
                        default:
1✔
136
                                alg = rfc.RsaPkcs1v15Sha256
1✔
137
                        }
UNCOV
138
                case 384:
×
UNCOV
139
                        switch typ {
×
UNCOV
140
                        case KeyTypePSS:
×
141
                                alg = rfc.RsaPssSha384
×
142
                        case KeyTypePKCS:
×
UNCOV
143
                                fallthrough
×
UNCOV
144
                        default:
×
UNCOV
145
                                alg = rfc.RsaPkcs1v15Sha384
×
146
                        }
147
                case 512:
×
UNCOV
148
                        switch typ {
×
UNCOV
149
                        case KeyTypePSS:
×
UNCOV
150
                                alg = rfc.RsaPssSha512
×
151
                        case KeyTypePKCS:
×
152
                                fallthrough
×
UNCOV
153
                        default:
×
UNCOV
154
                                alg = rfc.RsaPkcs1v15Sha512
×
155
                        }
156
                }
UNCOV
157
        case *ecdsa.PrivateKey:
×
UNCOV
158
                if p := pk.Params(); p != nil {
×
UNCOV
159
                        switch p.BitSize {
×
UNCOV
160
                        case 128, 256:
×
UNCOV
161
                                alg = rfc.EcdsaP256Sha256
×
UNCOV
162
                        case 384:
×
UNCOV
163
                                alg = rfc.EcdsaP384Sha384
×
UNCOV
164
                        case 512:
×
UNCOV
165
                                alg = rfc.EcdsaP521Sha512
×
166
                        }
167
                }
168
        case ed25519.PrivateKey:
×
UNCOV
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

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

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

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

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

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

233
type KeyEncoding int
234

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

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

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

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

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

297
        keyID := s.Actor.PublicKey.ID
1✔
298

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

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

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

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