Skip to content

Commit 2bae3b7

Browse files
committed
Content-Digest header - improved interface
1 parent cc83875 commit 2bae3b7

File tree

7 files changed

+193
-60
lines changed

7 files changed

+193
-60
lines changed

client.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,14 @@ func verifyClientResponse(req *http.Request, conf ClientConfig, res *http.Respon
8282
return fmt.Errorf("fetchVerifier returned a nil verifier")
8383
}
8484
}
85-
receivedContentDigest := res.Header.Get("Content-Digest")
85+
receivedContentDigest := res.Header.Values("Content-Digest")
8686
if conf.computeDigest &&
87-
res.Body != nil && receivedContentDigest != "" {
87+
res.Body != nil && len(receivedContentDigest) > 0 {
8888
// verify the header even if not explicitly required by verifier field list
89-
digest, err := GenerateContentDigest(&res.Body, conf.digestScheme)
89+
err := ValidateContentDigestHeader(receivedContentDigest, &res.Body, conf.digestSchemesRecv)
9090
if err != nil {
9191
return err
9292
}
93-
if receivedContentDigest != digest {
94-
return fmt.Errorf("bad Content-Digest received")
95-
}
9693
}
9794
err := VerifyResponse(signatureName, *verifier, res)
9895
if err != nil {
@@ -104,11 +101,11 @@ func verifyClientResponse(req *http.Request, conf ClientConfig, res *http.Respon
104101
func signClientRequest(req *http.Request, conf ClientConfig) error {
105102
if conf.computeDigest && conf.signer.fields.hasHeader("Content-Digest") &&
106103
req.Body != nil && req.Header.Get("Content-Digest") == "" {
107-
digest, err := GenerateContentDigest(&req.Body, conf.digestScheme)
104+
header, err := GenerateContentDigestHeader(&req.Body, conf.digestSchemesSend)
108105
if err != nil {
109106
return err
110107
}
111-
req.Header.Add("Content-Digest", digest)
108+
req.Header.Set("Content-Digest", header)
112109
}
113110
sigInput, sig, err := SignRequest(conf.signatureName, *conf.signer, req)
114111
if err != nil {

client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ func TestClient_PostForm(t *testing.T) {
294294
return
295295
}
296296
headers := []string{}
297-
for k, _ := range gotResp.Header {
297+
for k := range gotResp.Header {
298298
headers = append(headers, k)
299299
}
300300
sort.Strings(headers)

config.go

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -166,22 +166,25 @@ func NewVerifyConfig() *VerifyConfig {
166166
type HandlerConfig struct {
167167
reqNotVerified func(w http.ResponseWriter,
168168
r *http.Request, logger *log.Logger, err error)
169-
fetchVerifier func(r *http.Request) (sigName string, verifier *Verifier)
170-
fetchSigner func(res http.Response, r *http.Request) (sigName string, signer *Signer)
171-
logger *log.Logger
172-
digestScheme string
173-
computeDigest bool
169+
fetchVerifier func(r *http.Request) (sigName string, verifier *Verifier)
170+
fetchSigner func(res http.Response, r *http.Request) (sigName string, signer *Signer)
171+
logger *log.Logger
172+
computeDigest bool
173+
digestSchemesSend []string
174+
digestSchemesRecv []string
174175
}
175176

176177
// NewHandlerConfig generates a default configuration. When verification or respectively,
177178
// signing is required, the respective "fetch" callback must be supplied.
178179
func NewHandlerConfig() *HandlerConfig {
179180
return &HandlerConfig{
180-
reqNotVerified: defaultReqNotVerified,
181-
fetchVerifier: nil,
182-
fetchSigner: nil,
183-
logger: log.New(os.Stderr, "httpsign: ", log.LstdFlags|log.Lmsgprefix),
184-
computeDigest: true,
181+
reqNotVerified: defaultReqNotVerified,
182+
fetchVerifier: nil,
183+
fetchSigner: nil,
184+
logger: log.New(os.Stderr, "httpsign: ", log.LstdFlags|log.Lmsgprefix),
185+
computeDigest: true,
186+
digestSchemesSend: []string{DigestSha256},
187+
digestSchemesRecv: []string{DigestSha256, DigestSha512},
185188
}
186189
}
187190

@@ -243,27 +246,40 @@ func (h *HandlerConfig) SetComputeDigest(b bool) *HandlerConfig {
243246
return h
244247
}
245248

246-
// SetDigestScheme defines the scheme (cryptographic hash algorithm) to be used for the message digest.
247-
// It only needs to be set if a Content-Digest header is signed.
248-
func (h *HandlerConfig) SetDigestScheme(s string) *HandlerConfig {
249-
h.digestScheme = s
249+
// SetDigestSchemesSend defines the scheme(s) (cryptographic hash algorithms) to be used to generate the message digest.
250+
// It only needs to be set if a Content-Digest header is signed. Default: DigestSha256
251+
func (h *HandlerConfig) SetDigestSchemesSend(s []string) *HandlerConfig {
252+
h.digestSchemesSend = s
253+
return h
254+
}
255+
256+
// SetDigestSchemesRecv defines the cryptographic algorithms to accept when receiving the
257+
// Content-Digest header. Any recognized algorithm's digest must be correct, but the overall header is valid if at least
258+
// one accepted digest is included. Default: DigestSha256, DigestSha512.
259+
func (h *HandlerConfig) SetDigestSchemesRecv(s []string) *HandlerConfig {
260+
h.digestSchemesRecv = s
250261
return h
251262
}
252263

253264
// ClientConfig contains additional configuration for the HTTP client-side wrapper.
254265
// Signing and verification may either be skipped, independently.
255266
type ClientConfig struct {
256-
signatureName string
257-
signer *Signer
258-
verifier *Verifier
259-
fetchVerifier func(res *http.Response, req *http.Request) (sigName string, verifier *Verifier)
260-
computeDigest bool
261-
digestScheme string
267+
signatureName string
268+
signer *Signer
269+
verifier *Verifier
270+
fetchVerifier func(res *http.Response, req *http.Request) (sigName string, verifier *Verifier)
271+
computeDigest bool
272+
digestSchemesSend []string
273+
digestSchemesRecv []string
262274
}
263275

264276
// NewClientConfig creates a new, default ClientConfig.
265277
func NewClientConfig() *ClientConfig {
266-
return &ClientConfig{computeDigest: true, digestScheme: DigestSha256}
278+
return &ClientConfig{
279+
computeDigest: true,
280+
digestSchemesSend: []string{DigestSha256},
281+
digestSchemesRecv: []string{DigestSha256, DigestSha512},
282+
}
267283
}
268284

269285
// SetSignatureName sets the signature name to be used for signing or verification.
@@ -300,9 +316,17 @@ func (c *ClientConfig) SetComputeDigest(b bool) *ClientConfig {
300316
return c
301317
}
302318

303-
// SetDigestScheme defines the cryptographic algorithm to use for the
319+
// SetDigestSchemesSend defines the cryptographic algorithms to use when generating the
304320
// Content-Digest header. Default: DigestSha256.
305-
func (c *ClientConfig) SetDigestScheme(s string) *ClientConfig {
306-
c.digestScheme = s
321+
func (c *ClientConfig) SetDigestSchemesSend(s []string) *ClientConfig {
322+
c.digestSchemesSend = s
323+
return c
324+
}
325+
326+
// SetDigestSchemesRecv defines the cryptographic algorithms to accept when receiving the
327+
// Content-Digest header. Any recognized algorithm's digest must be correct, but the overall header is valid if at least
328+
// one accepted digest is included. Default: DigestSha256, DigestSha512.
329+
func (c *ClientConfig) SetDigestSchemesRecv(s []string) *ClientConfig {
330+
c.digestSchemesRecv = s
307331
return c
308332
}

digest.go

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"crypto/sha256"
66
"crypto/sha512"
77
"fmt"
8+
"github.com/dunglas/httpsfv"
89
"io"
910
"io/ioutil"
1011
)
@@ -15,26 +16,47 @@ const (
1516
DigestSha512 = "sha-512"
1617
)
1718

18-
// GenerateContentDigest generates a digest of the message body according to the given scheme (DigestSha256 or
19-
// DigestSha512). Side effect: the message body is fully read, and replaced by a static buffer
19+
// GenerateContentDigestHeader generates a digest of the message body according to the given scheme(s)
20+
// (currently supporting DigestSha256 and DigestSha512).
21+
// Side effect: the message body is fully read, and replaced by a static buffer
2022
// containing the body contents.
21-
func GenerateContentDigest(body *io.ReadCloser, scheme string) (string, error) {
23+
func GenerateContentDigestHeader(body *io.ReadCloser, schemes []string) (string, error) {
24+
if len(schemes) == 0 {
25+
return "", fmt.Errorf("received empty list of digest schemes")
26+
}
27+
err := validateSchemes(schemes)
28+
if err != nil {
29+
return "", err
30+
}
31+
buff, err := duplicateBody(body)
32+
if err != nil {
33+
return "", err
34+
}
35+
dict := httpsfv.NewDictionary()
36+
for _, scheme := range schemes {
37+
raw, err := rawDigest(buff.String(), scheme)
38+
if err != nil { // When sending, must recognize all schemes
39+
return "", err
40+
}
41+
i := httpsfv.NewItem(raw)
42+
dict.Add(scheme, httpsfv.Member(i))
43+
}
44+
return httpsfv.Marshal(dict)
45+
}
46+
47+
func duplicateBody(body *io.ReadCloser) (*bytes.Buffer, error) {
2248
buff := &bytes.Buffer{}
2349
if body != nil {
2450
_, err := buff.ReadFrom(*body)
2551
if err != nil {
26-
return "", err
52+
return nil, err
2753
}
2854

29-
defer (*body).Close()
55+
_ = (*body).Close()
3056

3157
*body = ioutil.NopCloser(bytes.NewReader(buff.Bytes()))
3258
}
33-
raw, err := rawDigest(buff.String(), scheme)
34-
if err != nil {
35-
return "", err
36-
}
37-
return scheme + "=" + encodeBytes(raw), nil
59+
return buff, nil
3860
}
3961

4062
func rawDigest(s string, scheme string) ([]byte, error) {
@@ -49,3 +71,64 @@ func rawDigest(s string, scheme string) ([]byte, error) {
4971
return nil, fmt.Errorf("unknown digest scheme")
5072
}
5173
}
74+
75+
func validateSchemes(schemes []string) error {
76+
valid := map[string]bool{DigestSha256: true, DigestSha512: true}
77+
for _, s := range schemes {
78+
if !valid[s] {
79+
return fmt.Errorf("invalid scheme: s")
80+
}
81+
}
82+
return nil
83+
}
84+
85+
func ValidateContentDigestHeader(received []string, body *io.ReadCloser, accepted []string) error {
86+
if len(accepted) == 0 {
87+
return fmt.Errorf("received no digest schemes to accept")
88+
}
89+
err := validateSchemes(accepted)
90+
if err != nil {
91+
return err
92+
}
93+
receivedDict, err := httpsfv.UnmarshalDictionary(received)
94+
if err != nil {
95+
return fmt.Errorf("received Content-Digest header: %w", err)
96+
}
97+
buff, err := duplicateBody(body)
98+
if err != nil {
99+
return err
100+
}
101+
var ok bool
102+
found:
103+
for _, a := range accepted {
104+
for _, r := range receivedDict.Names() {
105+
if a == r {
106+
ok = true
107+
break found
108+
}
109+
}
110+
}
111+
if !ok {
112+
return fmt.Errorf("no acceptable digest scheme found in Content-Digest header")
113+
}
114+
// But regardless of the list of accepted schemes, all included digest values (if recognized) must be correct
115+
for _, scheme := range receivedDict.Names() {
116+
raw, err := rawDigest(buff.String(), scheme)
117+
if err != nil {
118+
continue // unknown schemes are ignored
119+
}
120+
m, _ := receivedDict.Get(scheme)
121+
i, ok := m.(httpsfv.Item)
122+
if !ok {
123+
return fmt.Errorf("received Content-Digest header is malformed")
124+
}
125+
b, ok := i.Value.([]byte)
126+
if !ok {
127+
return fmt.Errorf("non-byte string in received Content-Digest header")
128+
}
129+
if !bytes.Equal(raw, b) {
130+
return fmt.Errorf("digest mismatch for scheme %s", scheme)
131+
}
132+
}
133+
return nil
134+
}

digest_test.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,37 +30,69 @@ Content-Digest: sha-256=:Wqdirjg/u3J688ejbUlApbjECpiUUtIwT8lY/z81Tno=:
3030
var resdigest5 = `HTTP/1.1 206 Partial Content
3131
Content-Type: application/json
3232
Content-Range: bytes 1-7/18
33-
Content-Digest: sha-512=:A8pplr4vsk4xdLkJruCXWp6+i+dy/3pSW5HW5ke1jDWS70Dv6Fstf1jS+XEcLqEVhW3i925IPlf/4tnpnvAQDw==:
33+
Content-Digest: sha-256=:Wqdirjg/u3J688ejbUlApbjECpiUUtIwT8lY/z81Tno=:, sha-512=:A8pplr4vsk4xdLkJruCXWp6+i+dy/3pSW5HW5ke1jDWS70Dv6Fstf1jS+XEcLqEVhW3i925IPlf/4tnpnvAQDw==:
3434
3535
"hello"`
3636

37+
var resdigest6 = `HTTP/1.1 200 OK
38+
Content-Type: application/json
39+
Content-Digest: sha-256=:X47E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
40+
41+
{"hello": "world"}`
42+
3743
func TestMessages(t *testing.T) {
3844
res1 := readResponse(resdigest1)
39-
d, err := GenerateContentDigest(&res1.Body, DigestSha256)
45+
d, err := GenerateContentDigestHeader(&res1.Body, []string{DigestSha256})
4046
assert.NoError(t, err, "should not fail to generate digest")
4147
assert.Equal(t, "sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:", d)
4248
h := res1.Header.Get("Content-Digest")
4349
assert.Equal(t, h, d)
4450

4551
res2 := readResponse(resdigest2)
46-
d, err = GenerateContentDigest(&res2.Body, DigestSha256)
52+
d, err = GenerateContentDigestHeader(&res2.Body, []string{DigestSha256})
4753
assert.NoError(t, err, "should not fail to generate digest")
4854
h = res2.Header.Get("Content-Digest")
4955
assert.Equal(t, h, d)
5056

5157
res3 := readResponse(resdigest3)
52-
d, err = GenerateContentDigest(&res3.Body, DigestSha256)
58+
d, err = GenerateContentDigestHeader(&res3.Body, []string{DigestSha256})
5359
assert.NoError(t, err, "should not fail to generate digest")
5460
h = res3.Header.Get("Content-Digest")
5561
assert.NotEqual(t, h, d)
5662

5763
res4 := readResponse(resdigest3)
58-
d, err = GenerateContentDigest(&res4.Body, "sha-999")
64+
d, err = GenerateContentDigestHeader(&res4.Body, []string{DigestSha256, "sha-999"})
5965
assert.Error(t, err, "bad digest scheme")
6066

6167
res5 := readResponse(resdigest5)
62-
d, err = GenerateContentDigest(&res5.Body, DigestSha512)
68+
d, err = GenerateContentDigestHeader(&res5.Body, []string{DigestSha256, DigestSha512})
6369
assert.NoError(t, err, "should not fail to generate digest")
6470
h = res5.Header.Get("Content-Digest")
6571
assert.Equal(t, h, d)
6672
}
73+
74+
func TestValidateContentDigestHeader(t *testing.T) {
75+
res1 := readResponse(resdigest1)
76+
hdr := res1.Header.Values("Content-Digest")
77+
err := ValidateContentDigestHeader(hdr, &res1.Body, []string{DigestSha256})
78+
assert.NoError(t, err, "should not fail")
79+
80+
err = ValidateContentDigestHeader(hdr, &res1.Body, []string{})
81+
assert.Error(t, err, "empty list of accepted schemes")
82+
83+
err = ValidateContentDigestHeader(hdr, &res1.Body, []string{"kuku"})
84+
assert.Error(t, err, "unknown scheme in list of accepted schemes")
85+
86+
hdr = []string{"123"}
87+
err = ValidateContentDigestHeader(hdr, &res1.Body, []string{DigestSha256})
88+
assert.Error(t, err, "bad received header")
89+
90+
hdr = res1.Header.Values("Content-Digest")
91+
err = ValidateContentDigestHeader(hdr, &res1.Body, []string{DigestSha512})
92+
assert.Error(t, err, "no acceptable scheme")
93+
94+
res6 := readResponse(resdigest6)
95+
hdr = res6.Header.Values("Content-Digest")
96+
err = ValidateContentDigestHeader(hdr, &res6.Body, []string{DigestSha256})
97+
assert.Error(t, err, "digest mismatch")
98+
}

0 commit comments

Comments
 (0)