Skip to content

Commit 4d5d1e1

Browse files
committed
Initial commit
0 parents  commit 4d5d1e1

File tree

7 files changed

+1081
-0
lines changed

7 files changed

+1081
-0
lines changed

canonicalize.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package httpsig
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"net/url"
9+
nurl "net/url"
10+
"strconv"
11+
"strings"
12+
"time"
13+
)
14+
15+
// message is a minimal representation of an HTTP request or response, containing the values
16+
// needed to construct a signature.
17+
type message struct {
18+
Method string
19+
URL *url.URL
20+
Header http.Header
21+
}
22+
23+
func messageFromRequest(r *http.Request) *message {
24+
hdr := r.Header.Clone()
25+
hdr.Set("Host", r.Host)
26+
return &message{
27+
Method: r.Method,
28+
URL: r.URL,
29+
Header: hdr,
30+
}
31+
}
32+
33+
func canonicalizeHeader(out io.Writer, name string, hdr http.Header) error {
34+
// XXX: Structured headers are not considered, and they should be :)
35+
v := hdr.Values(name)
36+
if len(v) == 0 { // empty values are permitted, but no values are not
37+
return fmt.Errorf("'%s' header not found", name)
38+
}
39+
40+
// Section 2.1 covers canonicalizing headers.
41+
// Section 2.4 step 2 covers using them as input.
42+
vc := make([]string, 0, len(v))
43+
for _, sv := range v {
44+
vc = append(vc, strings.TrimSpace(sv))
45+
}
46+
_, err := fmt.Fprintf(out, "\"%s\": %s\n", strings.ToLower(name), strings.Join(vc, ", "))
47+
return err
48+
}
49+
50+
func canonicalizeRequestTarget(out io.Writer, method string, url *nurl.URL) error {
51+
// Section 2.3.1 covers canonicalization the request target.
52+
// Section 2.4 step 2 covers using it as input.
53+
_, err := fmt.Fprintf(out, "\"@request-target\": %s %s\n", strings.ToLower(method), url.RequestURI())
54+
return err
55+
}
56+
57+
func canonicalizeSignatureParams(out io.Writer, sp *signatureParams) error {
58+
// Section 2.3.2 covers canonicalization of the signature parameters
59+
60+
// TODO: Deal with all the potential print errs. sigh.
61+
62+
_, err := fmt.Fprintf(out, "\"@signature-params\": %s", sp.canonicalize())
63+
if err != nil {
64+
return err
65+
}
66+
67+
return err
68+
}
69+
70+
type signatureParams struct {
71+
items []string
72+
keyID string
73+
alg string
74+
created time.Time
75+
expires *time.Time
76+
nonce string
77+
}
78+
79+
func (sp *signatureParams) canonicalize() string {
80+
li := make([]string, 0, len(sp.items))
81+
for _, i := range sp.items {
82+
li = append(li, fmt.Sprintf("\"%s\"", strings.ToLower(i)))
83+
}
84+
o := fmt.Sprintf("(%s)", strings.Join(li, " "))
85+
86+
// Items comes first. The params afterwards can be in any order. The order chosen here
87+
// matches what's in the examples in the standard, aiding in testing.
88+
89+
o += fmt.Sprintf(";created=%d", sp.created.Unix())
90+
91+
if sp.keyID != "" {
92+
o += fmt.Sprintf(";keyid=\"%s\"", sp.keyID)
93+
}
94+
95+
if sp.alg != "" {
96+
o += fmt.Sprintf(";alg=\"%s\"", sp.alg)
97+
}
98+
99+
if sp.expires != nil {
100+
o += fmt.Sprintf(";expires=%d", sp.expires.Unix())
101+
}
102+
103+
return o
104+
}
105+
106+
var malformedSignatureInput = errors.New("malformed signature-input header")
107+
108+
func parseSignatureInput(in string) (*signatureParams, error) {
109+
sp := &signatureParams{}
110+
111+
parts := strings.Split(in, ";")
112+
if len(parts) < 1 {
113+
return nil, malformedSignatureInput
114+
}
115+
116+
if parts[0][0] != '(' || parts[0][len(parts[0])-1] != ')' {
117+
return nil, malformedSignatureInput
118+
}
119+
120+
if len(parts[0]) > 2 { // not empty
121+
// TODO: headers can't have spaces, but it should still be handled
122+
items := strings.Split(parts[0][1:len(parts[0])-1], " ")
123+
124+
// TODO: error when not quoted
125+
for i := range items {
126+
items[i] = strings.Trim(items[i], `"`)
127+
}
128+
129+
sp.items = items
130+
}
131+
132+
for _, param := range parts[1:] {
133+
paramParts := strings.Split(param, "=")
134+
if len(paramParts) != 2 {
135+
return nil, malformedSignatureInput
136+
}
137+
138+
// TODO: error when not wrapped in quotes
139+
switch paramParts[0] {
140+
case "alg":
141+
sp.alg = strings.Trim(paramParts[1], `"`)
142+
case "keyid":
143+
sp.keyID = strings.Trim(paramParts[1], `"`)
144+
case "nonce":
145+
sp.nonce = strings.Trim(paramParts[1], `"`)
146+
case "created":
147+
i, err := strconv.ParseInt(paramParts[1], 10, 64)
148+
if err != nil {
149+
return nil, malformedSignatureInput
150+
}
151+
sp.created = time.Unix(i, 0)
152+
case "expires":
153+
i, err := strconv.ParseInt(paramParts[1], 10, 64)
154+
if err != nil {
155+
return nil, malformedSignatureInput
156+
}
157+
t := time.Unix(i, 0)
158+
sp.expires = &t
159+
default:
160+
// TODO: unknown params could be kept? hard to say.
161+
return nil, malformedSignatureInput
162+
}
163+
}
164+
165+
return sp, nil
166+
}

digest.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package httpsig
2+
3+
import (
4+
"crypto/sha256"
5+
"crypto/subtle"
6+
"encoding/base64"
7+
"fmt"
8+
)
9+
10+
// HTTP digest headers support according to the draft standard
11+
// https://datatracker.ietf.org/doc/draft-ietf-httpbis-digest-headers/
12+
13+
// TODO: support more algorithms, and maybe do its own package.
14+
15+
func calcDigest(in []byte) string {
16+
dig := sha256.Sum256(in)
17+
18+
return fmt.Sprintf("id-sha256=%s", base64.StdEncoding.EncodeToString(dig[:]))
19+
}
20+
21+
func verifyDigest(in []byte, dig string) bool {
22+
// TODO: case insensitity for incoming digest?
23+
calc := calcDigest(in)
24+
25+
return subtle.ConstantTimeCompare([]byte(dig), []byte(calc)) == 0
26+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/jbowes/httpsig
2+
3+
go 1.16

httpsig.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package httpsig
2+
3+
import (
4+
"bytes"
5+
"crypto/ecdsa"
6+
"crypto/rsa"
7+
"io/ioutil"
8+
"net/http"
9+
)
10+
11+
var defaultHeaders = []string{"content-type", "content-length", "host"} // also request path and digest
12+
13+
func sliceHas(haystack []string, needle string) bool {
14+
for _, n := range haystack {
15+
if n == needle {
16+
return true
17+
}
18+
}
19+
20+
return false
21+
}
22+
23+
// NewSignTransport returns a new client transport that wraps the provided transport with
24+
// http message signing and body digest creation
25+
func NewSignTransport(transport http.RoundTripper, opts ...signOption) http.RoundTripper {
26+
s := signer{}
27+
28+
for _, o := range opts {
29+
o.configureSign(&s)
30+
}
31+
32+
if len(s.headers) == 0 {
33+
s.headers = defaultHeaders[:]
34+
}
35+
36+
// TODO: normalize headers? lowercase & de-dupe
37+
38+
// request path first, for aesthetics
39+
if !sliceHas(s.headers, "@request-path") {
40+
s.headers = append([]string{"@request-path"}, s.headers...)
41+
}
42+
43+
if !sliceHas(s.headers, "digest") {
44+
s.headers = append(s.headers, "digest")
45+
}
46+
47+
return rt(func(r *http.Request) (*http.Response, error) {
48+
nr := r.Clone(r.Context())
49+
50+
b := &bytes.Buffer{}
51+
if r.Body != nil {
52+
n, err := b.ReadFrom(r.Body)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
defer r.Body.Close()
58+
59+
if n != 0 {
60+
r.Body = ioutil.NopCloser(bytes.NewReader(b.Bytes()))
61+
}
62+
}
63+
64+
// Always set a digest (for now)
65+
r.Header.Set("Digest", calcDigest(b.Bytes()))
66+
67+
msg := messageFromRequest(nr)
68+
hdr, err := s.Sign(msg)
69+
if err != nil {
70+
return nil, err
71+
}
72+
73+
for k, v := range hdr {
74+
nr.Header[k] = v
75+
}
76+
77+
return transport.RoundTrip(r)
78+
})
79+
}
80+
81+
type rt func(*http.Request) (*http.Response, error)
82+
83+
func (r rt) RoundTrip(req *http.Request) (*http.Response, error) { return r(req) }
84+
85+
// NewVerifyMiddleware returns a configured http server middleware that can be used to wrap
86+
// multiple handlers for http message signature and digest verification.
87+
//
88+
// TODO: form and multipart support
89+
func NewVerifyMiddleware(opts ...verifyOption) func(http.Handler) http.Handler {
90+
91+
v := verifier{}
92+
93+
for _, o := range opts {
94+
o.configureVerify(&v)
95+
}
96+
97+
serveErr := func(rw http.ResponseWriter) {
98+
// TODO: better error and custom error handler
99+
rw.Header().Set("Content-Type", "text/plain")
100+
rw.WriteHeader(http.StatusBadRequest)
101+
102+
rw.Write([]byte("invalid required signature"))
103+
104+
return
105+
}
106+
107+
return func(h http.Handler) http.Handler {
108+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
109+
110+
msg := messageFromRequest(r)
111+
err := v.Verify(msg)
112+
if err != nil {
113+
serveErr(rw)
114+
return
115+
}
116+
117+
b := &bytes.Buffer{}
118+
if r.Body != nil {
119+
n, err := b.ReadFrom(r.Body)
120+
if err != nil {
121+
serveErr(rw)
122+
return
123+
}
124+
125+
defer r.Body.Close()
126+
127+
if n != 0 {
128+
r.Body = ioutil.NopCloser(bytes.NewReader(b.Bytes()))
129+
}
130+
}
131+
132+
// Check the digest if set. We only support id-sha-256 for now.
133+
// TODO: option to require this?
134+
if dig := r.Header.Get("Digest"); dig != "" {
135+
if !verifyDigest(b.Bytes(), dig) {
136+
serveErr(rw)
137+
}
138+
}
139+
140+
h.ServeHTTP(rw, r)
141+
})
142+
}
143+
}
144+
145+
type signOption interface {
146+
configureSign(s *signer)
147+
}
148+
149+
type verifyOption interface {
150+
configureVerify(v *verifier)
151+
}
152+
153+
type signOrVerifyOption interface {
154+
signOption
155+
verifyOption
156+
}
157+
158+
type optImpl struct {
159+
s func(s *signer)
160+
v func(v *verifier)
161+
}
162+
163+
func (o *optImpl) configureSign(s *signer) { o.s(s) }
164+
func (o *optImpl) configureVerify(v *verifier) { o.v(v) }
165+
166+
// TODO: use this to implement required headers in verify?
167+
func WithHeaders(hdr ...string) signOption {
168+
return &optImpl{
169+
s: func(s *signer) { s.headers = hdr },
170+
}
171+
}
172+
173+
func WithSignRsaPssSha512(keyID string, pk *rsa.PrivateKey) signOption {
174+
return &optImpl{
175+
s: func(s *signer) { s.keys[keyID] = signRsaPssSha512(pk) },
176+
}
177+
}
178+
func WithVerifyRsaPssSha512(keyID string, pk *rsa.PublicKey) verifyOption {
179+
return &optImpl{
180+
v: func(v *verifier) { v.keys[keyID] = verifyRsaPssSha512(pk) },
181+
}
182+
}
183+
184+
func WithSignEcdsaP256Sha256(keyID string, pk *ecdsa.PrivateKey) signOption {
185+
return &optImpl{
186+
s: func(s *signer) { s.keys[keyID] = signEccP256(pk) },
187+
}
188+
}
189+
func WithVerifyEcdsaP256Sha256(keyID string, pk *ecdsa.PublicKey) verifyOption {
190+
return &optImpl{
191+
v: func(v *verifier) { v.keys[keyID] = verifyEccP256(pk) },
192+
}
193+
}
194+
195+
func WithHmacSha256(keyID string, secret []byte) signOrVerifyOption {
196+
return &optImpl{
197+
s: func(s *signer) { s.keys[keyID] = signHmacSha256(secret) },
198+
v: func(v *verifier) { v.keys[keyID] = verifyHmacSha256(secret) },
199+
}
200+
}

0 commit comments

Comments
 (0)