Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements #29

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 60 additions & 29 deletions canonicalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,49 @@ import (
"time"
)

// message is a minimal representation of an HTTP request or response, containing the values
// Message is a minimal representation of an HTTP request or response, containing the values
// needed to construct a signature.
type message struct {
type Message struct {
Method string
Authority string
URL *nurl.URL
Header http.Header
}

func messageFromRequest(r *http.Request) *message {
var DefaultPorts = map[string]struct{}{
"80": {}, // http
"443": {}, // https
"21": {}, // ftp
}

func MessageFromRequest(r *http.Request) *Message {
hdr := r.Header.Clone()
hdr.Set("Host", r.Host)
return &message{
Method: r.Method,
Authority: r.Host,
return &Message{
Method: r.Method,
// TODO Host header is used only in HTTP/1.1 - for other versions we should be using :authority header
// need to remove default port from Host header: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-19#content-request-authority
Authority: normalizeHostHeader(r.Host),
URL: r.URL,
Header: hdr,
}
}

func normalizeHostHeader(host string) string {
hostParts := strings.Split(host, ":")
if len(hostParts) == 0 || len(hostParts) > 2 {
panic("invalid host header: " + host)
} else if len(hostParts) == 1 {
return host
} else {
if _, isFound := DefaultPorts[hostParts[1]]; isFound {
return hostParts[0]
} else {
return host
}
}
}

func canonicalizeHeader(out io.Writer, name string, hdr http.Header) error {
// XXX: Structured headers are not considered, and they should be :)
v := hdr.Values(name)
Expand Down Expand Up @@ -73,6 +96,13 @@ func canonicalizePath(out io.Writer, path string) error {
return err
}

func canonicalizeRequestTarget(out io.Writer, requestTarget string) error {
// Section 2.2.5 (v19) covers canonicalization of the path.
// Section 2.4 step 2 covers using it as input.
_, err := fmt.Fprintf(out, "\"@request-target\": %s\n", requestTarget)
return err
}

func canonicalizeQuery(out io.Writer, rawQuery string) error {
// Section 2.3.8 covers canonicalization of the query.
// Section 2.4 step 2 covers using it as input.
Expand All @@ -94,12 +124,13 @@ func canonicalizeSignatureParams(out io.Writer, sp *signatureParams) error {
}

type signatureParams struct {
items []string
keyID string
alg string
created time.Time
expires *time.Time
nonce string
items []string
paramsOrder []string
keyID string
alg string
created time.Time
expires *time.Time
nonce string
}

func (sp *signatureParams) canonicalize() string {
Expand All @@ -111,19 +142,19 @@ func (sp *signatureParams) canonicalize() string {

// Items comes first. The params afterwards can be in any order. The order chosen here
// matches what's in the examples in the standard, aiding in testing.

o += fmt.Sprintf(";created=%d", sp.created.Unix())

if sp.keyID != "" {
o += fmt.Sprintf(";keyid=\"%s\"", sp.keyID)
}

if sp.alg != "" {
o += fmt.Sprintf(";alg=\"%s\"", sp.alg)
}

if sp.expires != nil {
o += fmt.Sprintf(";expires=%d", sp.expires.Unix())
for _, param := range sp.paramsOrder {
switch param {
case "created":
o += fmt.Sprintf(";created=%d", sp.created.Unix())
case "expires":
o += fmt.Sprintf(";expires=%d", sp.expires.Unix())
case "keyid":
o += fmt.Sprintf(";keyid=\"%s\"", sp.keyID)
case "alg":
o += fmt.Sprintf(";alg=\"%s\"", sp.alg)
case "nonce":
o += fmt.Sprintf(";nonce=\"%s\"", sp.nonce)
}
}

return o
Expand Down Expand Up @@ -156,10 +187,10 @@ func parseSignatureInput(in string) (*signatureParams, error) {
}

for _, param := range parts[1:] {
paramParts := strings.Split(param, "=")
if len(paramParts) != 2 {
return nil, errMalformedSignatureInput
}
// keyid can be base64 encoded, so it can have = symbols at the end
paramParts := strings.SplitN(param, "=", 2)

sp.paramsOrder = append(sp.paramsOrder, paramParts[0])

// TODO: error when not wrapped in quotes
switch paramParts[0] {
Expand Down
2 changes: 1 addition & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"net/http"
"time"

"github.com/jbowes/httpsig"
"github.com/ynodir/httpsig"
)

const secret = "support-your-local-cat-bonnet-store"
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/jbowes/httpsig
module github.com/ynodir/httpsig

go 1.18
50 changes: 25 additions & 25 deletions httpsig.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ func sliceHas(haystack []string, needle string) bool {
// key ids. You must provide at least one signing option. A signature for every provided key id is
// included on each request. Multiple included signatures allow you to gracefully introduce stronger
// algorithms, rotate keys, etc.
func NewSignTransport(transport http.RoundTripper, opts ...signOption) http.RoundTripper {
s := signer{
keys: map[string]sigHolder{},
func NewSignTransport(transport http.RoundTripper, opts ...SignOption) http.RoundTripper {
s := Signer{
keys: map[string]SigHolder{},
nowFunc: time.Now,
}

Expand Down Expand Up @@ -76,7 +76,7 @@ func NewSignTransport(transport http.RoundTripper, opts ...signOption) http.Roun
// TODO: we could skip setting digest on an empty body if content-length is included in the sig
nr.Header.Set("Digest", calcDigest(b.Bytes()))

msg := messageFromRequest(nr)
msg := MessageFromRequest(nr)
hdr, err := s.Sign(msg)
if err != nil {
return nil, err
Expand All @@ -103,7 +103,7 @@ func (r rt) RoundTrip(req *http.Request) (*http.Response, error) { return r(req)
// Requests with missing signatures, malformed signature headers, expired signatures, or
// invalid signatures are rejected with a `400` response. Only one valid signature is required
// from the known key ids. However, only the first known key id is checked.
func NewVerifyMiddleware(opts ...verifyOption) func(http.Handler) http.Handler {
func NewVerifyMiddleware(opts ...VerifyOption) func(http.Handler) http.Handler {

// TODO: form and multipart support
v := verifier{
Expand All @@ -118,15 +118,15 @@ func NewVerifyMiddleware(opts ...verifyOption) func(http.Handler) http.Handler {
serveErr := func(rw http.ResponseWriter) {
// TODO: better error and custom error handler
rw.Header().Set("Content-Type", "text/plain")
rw.WriteHeader(http.StatusBadRequest)
rw.WriteHeader(http.StatusUnauthorized)

_, _ = rw.Write([]byte("invalid required signature"))
}

return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {

msg := messageFromRequest(r)
msg := MessageFromRequest(r)
err := v.Verify(msg)
if err != nil {
serveErr(rw)
Expand Down Expand Up @@ -161,75 +161,75 @@ func NewVerifyMiddleware(opts ...verifyOption) func(http.Handler) http.Handler {
}
}

type signOption interface {
configureSign(s *signer)
type SignOption interface {
configureSign(s *Signer)
}

type verifyOption interface {
type VerifyOption interface {
configureVerify(v *verifier)
}

type signOrVerifyOption interface {
signOption
verifyOption
type SignOrVerifyOption interface {
SignOption
VerifyOption
}

type optImpl struct {
s func(s *signer)
s func(s *Signer)
v func(v *verifier)
}

func (o *optImpl) configureSign(s *signer) { o.s(s) }
func (o *optImpl) configureSign(s *Signer) { o.s(s) }
func (o *optImpl) configureVerify(v *verifier) { o.v(v) }

// WithHeaders sets the list of headers that will be included in the signature.
// The Digest header is always included (and the digest calculated).
//
// If not provided, the default headers `content-type, content-length, host` are used.
func WithHeaders(hdr ...string) signOption {
func WithHeaders(hdr ...string) SignOption {
// TODO: use this to implement required headers in verify?
return &optImpl{
s: func(s *signer) { s.headers = hdr },
s: func(s *Signer) { s.headers = hdr },
}
}

// WithSignRsaPssSha512 adds signing using `rsa-pss-sha512` with the given private key
// using the given key id.
func WithSignRsaPssSha512(keyID string, pk *rsa.PrivateKey) signOption {
func WithSignRsaPssSha512(keyID string, pk *rsa.PrivateKey) SignOption {
return &optImpl{
s: func(s *signer) { s.keys[keyID] = signRsaPssSha512(pk) },
s: func(s *Signer) { s.keys[keyID] = SignRsaPssSha512(pk) },
}
}

// WithVerifyRsaPssSha512 adds signature verification using `rsa-pss-sha512` with the
// given public key using the given key id.
func WithVerifyRsaPssSha512(keyID string, pk *rsa.PublicKey) verifyOption {
func WithVerifyRsaPssSha512(keyID string, pk *rsa.PublicKey) VerifyOption {
return &optImpl{
v: func(v *verifier) { v.keys[keyID] = verifyRsaPssSha512(pk) },
}
}

// WithSignEcdsaP256Sha256 adds signing using `ecdsa-p256-sha256` with the given private key
// using the given key id.
func WithSignEcdsaP256Sha256(keyID string, pk *ecdsa.PrivateKey) signOption {
func WithSignEcdsaP256Sha256(keyID string, pk *ecdsa.PrivateKey) SignOption {
return &optImpl{
s: func(s *signer) { s.keys[keyID] = signEccP256(pk) },
s: func(s *Signer) { s.keys[keyID] = SignEccP256(pk) },
}
}

// WithVerifyEcdsaP256Sha256 adds signature verification using `ecdsa-p256-sha256` with the
// given public key using the given key id.
func WithVerifyEcdsaP256Sha256(keyID string, pk *ecdsa.PublicKey) verifyOption {
func WithVerifyEcdsaP256Sha256(keyID string, pk *ecdsa.PublicKey) VerifyOption {
return &optImpl{
v: func(v *verifier) { v.keys[keyID] = verifyEccP256(pk) },
}
}

// WithHmacSha256 adds signing or signature verification using `hmac-sha256` with the
// given shared secret using the given key id.
func WithHmacSha256(keyID string, secret []byte) signOrVerifyOption {
func WithHmacSha256(keyID string, secret []byte) SignOrVerifyOption {
return &optImpl{
s: func(s *signer) { s.keys[keyID] = signHmacSha256(secret) },
s: func(s *Signer) { s.keys[keyID] = SignHmacSha256(secret) },
v: func(v *verifier) { v.keys[keyID] = verifyHmacSha256(secret) },
}
}
41 changes: 26 additions & 15 deletions sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,28 @@ type sigImpl struct {
sign func() []byte
}

type sigHolder struct {
type SigHolder struct {
alg string
signer func() sigImpl
}

type signer struct {
type Signer struct {
headers []string
keys map[string]sigHolder
keys map[string]SigHolder

// For testing
nowFunc func() time.Time
}

func (s *signer) Sign(msg *message) (http.Header, error) {
func NewSigner(headers []string, keys map[string]SigHolder) *Signer {
return &Signer{
headers: headers,
keys: keys,
nowFunc: time.Now,
}
}

func (s *Signer) Sign(msg *Message) (http.Header, error) {
var b bytes.Buffer

var items []string
Expand All @@ -61,6 +69,8 @@ func (s *signer) Sign(msg *message) (http.Header, error) {
err = canonicalizeQuery(&b, msg.URL.RawQuery)
case "@authority":
err = canonicalizeAuthority(&b, msg.Authority)
case "@request-target":
err = canonicalizeRequestTarget(&b, msg.URL.RequestURI())
default:
// handle default (header) components
err = canonicalizeHeader(&b, h, msg.Header)
Expand All @@ -80,10 +90,11 @@ func (s *signer) Sign(msg *message) (http.Header, error) {
i := 1 // 1 indexed icky
for k, si := range s.keys {
sp := &signatureParams{
items: items,
keyID: k,
created: now,
alg: si.alg,
items: items,
keyID: k,
created: now,
alg: si.alg,
paramsOrder: []string{"created", "keyid"},
}
sps[fmt.Sprintf("sig%d", i)] = sp.canonicalize()

Expand Down Expand Up @@ -122,8 +133,8 @@ func (s *signer) Sign(msg *message) (http.Header, error) {
return hdr, nil
}

func signRsaPssSha512(pk *rsa.PrivateKey) sigHolder {
return sigHolder{
func SignRsaPssSha512(pk *rsa.PrivateKey) SigHolder {
return SigHolder{
alg: "rsa-pss-sha512",
signer: func() sigImpl {
h := sha256.New()
Expand All @@ -142,8 +153,8 @@ func signRsaPssSha512(pk *rsa.PrivateKey) sigHolder {
}
}

func signEccP256(pk *ecdsa.PrivateKey) sigHolder {
return sigHolder{
func SignEccP256(pk *ecdsa.PrivateKey) SigHolder {
return SigHolder{
alg: "ecdsa-p256-sha256",
signer: func() sigImpl {
h := sha256.New()
Expand All @@ -162,9 +173,9 @@ func signEccP256(pk *ecdsa.PrivateKey) sigHolder {
}
}

func signHmacSha256(secret []byte) sigHolder {
// TODO: add alg description
return sigHolder{
func SignHmacSha256(secret []byte) SigHolder {
return SigHolder{
alg: "hmac-sha256",
signer: func() sigImpl {
h := hmac.New(sha256.New, secret)

Expand Down
Loading