Skip to content

Commit

Permalink
feat: Use structured fields and support latest spec
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexanderTar committed Jan 11, 2024
1 parent 9039c6c commit 0d3c544
Show file tree
Hide file tree
Showing 20 changed files with 2,470 additions and 1,792 deletions.
21 changes: 10 additions & 11 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,23 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 1.18
go-version: 1.21
cache-dependency-path: go.sum

- name: Install dependencies
run: go get ./...

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./... -race -coverprofile=coverage.txt -covermode=atomic

- name: Lint
uses: golangci/golangci-lint-action@v3
with:
version: latest

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
env:
Expand All @@ -42,13 +51,3 @@ jobs:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: Release ${{ steps.tag_version.outputs.new_tag }}
body: ${{ steps.tag_version.outputs.changelog }}

golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.5.3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
45 changes: 17 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,29 +79,28 @@ For more usage examples and documentation, see the [godoc refernce][godoc]

## The Big Feature Matrix

This implementation is based on version `06` of [HTTP Message Signatures][msgsig]
(`draft-ietf-htttpbis-message-signatures-05` from 8 June 2021). Digest
computation is based on version `05` of [Digest Headers][dighdr]
(`draft-ietf-httpbis-digest-headers-05` from 13 April 2021).
This implementation is based on version `19` of [HTTP Message Signatures][msgsig]
(`draft-ietf-htttpbis-message-signatures-19` from 26 July 2023). Digest
computation is based on version `13` of [Digest Headers][dighdr]
(`draft-ietf-httpbis-digest-headers-13` from 10 July 2023).

| Feature | | | Notes |
| ------------------------------- | - | - | ---------------------------------------------------------------------- |
| sign requests || | |
| verify requests || | |
| sign responses | | | |
| verify responses | | | |
| add `expires` to signature | || sorely needed |
| sign responses | | | |
| verify responses | | | |
| add `expires` to signature | | | |
| enforce `expires` in verify || | |
| `@method` component || | |
| `@authority` component || | |
| `@scheme` component | || |
| `@target-uri` component | || |
| `@request-target` component | || Semantics changed in draft-06, no longer recommented for use. |
| `@scheme` component || | |
| `@target-uri` component || | |
| `@path` component || | |
| `@query` component || | Encoding handling is missing. |
| `@query-params` component | | | |
| `@status` component | | | |
| request-response binding | | | |
| `@query-params` component | | | |
| `@status` component | | | |
| request-response binding | | | |
| `Accept-Signature` header | || |
| create multiple signatures || | |
| verify from multiple signatures || | |
Expand All @@ -111,22 +110,12 @@ computation is based on version `05` of [Digest Headers][dighdr]
| `ecdsa-p256-sha256` || | |
| `ecdsa-p384-sha384` || | |
| `ed25519` || | |
| custom signature formats | || `eddsa` is not part of the spec, so custom support here would be nice! |
| JSON Web Signatures | || JWS doesn't support any additional algs, but it is part of the spec |
| Signature-Input as trailer | || Trailers can be dropped. accept for verification only. |
| Signature as trailer | || Trailers can be dropped. accept for verification only. |
| request digests || | |
| response digests | || Tricky to support for signature use according to the spec |
| multiple digests | || |
| digest: `sha-256` | || |
| digest: `sha-512` ||| |
| digest: `md5` | || Deprecated in the spec. Unlikely to be supported. |
| digest: `sha` | || Deprecated in the spec. Unlikely to be supported. |
| digest: `unixsum` | || |
| digest: `unixcksum` | || |
| digest: `id-sha-512` | || |
| digest: `id-sha-256` | || |
| custom digest formats | || |
| multiple digests || | |
| digest: `sha-256` || | |
| digest: `sha-512` || | |

## Contributing

Expand All @@ -149,8 +138,8 @@ I would love your help!

<!-- Other links -->
[go]: https://golang.org
[msgsig]: https://datatracker.ietf.org/doc/draft-ietf-httpbis-message-signatures/
[dighdr]: https://datatracker.ietf.org/doc/draft-ietf-httpbis-digest-headers/
[msgsig]: https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-19.html
[dighdr]: https://www.ietf.org/archive/id/draft-ietf-httpbis-digest-headers-13.html
[myblog]: https://repl.ca/modern-webhook-signatures/

[godoc]: https://pkg.go.dev/github.com/offblocks/httpsig
Expand Down
217 changes: 217 additions & 0 deletions base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package httpsig

import (
"encoding/base64"
"errors"
"slices"
"strings"
"time"

"github.com/dunglas/httpsfv"
)

type signatureItem struct {
key httpsfv.Item
value httpsfv.StructuredFieldValue
}

func createSigningParameters(config *SignConfig) *httpsfv.Params {
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#name-signature-parameters

now := time.Now()

params := config.Params
if len(params) == 0 {
params = defaultParams
}

output := httpsfv.NewParams()

if slices.Contains(params, ParamCreated) {
// created is optional but recommended. If created is supplied but is nil, that's an explicit
// instruction to *not* include the created parameter
var created *time.Time
if config.ParamValues != nil && config.ParamValues.Created == nil {
created = nil
} else if config.ParamValues != nil && config.ParamValues.Created != nil {
created = config.ParamValues.Created
} else {
created = &now
}
if created != nil {
output.Add("created", created.Unix())
}
}

if slices.Contains(params, ParamExpires) {
// attempt to obtain an explicit expires time, otherwise create one that is 300 seconds after
// creation. Don't add an expires time if there is no created time
var expires *time.Time
if config.ParamValues != nil && config.ParamValues.Expires != nil && config.ParamValues.Created != nil {
expires = config.ParamValues.Expires
} else if config.ParamValues != nil && config.ParamValues.Created != nil {
exp := now.Add(300 * time.Second)
expires = &exp
}
if expires != nil {
output.Add("expires", expires.Unix())
}
}

if slices.Contains(params, ParamKeyID) {
// attempt to obtain an overriden key id, otherwise use the one supplied by the key
var keyID *string
if config.ParamValues != nil && config.ParamValues.KeyID != nil {
keyID = config.ParamValues.KeyID
} else {
k := config.Key.GetKeyID()
keyID = &k
}
output.Add("keyid", *keyID)
}

if slices.Contains(params, ParamAlg) {
// attempt to obtain an overriden algorithm, otherwise use the one supplied by the key
var alg *Algorithm
if config.ParamValues != nil && config.ParamValues.Alg != nil {
alg = config.ParamValues.Alg
} else {
a := config.Key.GetAlgorithm()
alg = &a
}
output.Add("alg", string(*alg))
}

if slices.Contains(params, ParamNonce) {
// attempt to obtain an explicit nonce, otherwise create one
var n *string
if config.ParamValues != nil && config.ParamValues.Nonce != nil {
n = config.ParamValues.Nonce
} else {
nonce := nonce()
n = &nonce
}
output.Add("nonce", *n)
}

if slices.Contains(params, ParamTag) {
var tag *string
if config.ParamValues != nil {
tag = config.ParamValues.Tag
}
output.Add("tag", *tag)
}

return output
}

func parseParams(params *httpsfv.Params) (*SignatureParameters, error) {
output := SignatureParameters{}

if params == nil {
return nil, errors.New("no parameters provided")
}

for _, k := range params.Names() {
p, _ := params.Get(k)

if k == "created" {
if v, ok := p.(int64); ok {
t := time.Unix(v, 0)
output.Created = &t
} else {
return nil, errors.New("invalid created parameter")
}
} else if k == "expires" {
if v, ok := p.(int64); ok {
t := time.Unix(v, 0)
output.Expires = &t
} else {
return nil, errors.New("invalid expires parameter")
}
} else if k == "nonce" {
if v, ok := p.(string); ok {
output.Nonce = &v
} else {
return nil, errors.New("invalid nonce parameter")
}
} else if k == "alg" {
if v, ok := p.(string); ok {
a := Algorithm(v)
output.Alg = &a
} else {
return nil, errors.New("invalid alg parameter")
}
} else if k == "keyid" {
if v, ok := p.(string); ok {
output.KeyID = &v
} else {
return nil, errors.New("invalid keyid parameter")
}
} else if k == "tag" {
if v, ok := p.(string); ok {
output.Tag = &v
} else {
return nil, errors.New("invalid tag parameter")
}
} else {
return nil, errors.New("unknown parameter")
}
}

return &output, nil
}

func normaliseParams(params *httpsfv.Params) *httpsfv.Params {
if params == nil {
return nil
}

ps := httpsfv.NewParams()

for _, k := range params.Names() {
p, _ := params.Get(k)

if v, ok := p.([]byte); ok {
encoded := base64.StdEncoding.EncodeToString(v)
ps.Add(k, encoded)
} else if v, ok := p.(httpsfv.Token); ok {
ps.Add(k, string(v))
} else {
ps.Add(k, p)
}
}

return ps
}

func createSignatureBase(fields []string, msg *Message) ([]signatureItem, error) {
items := make([]signatureItem, 0)
for _, f := range fields {
field, err := httpsfv.UnmarshalItem([]string{quoteString(f)})
if err != nil {
return nil, err
}

params := normaliseParams(field.Params)
lcName := strings.ToLower(field.Value.(string))

if lcName != "@signature-params" {
var value httpsfv.StructuredFieldValue
if strings.HasPrefix(lcName, "@") {
value, err = canonicaliseComponent(lcName, params, msg)
} else {
value, err = canonicaliseHeader(lcName, params, msg)
}
if err != nil {
return nil, err
}
item := httpsfv.NewItem(field.Value)
item.Params = params

items = append(items, signatureItem{item, value})
}
}

return items, nil
}
Loading

0 comments on commit 0d3c544

Please sign in to comment.