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

go-ap / client / #68

11 Jul 2026 02:26PM UTC coverage: 77.795% (-0.1%) from 77.925%
#68

push

sourcehut

mariusor
Added examples for initializing the client with s2s and c2s credentials

988 of 1270 relevant lines covered (77.8%)

5.98 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
        "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)
43

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

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

56
func WithAlg(alg KeyEncoding) OptionFn {
1✔
57
        return func(h *Signer) {
2✔
58
                h.Alg = alg
1✔
59
        }
1✔
60
}
61

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

69
func WithApplicationTag(t string) OptionFn {
2✔
70
        return func(h *Signer) {
4✔
71
                h.tag = t
2✔
72
        }
2✔
73
}
74

75
// New initializes the Signer
76
// that might come from the initialization functions.
77
func New(initFns ...OptionFn) *Signer {
4✔
78
        h := new(Signer)
4✔
79
        for _, fn := range initFns {
9✔
80
                fn(h)
5✔
81
        }
5✔
82
        return h
4✔
83
}
84

85
type noncer func() (string, error)
86

87
func (n noncer) GetNonce(_ context.Context) (string, error) {
1✔
88
        return n()
1✔
89
}
1✔
90

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

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

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

165
func (s *Signer) signRequestRFC(coveredComponents []string) func(req *http.Request) error {
2✔
166
        return func(req *http.Request) error {
4✔
167
                if s.Actor == nil {
3✔
168
                        return errors.Newf("unable to sign request, Actor is invalid")
1✔
169
                }
1✔
170
                if s.Key == nil {
1✔
171
                        return errors.Newf("unable to sign request, private key is invalid")
×
172
                }
×
173

174
                pubKey, err := toCryptoPublicKey(s.Actor.PublicKey)
1✔
175
                if err != nil {
1✔
176
                        return errors.Annotatef(err, "unable to sign request, unable to validate the Actor's public key")
×
177
                }
×
178
                if err = validateActorPublicKey(s.Key, pubKey); err != nil {
1✔
179
                        return errors.Annotatef(err, "unable to sign request, Actor public key does not match it's private key")
×
180
                }
×
181

182
                key := rfc.Key{
1✔
183
                        KeyID:     string(s.Actor.PublicKey.ID),
1✔
184
                        Algorithm: rfcAlgorithmFromPrivateKey(s.Key, s.Alg),
1✔
185
                        Key:       s.Key,
1✔
186
                }
1✔
187

1✔
188
                initFns := []rfc.SignerOption{
1✔
189
                        rfc.WithTTL(sigValidDuration),
1✔
190
                }
1✔
191

1✔
192
                if s.tag != "" {
1✔
193
                        initFns = append(initFns, rfc.WithTag(s.tag))
×
194
                }
×
195

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

224
type KeyEncoding int
225

226
const (
227
        KeyTypeUnknown KeyEncoding = 0
228
        KeyTypePKCS    KeyEncoding = 1
229
        KeyTypePSS     KeyEncoding = 2
230
)
231

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

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

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

277
func (s *Signer) signRequestDraft(req *http.Request) error {
2✔
278
        if s.Actor == nil {
3✔
279
                return errors.Newf("unable to sign request, Actor is invalid")
1✔
280
        }
1✔
281
        if s.Key == nil {
1✔
282
                return errors.Newf("unable to sign request, private key is invalid")
×
283
        }
×
284
        if !s.Actor.PublicKey.ID.IsValid() {
1✔
285
                return errors.Newf("unable to sign request, invalid Actor public key ID")
×
286
        }
×
287

288
        keyID := s.Actor.PublicKey.ID
1✔
289

1✔
290
        headers := HeadersToSign
1✔
291
        bodyBuf := bytes.Buffer{}
1✔
292
        if req.Body != nil {
2✔
293
                if _, err := io.Copy(&bodyBuf, req.Body); err == nil {
2✔
294
                        req.Body = io.NopCloser(&bodyBuf)
1✔
295
                        if bodyBuf.Len() > 0 {
1✔
296
                                headers = append(HeadersToSign, "digest")
×
297
                        }
×
298
                }
299
        }
300

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

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

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