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

go-ap / client / #70

14 Jul 2026 12:54PM UTC coverage: 77.377% (-0.1%) from 77.519%
#70

push

sourcehut

mariusor
Brought back the exported content types for activities and json-ld

708 of 915 relevant lines covered (77.38%)

7.07 hits per line

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

62.61
/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

36
type OptionFn func(transport *Signer)
37

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

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

50
func WithAlg(alg KeyEncoding) OptionFn {
1✔
51
        return func(h *Signer) {
2✔
52
                h.Alg = alg
1✔
53
        }
1✔
54
}
55

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

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

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

79
type noncer func() (string, error)
80

81
func (n noncer) GetNonce(_ context.Context) (string, error) {
1✔
82
        return n()
1✔
83
}
1✔
84

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

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

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

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

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
                        KeyID:     string(s.Actor.PublicKey.ID),
1✔
178
                        Algorithm: rfcAlgorithmFromPrivateKey(s.Key, s.Alg),
1✔
179
                        Key:       s.Key,
1✔
180
                }
1✔
181

1✔
182
                initFns := []rfc.SignerOption{
1✔
183
                        rfc.WithTTL(sigValidDuration),
1✔
184
                }
1✔
185

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

190
                if coveredComponents != nil {
2✔
191
                        initFns = append(initFns, rfc.WithComponents(coveredComponents...))
1✔
192
                }
1✔
193
                if req.Method == http.MethodPost {
2✔
194
                        initFns = append(initFns, rfc.WithContentDigestAlgorithm(rfc.Sha256))
1✔
195
                }
1✔
196
                if s.nonceFn != nil {
2✔
197
                        initFns = append(initFns, rfc.WithNonce(s.nonceFn))
1✔
198
                }
1✔
199
                signer, err := rfc.NewSigner(key, initFns...)
1✔
200
                if err != nil {
1✔
201
                        return err
×
202
                }
×
203
                msg := rfc.MessageFromRequest(req)
1✔
204
                // NOTE(marius): for some fetch requests, we have a non empty fragment
1✔
205
                // I'm not clear if this case is handled correctly on the verifier side.
1✔
206
                //if msg.URL.Fragment != "" {
1✔
207
                //        req.URL.Fragment = ""
1✔
208
                //}
1✔
209
                postSignHeaders, err := signer.Sign(msg)
1✔
210
                if err != nil {
1✔
211
                        return err
×
212
                }
×
213
                req.Header = postSignHeaders
1✔
214
                return nil
1✔
215
        }
216
}
217

218
type KeyEncoding int
219

220
const (
221
        KeyTypeUnknown KeyEncoding = 0
222
        KeyTypePKCS    KeyEncoding = 1
223
        KeyTypePSS     KeyEncoding = 2
224
)
225

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

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

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

271
func (s *Signer) signRequestDraft(req *http.Request) error {
2✔
272
        if s.Actor == nil {
3✔
273
                return errors.Newf("unable to sign request, Actor is invalid")
1✔
274
        }
1✔
275
        if s.Key == nil {
1✔
276
                return errors.Newf("unable to sign request, private key is invalid")
×
277
        }
×
278
        if !s.Actor.PublicKey.ID.IsValid() {
1✔
279
                return errors.Newf("unable to sign request, invalid Actor public key ID")
×
280
        }
×
281

282
        keyID := s.Actor.PublicKey.ID
1✔
283

1✔
284
        headers := HeadersToSign
1✔
285
        bodyBuf := bytes.Buffer{}
1✔
286
        if req.Body != nil {
2✔
287
                if _, err := io.Copy(&bodyBuf, req.Body); err == nil {
2✔
288
                        req.Body = io.NopCloser(&bodyBuf)
1✔
289
                        if bodyBuf.Len() > 0 {
1✔
290
                                headers = append(HeadersToSign, "digest")
×
291
                        }
×
292
                }
293
        }
294

295
        algo := draftAlgorithmFromPrivateKey(s.Key)
1✔
296
        secToExpiration := int64(sigValidDuration.Seconds())
1✔
297
        // NOTE(marius): The only http-signatures accepted by Mastodon instances is "Signature", not "Authorization"
1✔
298
        sig, _, err := draft.NewSigner([]draft.Algorithm{algo}, draft.DigestSha256, headers, draft.Signature, secToExpiration)
1✔
299
        if err != nil {
1✔
300
                return err
×
301
        }
×
302
        return sig.SignRequest(s.Key, string(keyID), req, bodyBuf.Bytes())
1✔
303
}
304

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

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