Updated httpsig-go library to 1.2.0 and removed vendored version. (#32426)

Fixes #32393 

httpsig-go library has encorporated the changes needed to support TPM,
so we are removing our local version of this library.

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing
- [x] QA'd all new/changed functionality manually

## fleetd/orbit/Fleet Desktop

- [x] Verified compatibility with the latest released version of Fleet
(see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md))
This commit is contained in:
Victor Lyuboslavsky 2025-08-28 14:28:30 -05:00 committed by GitHub
parent 6f768ba6e9
commit 3432d2078d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 20 additions and 4501 deletions

4
go.mod
View file

@ -110,7 +110,7 @@ require (
github.com/pmezard/go-difflib v1.0.0
github.com/prometheus/client_golang v1.21.1
github.com/quasilyte/go-ruleguard/dsl v0.3.22
github.com/remitly-oss/httpsig-go v1.1.3
github.com/remitly-oss/httpsig-go v1.2.0
github.com/rs/zerolog v1.32.0
github.com/russellhaering/goxmldsig v1.4.0
github.com/saferwall/pe v1.5.5
@ -349,5 +349,3 @@ tool (
github.com/kevinburke/go-bindata
github.com/quasilyte/go-ruleguard/dsl
)
replace github.com/remitly-oss/httpsig-go => ./third_party/httpsig-go

2
go.sum
View file

@ -838,6 +838,8 @@ github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe
github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remitly-oss/httpsig-go v1.2.0 h1:rI634TJkh+US3qkWQfkJ7VDJgCvlIbyEepsEw+37W50=
github.com/remitly-oss/httpsig-go v1.2.0/go.mod h1:HYfozYlK9Zv9GYyw+eIuXugk1OV2kjowVrvdv0KQ4XU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=

View file

@ -0,0 +1 @@
* Updated httpsig-go library to 1.2.0 (for host identity certificates and HTTP message signatures).

View file

@ -3,6 +3,7 @@ package fleethttpsig
import (
"crypto"
"crypto/ecdsa"
"github.com/remitly-oss/httpsig-go"
)
@ -28,13 +29,22 @@ func Verifier(kf httpsig.KeyFetcher) (*httpsig.Verifier, error) {
}
// Signer returns a *httpsig.Signer to sign HTTP requests to a Fleet server.
// It handles both regular ECDSA keys and TPM-backed signers.
func Signer(metaKeyID string, signer crypto.Signer, signingAlgorithm httpsig.Algorithm) (*httpsig.Signer, error) {
signingKey := httpsig.SigningKey{
MetaKeyID: metaKeyID,
}
if _, ok := signer.(*ecdsa.PrivateKey); ok {
signingKey.Key = signer
} else {
// TPM or other hardware-backed signer
signingKey.Opts = httpsig.SigningKeyOpts{
Signer: signer,
}
}
return httpsig.NewSigner(httpsig.SigningProfile{
Algorithm: signingAlgorithm,
Fields: requiredFields,
Metadata: requiredMetadata,
}, httpsig.SigningKey{
Key: signer,
MetaKeyID: metaKeyID,
})
}, signingKey)
}

2
third_party/README.md vendored Normal file
View file

@ -0,0 +1,2 @@
This directory is for vendored third party libraries.
See: https://github.com/fleetdm/fleet/blob/main/docs/Contributing/adr/0004-third-party-vendoring.md

View file

@ -1,84 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '42 20 * * 0'
jobs:
analyze:
name: Analyze
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners
# Consider using larger runners for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
# required for all workflows
security-events: write
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
# Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View file

@ -1,14 +0,0 @@
on: [push, pull_request]
name: Test
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Checkout code
uses: actions/checkout@v3
- name: Test
run: go test -v ./...

View file

@ -1 +0,0 @@
testdata/*.key

View file

@ -1,49 +0,0 @@
# To avoid giving your LLM irrelevant context, this .sideignore file can be used
# to exclude any code that isn't part of what is normally edited. The format is
# the same as .gitignore. Note that any patterns listed here are ignored *in
# addition* to to what is ignored via .sideignore.
#
# The following is a list of common vendored dependencies or known paths that
# should not be edited, in case they are not already in a .gitignore file.
# Adjust by:
#
# 1. Removing paths for languages/frameworks not relevant to you
# 2. Add any paths specific to your project that get auto-generated but not
# normally imported, such as mocks etc.
# General vendored dependencies
vendor/
third_party/
extern/
deps/
# Node
node_modules/
# Python virtual environments (less common names)
.venv/
env/
# Go vendored dependencies
vendor/
# Ruby
.bundle/
vendor/bundle/
# PHP
vendor/
# Java / Kotlin
.gradle/
# C / C++
deps/
# Swift / Dart / Flutter
.dart_tool/
Carthage/
# Elixir / Erlang
_build/
deps/

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 remitly-oss
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,65 +0,0 @@
# HTTP Message Signatures
[![Go Reference](https://pkg.go.dev/badge/github.com/remitly-oss/httpsig-go.svg)](https://pkg.go.dev/github.com/remitly-oss/httpsig-go)
[![Go Report Card](https://goreportcard.com/badge/github.com/remitly-oss/httpsig-go)](https://goreportcard.com/report/github.com/remitly-oss/httpsig-go)
An implementation of HTTP Message Signatures from [RFC 9421](https://datatracker.ietf.org/doc/rfc9421/).
HTTP signatures are a mechanism for signing and verifying HTTP requests and responses.
HTTP signatures can be (or will be able to) used for demonstrating proof-of-posession ([DPoP](https://www.rfc-editor.org/rfc/rfc9449.html)) for [OAuth](https://oauth.net/2/dpop/) bearer tokens.
## Supported Features
The full specification is supported with the exception of the following. File a ticket or PR and support will be added
Planned but not currently supported features:
- JWS algorithms
- Header parameters including trailers
## net/http integration
Create net/http clients that sign requests and/or verifies repsonses.
```go
params := httpsig.SigningOptions{
PrivateKey: nil, // Fill in your private key
Algorithm: httpsig.Algo_ECDSA_P256_SHA256,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaKeyID},
MetaKeyID: "key123",
}
// Create the signature signer
signer, _ := httpsig.NewSigner(params)
// Create a net/http Client that signs all requests
signingClient := httpsig.NewHTTPClient(nil, signer, nil)
```
Create net/http Handlers that verify incoming requests to the server.
```go
myhandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Lookup the results of verification
if veriftyResult, ok := httpsig.GetVerifyResult(r.Context()); ok {
keyid, _ := veriftyResult.KeyID()
fmt.Fprintf(w, "Hello, %s", keyid)
} else {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
}
})
// Create a verifier
verifier, _ := httpsig.NewVerifier(nil, httpsig.DefaultVerifyProfile)
mux := http.NewServeMux()
// Wrap the handler with the a signature verification handler.
mux.Handle("/", httpsig.NewHandler(myhandler, verifier))
```
## Stability
The v1.1+ release is stable and production ready.
Please file issues and bugs in the github projects issue tracker.
## References
- [RFC 9421](https://datatracker.ietf.org/doc/rfc9421/)
- [OAuth support](https://oauth.net/http-signatures/)
- [Interactive UI](https://httpsig.org/)

View file

@ -1,42 +0,0 @@
These are instructions for pulling in the latest changes from the upstream version of this library.
The `UPSTREAM_COMMIT` file tracks the upstream version that we last synced with.
_Notes:_
- Update `/path/to/your/monorepo` below to your fleet repo location
- These instructions have not been fully tested.
```bash
export FLEET_REPO=/path/to/your/monorepo
# Clone upstream
git clone https://github.com/remitly-oss/httpsig-go.git ~/httpsig-go-merge
cd ~/httpsig-go-merge
# Check out the last upstream commit we vendored
git checkout $(cat "$FLEET_REPO"/third_party/httpsig-go/UPSTREAM_COMMIT)
# Create a branch for our downstream changes
git checkout -b internal-changes
# Copy current vendored version into this working repo
rsync -a --delete "$FLEET_REPO"/third_party/httpsig-go/ ./ --exclude .git
git add .
git commit -m "Apply downstream changes"
# Fetch upstream updates and merge them
git fetch origin
git checkout main
git merge origin/main
git checkout internal-changes
git merge main # resolve conflicts
# Copy merged result back into monorepo
rsync -a --delete ./ "$FLEET_REPO"/third_party/httpsig-go/ --exclude .git
# Record the new upstream commit. Manually double check that it matches the upstream commit.
git rev-parse origin/main > "$FLEET_REPO"/third_party/httpsig-go/UPSTREAM_COMMIT
# Commit to monorepo
cd "$FLEET_REPO"
git add third_party/httpsig-go
git commit -m "Update httpsig-go with latest upstream changes"
```

View file

@ -1 +0,0 @@
d68e2e99a37987076d8588fafe2aa34147abcc24

View file

@ -1,68 +0,0 @@
package httpsig
import (
"fmt"
sfv "github.com/dunglas/httpsfv"
)
type AcceptSignature struct {
Profile SigningProfile
MetaNonce string // 'nonce'
MetaKeyID string // 'keyid'
MetaTag string // 'tag' - No default. A value must be provided if the parameter is in Metadata.
}
func ParseAcceptSignature(acceptHeader string) (AcceptSignature, error) {
as := AcceptSignature{}
acceptDict, err := sfv.UnmarshalDictionary([]string{acceptHeader})
if err != nil {
return as, newError(ErrInvalidAcceptSignature, "Unable to parse Accept-Signature value", err)
}
profiles := acceptDict.Names()
if len(profiles) == 0 {
return as, newError(ErrMissingAcceptSignature, "No Accept-Signature value")
}
label := profiles[0]
profileItems, _ := acceptDict.Get(label)
profileList, isList := profileItems.(sfv.InnerList)
if !isList {
return as, newError(ErrInvalidAcceptSignature, "Unable to parse Accept-Signature value. Accept-Signature must be a dictionary.")
}
fields := []string{}
for _, componentItem := range profileList.Items {
field, ok := componentItem.Value.(string)
if !ok {
return as, newError(ErrInvalidAcceptSignature, fmt.Sprintf("Invalid signature component '%v', Components must be strings", componentItem.Value))
}
fields = append(fields, field)
}
as.Profile = SigningProfile{
Fields: Fields(fields...),
Label: label,
Metadata: []Metadata{},
}
md := metadataProviderFromParams{profileList.Params}
for _, meta := range profileList.Params.Names() {
as.Profile.Metadata = append(as.Profile.Metadata, Metadata(meta))
switch Metadata(meta) {
case MetaNonce:
as.MetaNonce, _ = md.Nonce()
case MetaAlgorithm:
alg, _ := md.Alg()
as.Profile.Algorithm = Algorithm(alg)
case MetaKeyID:
as.MetaKeyID, _ = md.KeyID()
case MetaTag:
as.MetaTag, _ = md.Tag()
}
}
return as, nil
}

View file

@ -1,64 +0,0 @@
package httpsig
import (
"testing"
"github.com/remitly-oss/httpsig-go/sigtest"
)
func TestAcceptParseSignature(t *testing.T) {
testcases := []struct {
Name string
Desc string
AcceptHeader string
Expected AcceptSignature
ExpectedErrCode ErrCode
}{
{
Name: "FromSpecification",
Desc: "Accept header used in the RFC",
AcceptHeader: `sig1=("@method" "@target-uri" "@authority" "content-digest" "cache-control");keyid="test-key-rsa-pss";created;tag="app-123"`,
Expected: AcceptSignature{
MetaKeyID: "test-key-rsa-pss",
MetaTag: "app-123",
Profile: SigningProfile{
Fields: Fields("@method", "@target-uri", "@authority", "content-digest", "cache-control"),
Metadata: []Metadata{"keyid", "created", "tag"},
Label: "sig1",
},
},
},
{
Name: "InvalidAcceptSig",
AcceptHeader: `("@method" "@target-uri" "@authority" "content-digest" "cache-control");keyid="test-key-rsa-pss";created;tag="app-123"`,
ExpectedErrCode: ErrInvalidAcceptSignature,
},
{
Name: "NoAcceptSig",
AcceptHeader: "",
ExpectedErrCode: ErrMissingAcceptSignature,
},
{
Name: "NotAList",
AcceptHeader: `sig1="@method"`,
ExpectedErrCode: ErrInvalidAcceptSignature,
},
{
Name: "BadComponent",
AcceptHeader: `sig1=("@method" 1 "@authority" "content-digest" "cache-control");keyid="test-key-rsa-pss";created;tag="app-123"`,
ExpectedErrCode: ErrInvalidAcceptSignature,
},
}
for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
actual, err := ParseAcceptSignature(tc.AcceptHeader)
if sigtest.Diff(t, tc.ExpectedErrCode, errCode(err), "Wrong error code") {
t.Logf("%+v\n", err)
return
}
sigtest.Diff(t, tc.Expected, actual, "Wrong signature options")
})
}
}

View file

@ -1,248 +0,0 @@
package httpsig
import (
"context"
"fmt"
"io"
"net/http"
"slices"
"strconv"
"strings"
sfv "github.com/dunglas/httpsfv"
)
// sigBaseInput is the required input to calculate the signature base
type sigBaseInput struct {
Components []componentID
MetadataParams []Metadata // metadata parameters to add to the signature and their values
MetadataValues MetadataProvider
}
type httpMessage struct {
IsResponse bool
Req *http.Request
Resp *http.Response
}
func (hrr httpMessage) Headers() http.Header {
if hrr.IsResponse {
return hrr.Resp.Header
}
return hrr.Req.Header
}
func (hrr httpMessage) Body() io.ReadCloser {
if hrr.IsResponse {
return hrr.Resp.Body
}
return hrr.Req.Body
}
func (hrr httpMessage) SetBody(body io.ReadCloser) {
if hrr.IsResponse {
hrr.Resp.Body = body
return
}
hrr.Req.Body = body
}
func (hrr httpMessage) Context() context.Context {
if hrr.IsResponse {
return context.Background()
}
return hrr.Req.Context()
}
func (hrr httpMessage) isDebug() bool {
if dbgval, ok := hrr.Context().Value(ctxKeyAddDebug).(bool); ok {
return dbgval
}
return false
}
/*
calculateSignatureBase calculates the 'signature base' - the data used as the input to signing or verifying
The signature base is an ASCII string containing the canonicalized HTTP message components covered by the signature.
*/
func calculateSignatureBase(msg httpMessage, bp sigBaseInput) (signatureBase, error) {
signatureParams := sfv.InnerList{
Items: []sfv.Item{},
Params: sfv.NewParams(),
}
componentNames := []string{}
var base strings.Builder
// Add all the required components
for _, component := range bp.Components {
name, err := component.signatureName()
if err != nil {
return signatureBase{}, err
}
if slices.Contains(componentNames, name) {
return signatureBase{}, newError(ErrInvalidSignatureOptions, fmt.Sprintf("Repeated component name not allowed: '%s'", name))
}
componentNames = append(componentNames, name)
signatureParams.Items = append(signatureParams.Items, component.Item)
value, err := component.signatureValue(msg)
if err != nil {
return signatureBase{}, err
}
base.WriteString(fmt.Sprintf("%s: %s\n", name, value))
}
// Add signature metadata parameters
for _, meta := range bp.MetadataParams {
switch meta {
case MetaCreated:
created, err := bp.MetadataValues.Created()
if err != nil {
return signatureBase{}, newError(ErrInvalidMetadata, fmt.Sprintf("Failed to get value for %s metadata parameter", meta), err)
}
signatureParams.Params.Add(string(MetaCreated), created)
case MetaExpires:
expires, err := bp.MetadataValues.Expires()
if err != nil {
return signatureBase{}, newError(ErrInvalidMetadata, fmt.Sprintf("Failed to get value for %s metadata parameter", meta), err)
}
signatureParams.Params.Add(string(MetaExpires), expires)
case MetaNonce:
nonce, err := bp.MetadataValues.Nonce()
if err != nil {
return signatureBase{}, newError(ErrInvalidMetadata, fmt.Sprintf("Failed to get value for %s metadata parameter", meta), err)
}
signatureParams.Params.Add(string(MetaNonce), nonce)
case MetaAlgorithm:
alg, err := bp.MetadataValues.Alg()
if err != nil {
return signatureBase{}, newError(ErrInvalidMetadata, fmt.Sprintf("Failed to get value for %s metadata parameter", meta), err)
}
signatureParams.Params.Add(string(MetaAlgorithm), alg)
case MetaKeyID:
keyID, err := bp.MetadataValues.KeyID()
if err != nil {
return signatureBase{}, newError(ErrInvalidMetadata, fmt.Sprintf("Failed to get value for %s metadata parameter", meta), err)
}
signatureParams.Params.Add(string(MetaKeyID), keyID)
case MetaTag:
tag, err := bp.MetadataValues.Tag()
if err != nil {
return signatureBase{}, newError(ErrInvalidMetadata, fmt.Sprintf("Failed to get value for %s metadata parameter", meta), err)
}
signatureParams.Params.Add(string(MetaTag), tag)
default:
return signatureBase{}, newError(ErrInvalidMetadata, fmt.Sprintf("Invalid metadata field '%s'", meta))
}
}
paramsOut, err := sfv.Marshal(signatureParams)
if err != nil {
return signatureBase{}, fmt.Errorf("Failed to marshal params: %w", err)
}
base.WriteString(fmt.Sprintf("\"%s\": %s", sigparams, paramsOut))
return signatureBase{
base: []byte(base.String()),
signatureInput: paramsOut,
}, nil
}
// componentID is the signature 'component identifier' as detailed in the specification.
type componentID struct {
Name string // canonical, lower case component name. The name is also the value of the Item.
Item sfv.Item // The sfv representation of the component identifier. This contains the name and parameters.
}
// SignatureName is the components serialized name required by the signature.
func (cID componentID) signatureName() (string, error) {
signame, err := sfv.Marshal(cID.Item)
if err != nil {
return "", newError(ErrInvalidComponent, fmt.Sprintf("Unable to serialize component identifier '%s'", cID.Name), err)
}
return signame, nil
}
// signatureValue is the components value required by the signature.
func (cID componentID) signatureValue(msg httpMessage) (string, error) {
val := ""
var err error
if strings.HasPrefix(cID.Name, "@") {
val, err = deriveComponentValue(msg, cID)
if err != nil {
return "", err
}
} else {
values := msg.Headers().Values(cID.Name)
if len(values) == 0 {
return "", newError(ErrInvalidComponent, fmt.Sprintf("Message is missing required component '%s'", cID.Name))
}
// TODO Handle multi value
if len(values) > 1 {
return "", newError(ErrUnsupported, fmt.Sprintf("This library does yet support signatures for components/headers with multiple values: %s", cID.Name))
}
val = msg.Headers().Get(cID.Name)
}
return val, nil
}
func deriveComponentValue(r httpMessage, component componentID) (string, error) {
if r.IsResponse {
return deriveComponentValueResponse(r.Resp, component)
}
return deriveComponentValueRequest(r.Req, component)
}
func deriveComponentValueResponse(resp *http.Response, component componentID) (string, error) {
switch component.Name {
case "@status":
return strconv.Itoa(resp.StatusCode), nil
}
return "", nil
}
func deriveComponentValueRequest(req *http.Request, component componentID) (string, error) {
switch component.Name {
case "@method":
return req.Method, nil
case "@target-uri":
return deriveTargetURI(req), nil
case "@authority":
return req.Host, nil
case "@scheme":
case "@request-target":
case "@path":
return req.URL.Path, nil
case "@query":
return fmt.Sprintf("?%s", req.URL.RawQuery), nil
case "@query-param":
paramKey, found := component.Item.Params.Get("name")
if !found {
return "", newError(ErrInvalidSignatureOptions, fmt.Sprintf("@query-param specified but missing 'name' parameter to indicate which parameter."))
}
paramName, ok := paramKey.(string)
if !ok {
return "", newError(ErrInvalidSignatureOptions, fmt.Sprintf("@query-param specified but the 'name' parameter must be a string to indicate which parameter."))
}
paramValue := req.URL.Query().Get(paramName)
// TODO support empty - is this still a string with a space in it?
if paramValue == "" {
return "", newError(ErrInvalidSignatureOptions, fmt.Sprintf("@query-param '%s' specified but that query param is not in the request", paramName))
}
return paramValue, nil
default:
return "", newError(ErrInvalidSignatureOptions, fmt.Sprintf("Unsupported derived component identifier for a request '%s'", component.Name))
}
return "", nil
}
// deriveTargetURI resolves to an absolute form as required by RFC 9110 and referenced by the http signatures spec.
// The target URI excludes the reference's fragment component, if any, since fragment identifiers are reserved for client-side processing
func deriveTargetURI(req *http.Request) string {
scheme := "https"
if req.TLS == nil {
scheme = "http"
}
return fmt.Sprintf("%s://%s%s%s", scheme, req.Host, req.URL.RawPath, req.URL.RawQuery)
}

View file

@ -1,123 +0,0 @@
package httpsig
import (
"bytes"
"crypto/sha256"
"crypto/sha512"
"fmt"
"io"
"net/http"
sfv "github.com/dunglas/httpsfv"
)
const (
digestAlgoSHA256 = "sha-256"
digestAlgoSHA512 = "sha-512"
)
var (
emptySHA256 = sha256.Sum256([]byte{})
emptySHA512 = sha512.Sum512([]byte{})
)
// digestBody reads the entire body to calculate the digest and returns a new io.ReaderCloser which can be set as the new request body.
type digestInfo struct {
Digest []byte
NewBody io.ReadCloser // NewBody is intended as the http.Request Body replacement. Calculating the digest requires reading the body.
}
func digestBody(digAlgo Digest, body io.ReadCloser) (digestInfo, error) {
var digest []byte
// client GET requests have a nil body
// received/server GET requests have a body but its NoBody
if body == nil || body == http.NoBody {
switch digAlgo {
case DigestSHA256:
digest = emptySHA256[:]
case DigestSHA512:
digest = emptySHA512[:]
default:
return digestInfo{}, newError(ErrNoSigUnsupportedDigest, fmt.Sprintf("Unsupported digest algorithm '%s'", digAlgo))
}
return digestInfo{
Digest: digest,
NewBody: body,
}, nil
}
var buf bytes.Buffer
if _, err := buf.ReadFrom(body); err != nil {
return digestInfo{}, newError(ErrNoSigMessageBody, "Failed to read message body to calculate digest", err)
}
if err := body.Close(); err != nil {
return digestInfo{}, newError(ErrNoSigMessageBody, "Failed to close message body to calculate digest", err)
}
switch digAlgo {
case DigestSHA256:
d := sha256.Sum256(buf.Bytes())
digest = d[:]
case DigestSHA512:
d := sha512.Sum512(buf.Bytes())
digest = d[:]
default:
return digestInfo{}, newError(ErrNoSigUnsupportedDigest, fmt.Sprintf("Unsupported digest algorithm '%s'", digAlgo))
}
return digestInfo{
Digest: digest,
NewBody: io.NopCloser(bytes.NewReader(buf.Bytes())),
}, nil
}
func createDigestHeader(algo Digest, digest []byte) (string, error) {
sfValue := sfv.NewItem(digest)
header := sfv.NewDictionary()
switch algo {
case DigestSHA256:
header.Add(digestAlgoSHA256, sfValue)
case DigestSHA512:
header.Add(digestAlgoSHA512, sfValue)
default:
return "", newError(ErrNoSigUnsupportedDigest, fmt.Sprintf("Unsupported digest algorithm '%s'", algo))
}
value, err := sfv.Marshal(header)
if err != nil {
return "", newError(ErrInternal, "Failed to marshal digest", err)
}
return value, nil
}
// getSupportedDigestFromHeader returns the first supported digest from the supplied header. If no supported header is found a nil digest is returned.
func getSupportedDigestFromHeader(contentDigestHeader []string) (algo Digest, digest []byte, err error) {
digestDict, err := sfv.UnmarshalDictionary(contentDigestHeader)
if err != nil {
return "", nil, newError(ErrNoSigInvalidHeader, "Could not parse Content-Digest header", err)
}
for _, algo := range digestDict.Names() {
switch Digest(algo) {
case DigestSHA256:
fallthrough
case DigestSHA512:
member, ok := digestDict.Get(algo)
if !ok {
continue
}
item, ok := member.(sfv.Item)
if !ok {
// If not a an Item it's not a valid header value. Skip
continue
}
if digest, ok := item.Value.([]byte); ok {
return Digest(algo), digest, nil
}
default:
// Unsupported
continue
}
}
return "", nil, nil
}

View file

@ -1,127 +0,0 @@
package httpsig
import (
"encoding/base64"
"errors"
"io"
"testing"
"github.com/remitly-oss/httpsig-go/sigtest"
)
func TestDigestCreate(t *testing.T) {
testcases := []struct {
Name string
Algo Digest
Body io.ReadCloser
ExpectedDigest string // base64 encoded digest
ExpectedHeader string
ExpectedErrCode ErrCode
}{
{
Name: "sha-256",
Algo: DigestSHA256,
Body: sigtest.MakeBody("hello world"),
ExpectedDigest: "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=",
ExpectedHeader: "sha-256=:uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=:",
},
{
Name: "sha-512",
Algo: DigestSHA512,
Body: sigtest.MakeBody("hello world"),
ExpectedDigest: "MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==",
ExpectedHeader: "sha-512=:MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==:",
},
{
Name: "UnsupportedAlgorithm",
Algo: Digest("nope"),
Body: sigtest.MakeBody("hello world"),
ExpectedErrCode: ErrNoSigUnsupportedDigest,
},
}
for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
actual, err := digestBody(tc.Algo, tc.Body)
if err != nil {
if tc.ExpectedErrCode != "" {
diffErrorCode(t, err, tc.ExpectedErrCode)
return
}
t.Fatal(err)
}
actualEncoded := base64.StdEncoding.EncodeToString(actual.Digest)
sigtest.Diff(t, tc.ExpectedDigest, actualEncoded, "Wrong digest")
actualHeader, err := createDigestHeader(tc.Algo, actual.Digest)
if err != nil {
t.Fatal(err)
}
sigtest.Diff(t, tc.ExpectedHeader, actualHeader, "Wrong digest header")
})
}
}
func TestDigestParse(t *testing.T) {
testcases := []struct {
Name string
Header []string
ExcepctedAlgo Digest
ExpectedDigest string // base64 encoded digest
ExpectedErrCode ErrCode
}{
{
Name: "sha-256",
Header: []string{"sha-256=:uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=:"},
ExcepctedAlgo: DigestSHA256,
ExpectedDigest: "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=",
},
{
Name: "sha-512",
Header: []string{"sha-512=:MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==:"},
ExcepctedAlgo: DigestSHA512,
ExpectedDigest: "MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6TrpzXbw==",
},
{
Name: "Empty",
Header: []string{},
ExpectedDigest: "",
},
{
Name: "BadHeader",
Header: []string{"bl===ah"},
ExpectedErrCode: ErrNoSigInvalidHeader,
},
{
Name: "Unsupported",
Header: []string{"md5=:blah:"},
ExpectedDigest: "",
},
}
for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
actualAlgo, actualDigest, err := getSupportedDigestFromHeader(tc.Header)
if err != nil {
if tc.ExpectedErrCode != "" {
diffErrorCode(t, err, tc.ExpectedErrCode)
return
}
t.Fatal(err)
} else if tc.ExpectedErrCode != "" {
t.Fatal("Expected an err")
}
digestEncoded := base64.StdEncoding.EncodeToString(actualDigest)
sigtest.Diff(t, tc.ExcepctedAlgo, actualAlgo, "Wrong digest algo")
sigtest.Diff(t, tc.ExpectedDigest, digestEncoded, "Wrong digest")
})
}
}
func diffErrorCode(t *testing.T, err error, code ErrCode) bool {
var sigerr *SignatureError
if errors.As(err, &sigerr) {
return sigtest.Diff(t, code, sigerr.Code, "Wrong error code")
}
return false
}

View file

@ -1,104 +0,0 @@
package httpsig_test
import (
"crypto"
"fmt"
"html"
"net/http"
"net/http/httptest"
"github.com/remitly-oss/httpsig-go"
"github.com/remitly-oss/httpsig-go/keyman"
"github.com/remitly-oss/httpsig-go/keyutil"
)
func ExampleSign() {
pkeyEncoded := `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNTK6255ubaaj1i/c
ppuLouTgjAVyHGSxI0pYX8z1e2GhRANCAASkbVuWv1KXXs2H8b0ruFLyv2lKJWtT
BznPJ5sSI1Jn+srosJB/GbEZ3Kg6PcEi+jODF9fdpNEaHGbbGdaVhJi1
-----END PRIVATE KEY-----`
pkey, _ := keyutil.ReadPrivateKey([]byte(pkeyEncoded))
req := httptest.NewRequest("GET", "https://example.com/data", nil)
profile := httpsig.SigningProfile{
Algorithm: httpsig.Algo_ECDSA_P256_SHA256,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaKeyID},
}
skey := httpsig.SigningKey{
Key: pkey,
MetaKeyID: "key123",
}
signer, _ := httpsig.NewSigner(profile, skey)
signer.Sign(req)
}
func ExampleVerify() {
pubkeyEncoded := `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIUctKvU5L/eEYxua5Zlz0HIQJRQq
MTQ7eYQXwqpTvTJkuTffGXKLilT75wY2YZWfybv9flu5d6bCfw+4UB9+cg==
-----END PUBLIC KEY-----`
pubkey, _ := keyutil.ReadPublicKey([]byte(pubkeyEncoded))
req := httptest.NewRequest("GET", "https://example.com/data", nil)
kf := keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"key123": {
KeyID: "key123",
Algo: httpsig.Algo_ECDSA_P256_SHA256,
PubKey: pubkey,
},
})
httpsig.Verify(req, kf, httpsig.DefaultVerifyProfile)
}
func ExampleNewHandler() {
myhandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Lookup the results of verification
if veriftyResult, ok := httpsig.GetVerifyResult(r.Context()); ok {
keyid, _ := veriftyResult.KeyID()
fmt.Fprintf(w, "Hello, %s", html.EscapeString(keyid))
} else {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
}
})
// Create a verifier
verifier, _ := httpsig.NewVerifier(nil, httpsig.DefaultVerifyProfile)
mux := http.NewServeMux()
// Wrap the handler with the a signature verification handler.
mux.Handle("/", httpsig.NewHandler(myhandler, verifier))
}
func ExampleClient() {
profile := httpsig.SigningProfile{
Algorithm: httpsig.Algo_ECDSA_P256_SHA256,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaKeyID},
}
var privateKey crypto.PrivateKey // Get your private key
sk := httpsig.SigningKey{
Key: privateKey,
MetaKeyID: "key123",
}
// Create the signature signer
signer, _ := httpsig.NewSigner(profile, sk)
// Create a net/http Client that signs all requests
signingClient := httpsig.NewHTTPClient(nil, signer, nil)
// This call will be signed.
signingClient.Get("https://example.com")
verifier, _ := httpsig.NewVerifier(nil, httpsig.DefaultVerifyProfile)
// Create a net/http Client that signs and verifies all requests
signVerifyClient := httpsig.NewHTTPClient(nil, signer, verifier)
signVerifyClient.Get("https://example.com")
}

View file

@ -1,200 +0,0 @@
package httpsig
import (
"bufio"
"bytes"
"net/http"
"os"
"testing"
"github.com/remitly-oss/httpsig-go/sigtest"
)
// FuzzSigningOptions fuzzes the basic user input to SigningOptions
func FuzzSigningOptions1(f *testing.F) {
testcases := [][]string{
{"", "", "", ""},
{"", "0", "0", "\xde"},
{"", "\n", "0", "0"},
{"", "", "0", "@"},
{"", "@query-param", "0", "0"},
{string(Algo_ECDSA_P256_SHA256), "@query", "0", "0"},
{"any", "@query", "0", "0"},
{string(Algo_ED25519), "@query", "0", "0"},
}
for _, tc := range testcases {
f.Add(tc[0], tc[1], tc[2], tc[3])
}
reqtxt, err := os.ReadFile("testdata/rfc-test-request.txt")
if err != nil {
f.Fatal(err)
}
f.Fuzz(func(t *testing.T, algo, label, keyID, tag string) {
t.Logf("Label: %s\n", label)
t.Logf("keyid: %s\n", keyID)
t.Logf("tag: %s\n", tag)
fields := Fields(label, keyID, tag)
fields = append(fields, SignedField{
Name: label,
Parameters: map[string]any{
keyID: tag,
},
})
privKey := sigtest.ReadTestPrivateKey(t, "test-key-ed25519.key")
so := SigningProfile{
Algorithm: Algo_ED25519,
Fields: Fields(label, keyID, tag),
Metadata: []Metadata{MetaKeyID, MetaTag},
Label: label,
}
sk := SigningKey{
Key: privKey,
MetaKeyID: keyID,
MetaTag: tag,
}
if so.validate(sk) != nil {
// Catching invalidate signing options is good.
return
}
req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(reqtxt)))
if err != nil {
t.Fatal(err)
}
err = Sign(req, so, sk)
if err != nil {
if _, ok := err.(*SignatureError); ok {
// Handled error
return
}
// Unhandled error
t.Error(err)
}
})
}
func FuzzSigningOptionsFields(f *testing.F) {
testcases := [][]string{
{"", "", ""},
{"0", "0", "\xde"},
{"\n", "0", "0"},
{"", "0", "@"},
{"@query-param", "name", "0"},
{"@query", "0", "0"},
{"@method", "", ""},
{"@status", "", ""},
}
for _, tc := range testcases {
f.Add(tc[0], tc[1], tc[2])
}
reqtxt, err := os.ReadFile("testdata/rfc-test-request.txt")
if err != nil {
f.Fatal(err)
}
f.Fuzz(func(t *testing.T, field, tagName, tagValue string) {
t.Logf("field: %s\n", field)
t.Logf("tag: %s:%s\n", tagName, tagValue)
fields := []SignedField{}
if tagName == "" {
fields = append(fields, SignedField{
Name: field,
})
} else {
fields = append(fields, SignedField{
Name: field,
Parameters: map[string]any{
tagName: tagValue,
},
})
}
so := SigningProfile{
Algorithm: Algo_ED25519,
Fields: fields,
}
sk := SigningKey{
Key: sigtest.ReadTestPrivateKey(t, "test-key-ed25519.key"),
}
if so.validate(sk) != nil {
// Catching invalidate signing options is good.
return
}
req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(reqtxt)))
if err != nil {
t.Fatal(err)
}
err = Sign(req, so, sk)
if err != nil {
if _, ok := err.(*SignatureError); ok {
// Handled error
return
}
// Unhandled error
t.Error(err)
}
})
}
func FuzzExtractSignatures(f *testing.F) {
testcases := []struct {
SignatureHeader string
SignatureInputHeader string
}{
{
SignatureHeader: "",
SignatureInputHeader: "",
},
{
SignatureHeader: "sig-b24=(\"@status\" \"content-type\" \"content-digest\" \"content-length\");created=1618884473;keyid=\"test-key-ecc-p256\"",
SignatureInputHeader: "sig-b24=:wNmSUAhwb5LxtOtOpNa6W5xj067m5hFrj0XQ4fvpaCLx0NKocgPquLgyahnzDnDAUy5eCdlYUEkLIj+32oiasw==:",
},
}
for _, tc := range testcases {
f.Add(tc.SignatureHeader, tc.SignatureInputHeader)
}
reqtxt, err := os.ReadFile("testdata/rfc-test-request.txt")
if err != nil {
f.Fatal(err)
}
f.Fuzz(func(t *testing.T, sigHeader, sigInputHeader string) {
t.Logf("signature header: %s\n", sigHeader)
t.Logf("signature input header: %s\n", sigInputHeader)
req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(reqtxt)))
if err != nil {
t.Fatal(err)
}
req.Header.Set("signature", sigHeader)
req.Header.Set("signature-input", sigInputHeader)
sigSFV, err := parseSignaturesFromRequest(req.Header)
if err != nil {
return
}
for _, label := range sigSFV.Sigs.Names() {
_, err = unmarshalSignature(sigSFV, label)
if err != nil {
if _, ok := err.(*SignatureError); ok {
// Handled error
return
}
// Unhandled error
t.Error(err)
}
}
})
}

View file

@ -1,8 +0,0 @@
module github.com/remitly-oss/httpsig-go
go 1.21.4
require (
github.com/dunglas/httpsfv v1.0.2
github.com/google/go-cmp v0.7.0
)

View file

@ -1,4 +0,0 @@
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=

View file

@ -1,102 +0,0 @@
package httpsig
import (
"context"
"net/http"
)
type contextKey int
const verifyKey contextKey = 0
// NewSigningHTTPClient creates an *http.Client that signs requests before sending. If hc is nil a new *http.Client is created. If signer is not nil all requests will be signed. If verifier is not nil all requests will be verified.
func NewHTTPClient(hc *http.Client, signer *Signer, verifier *Verifier) *http.Client {
if hc == nil {
hc = &http.Client{}
}
hc.Transport = NewTransport(hc.Transport, signer, verifier)
return hc
}
// NewTransport returns an http.RoundTripper implementation that signs requests and verifies responses if signer and verifier are not nil. If rt is nil http.DefaultTransport is used.
func NewTransport(rt http.RoundTripper, signer *Signer, verifier *Verifier) http.RoundTripper {
if rt == nil {
rt = http.DefaultTransport
}
return &transport{
sign: signer != nil,
signer: signer,
verify: verifier != nil,
verifier: verifier,
rt: rt,
}
}
// VerifyHandler verifies the http signature of each request. If not verified it returns a 401 Unauthorized HTTP error. If verified it puts the verification result in the requests context. Use GetVerifyResult to read the context.
type VerifyHandler struct {
handler http.Handler
verifier *Verifier
}
// NewHandler wraps an http.Handler with a an http.Handler that verifies each request. verifier cannot be nil.
func NewHandler(h http.Handler, verifier *Verifier) http.Handler {
if verifier == nil {
panic("verifier cannot be nil")
}
return &VerifyHandler{
handler: h,
verifier: verifier,
}
}
func (vh VerifyHandler) ServeHTTP(rw http.ResponseWriter, inReq *http.Request) {
vr, err := vh.verifier.Verify(inReq)
if err != nil {
// Failed to verify
rw.Write([]byte("Unauthorized"))
rw.WriteHeader(http.StatusUnauthorized)
return
}
req := inReq.WithContext(context.WithValue(inReq.Context(), verifyKey, &vr))
vh.handler.ServeHTTP(rw, req)
}
// GetVerifyResult returns the results of a successful request signature verification.
func GetVerifyResult(ctx context.Context) (v VerifyResult, found bool) {
if vr, ok := ctx.Value(verifyKey).(*VerifyResult); ok && vr != nil {
return *vr, true
}
return VerifyResult{}, false
}
// transport implements http.RoundTripper interface. It signs the request before calling the underlying RoundTripper
type transport struct {
sign bool
verify bool
signer *Signer
verifier *Verifier
rt http.RoundTripper
}
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
if t.sign {
// Signing does not read or close the body
err := t.signer.Sign(req)
if err != nil {
return nil, err
}
}
resp, err := t.rt.RoundTrip(req)
if err != nil {
return resp, err
}
if t.verify {
// Verifying does not read or close the response body
_, err = t.verifier.VerifyResponse(resp)
}
return resp, err
}

View file

@ -1,34 +0,0 @@
// keyman provides key management functionality
package keyman
import (
"context"
"fmt"
"net/http"
"github.com/remitly-oss/httpsig-go"
)
// KeyFetchInMemory implements KeyFetcher for public keys stored in memory.
type KeyFetchInMemory struct {
pubkeys map[string]httpsig.KeySpec
}
func NewKeyFetchInMemory(pubkeys map[string]httpsig.KeySpec) *KeyFetchInMemory {
if pubkeys == nil {
pubkeys = map[string]httpsig.KeySpec{}
}
return &KeyFetchInMemory{pubkeys}
}
func (kf *KeyFetchInMemory) FetchByKeyID(ctx context.Context, rh http.Header, keyID string) (httpsig.KeySpecer, error) {
ks, found := kf.pubkeys[keyID]
if !found {
return nil, fmt.Errorf("Key for keyid '%s' not found", keyID)
}
return ks, nil
}
func (kf *KeyFetchInMemory) Fetch(context.Context, http.Header, httpsig.MetadataProvider) (httpsig.KeySpecer, error) {
return nil, fmt.Errorf("Fetch without keyid not supported")
}

View file

@ -1,284 +0,0 @@
package keyutil
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"os"
)
const (
KeyTypeEC = "EC"
KeyTypeOct = "oct"
)
func ReadJWKFile(jwkFile string) (JWK, error) {
keyBytes, err := os.ReadFile(jwkFile)
if err != nil {
return JWK{}, fmt.Errorf("Failed to read jwk key file '%s': %w", jwkFile, err)
}
return ReadJWK(keyBytes)
}
func ReadJWK(jwkBytes []byte) (JWK, error) {
base := jwk{}
err := json.Unmarshal(jwkBytes, &base)
if err != nil {
return JWK{}, fmt.Errorf("Failed to json parse JWK: %w", err)
}
jwk := JWK{
KeyType: base.KeyType,
Algorithm: base.Algo,
KeyID: base.KeyID,
}
switch base.KeyType {
case KeyTypeEC:
jec := jwkEC{}
err := json.Unmarshal(jwkBytes, &jec)
if err != nil {
return JWK{}, fmt.Errorf("Failed to json parse JWK: %w", err)
}
jwk.jwtImpl = jec
case KeyTypeOct:
jsym := jwkSymmetric{}
err := json.Unmarshal(jwkBytes, &jsym)
if err != nil {
return JWK{}, fmt.Errorf("Failed to json parse JWK: %w", err)
}
jwk.jwtImpl = jsym
default:
return JWK{}, fmt.Errorf("Unsupported key type/kty - '%s'", base.KeyType)
}
return jwk, nil
}
// ReadJWKFromPEM converts a PEM encoded private key to JWK. 'kty' is set based on the passed in PrivateKey type.
func ReadJWKFromPEM(pkeyBytes []byte) (JWK, error) {
pkey, err := ReadPrivateKey(pkeyBytes)
if err != nil {
return JWK{}, err
}
return FromPrivateKey(pkey)
}
// FromPrivateKey creates a JWK from a crypto.PrivateKey. 'kty' is set based on the passed in PrivateKey.
func FromPrivateKey(pkey crypto.PrivateKey) (JWK, error) {
switch key := pkey.(type) {
case *ecdsa.PrivateKey:
jec := jwkEC{
jwk: jwk{
KeyType: KeyTypeEC,
},
Curve: key.Curve.Params().Name,
X: &octet{*key.X},
Y: &octet{*key.Y},
D: &octet{*key.D},
}
return JWK{
KeyType: KeyTypeEC,
jwtImpl: jec,
}, nil
default:
return JWK{}, fmt.Errorf("Unsupported private key type '%T'", pkey)
}
}
// JWK provides basic data and usage for a JWK.
type JWK struct {
// Common fields are duplicated as struct members for better usability.
KeyType string // 'kty' - "EC", "RSA", "oct"
Algorithm string // 'alg'
KeyID string // 'kid'
jwtImpl any // the specific implementation of JWK based on KeyType.
}
func (ji *JWK) PublicKey() (crypto.PublicKey, error) {
switch ji.KeyType {
case ji.KeyType: // ECC
if jec, ok := ji.jwtImpl.(jwkEC); ok {
return jec.PublicKey()
}
}
return nil, fmt.Errorf("Unsupported key type for PublicKey - '%s'", ji.KeyType)
}
func (ji *JWK) PublicKeyJWK() (JWK, error) {
switch ji.KeyType {
case KeyTypeEC:
if jec, ok := ji.jwtImpl.(jwkEC); ok {
jec.jwk.Algo = ji.Algorithm
jec.jwk.KeyID = ji.KeyID
return jec.PublicKeyJWK()
}
}
return JWK{}, fmt.Errorf("Unsupported key type for PublicKey'%s'", ji.KeyType)
}
func (ji *JWK) PrivateKey() (crypto.PrivateKey, error) {
switch ji.KeyType {
case KeyTypeEC:
if jec, ok := ji.jwtImpl.(jwkEC); ok {
return jec.PrivateKey()
}
}
return nil, fmt.Errorf("Unsupported key type PrivateKey - '%s'", ji.KeyType)
}
func (ji *JWK) SecretKey() ([]byte, error) {
switch ji.KeyType {
case KeyTypeOct:
if jsym, ok := ji.jwtImpl.(jwkSymmetric); ok {
return jsym.Key(), nil
}
}
return nil, fmt.Errorf("Unsupported key type for Secret '%s'", ji.KeyType)
}
// octet represents the data for base64 URL encoded data as specified by JWKs.
type octet struct {
big.Int
}
func (ob octet) MarshalJSON() ([]byte, error) {
out := fmt.Sprintf("\"%s\"", base64.RawURLEncoding.EncodeToString(ob.Bytes()))
return []byte(out), nil
}
func (ob *octet) UnmarshalJSON(data []byte) error {
// data is the json value and must be unmarshaled into a go string first
encoded := ""
err := json.Unmarshal(data, &encoded)
if err != nil {
return err
}
rawBytes, err := base64.RawURLEncoding.DecodeString(encoded)
if err != nil {
return fmt.Errorf("Failed to base64 decode: %w", err)
}
x := new(big.Int)
x.SetBytes(rawBytes)
*ob = octet{*x}
return nil
}
type jwk struct {
KeyType string `json:"kty"` // kty algorithm family used with the key such as "RSA" or "EC".
Algo string `json:"alg,omitempty"` // alg identifies the algorithm intended for use with the key.
KeyID string `json:"kid,omitempty"` // Used to match a specific key
}
type jwkEC struct {
jwk
Curve string `json:"crv"` // The curve used with the key e.g. P-256
X *octet `json:"x"` // x coordinate of the curve.
Y *octet `json:"y"` // y coordinate of the curve.
D *octet `json:"d,omitempty"` // For private keys.
}
func (ec *jwkEC) params() (crv elliptic.Curve, byteLen int, e error) {
switch ec.Curve {
case "P-256":
crv = elliptic.P256()
case "P-384":
crv = elliptic.P384()
case "P-521":
crv = elliptic.P521()
default:
return nil, 0, fmt.Errorf("Unsupported ECC curve '%s'", ec.Curve)
}
return crv, crv.Params().BitSize / 8, nil
}
func (ec *jwkEC) PublicKey() (*ecdsa.PublicKey, error) {
crv, byteLen, err := ec.params()
if err != nil {
return nil, err
}
if len(ec.X.Bytes()) != byteLen {
return nil, fmt.Errorf("X coordinate must be %d byte length for curve '%s'. Got '%d'", byteLen, ec.Curve, len(ec.X.Bytes()))
}
if len(ec.Y.Bytes()) != byteLen {
return nil, fmt.Errorf("Y coordinate must be %d byte length for curve '%s'. Got '%d'", byteLen, ec.Curve, len(ec.Y.Bytes()))
}
return &ecdsa.PublicKey{
Curve: crv,
X: &ec.X.Int,
Y: &ec.Y.Int,
}, nil
}
func (ec *jwkEC) PublicKeyJWK() (JWK, error) {
return JWK{
KeyType: ec.KeyType,
Algorithm: ec.Algo,
KeyID: ec.KeyID,
jwtImpl: jwkEC{
jwk: ec.jwk,
Curve: ec.Curve,
X: ec.X,
Y: ec.Y,
},
}, nil
}
func (ec *jwkEC) PrivateKey() (*ecdsa.PrivateKey, error) {
if ec.D == nil {
return nil, fmt.Errorf("JWK does not contain a private key")
}
pubkey, err := ec.PublicKey()
if err != nil {
return nil, err
}
_, byteLen, err := ec.params()
if err != nil {
return nil, err
}
if len(ec.D.Bytes()) != byteLen {
return nil, fmt.Errorf("D coordinate must be %d byte length for curve '%s'. Got '%d'", byteLen, ec.Curve, len(ec.D.Bytes()))
}
return &ecdsa.PrivateKey{
PublicKey: *pubkey,
D: &ec.D.Int,
}, nil
}
type jwkSymmetric struct {
jwk
K *octet `json:"k" ` // Symmetric key
}
func (js *jwkSymmetric) Key() []byte {
return js.K.Bytes()
}
func (j JWK) MarshalJSON() ([]byte, error) {
// Set the Algo and KeyID in case the JWK fields have changed
switch jt := j.jwtImpl.(type) {
case jwkEC:
jt.jwk.Algo = j.Algorithm
jt.jwk.KeyID = j.KeyID
return json.Marshal(jt)
case jwkSymmetric:
jt.jwk.Algo = j.Algorithm
jt.jwk.KeyID = j.KeyID
return json.Marshal(jt)
}
return json.Marshal(j.jwtImpl)
}

View file

@ -1,142 +0,0 @@
package keyutil
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestParseJWK(t *testing.T) {
tests := []struct {
Name string
InputFile string // one of InputFile or Input is used
Input string
Expected JWK
ExpectedErrContains string
}{
{
Name: "Valid EC JWK",
InputFile: "testdata/test-jwk-ec.json",
Expected: JWK{
KeyType: "EC",
KeyID: "test-key-ecc-p256",
},
},
{
Name: "Valid symmetric JWK",
InputFile: "testdata/test-jwk-symmetric.json",
Expected: JWK{
KeyType: "oct",
KeyID: "test-symmetric-key",
},
},
{
Name: "Invalid JSON",
Input: `{"kty": malformed`,
ExpectedErrContains: "parse",
},
{
Name: "Empty input",
Input: "",
ExpectedErrContains: "parse",
},
}
for _, tc := range tests {
t.Run(tc.Name, func(t *testing.T) {
var actual JWK
var actualErr error
if tc.InputFile != "" {
actual, actualErr = ReadJWKFile(tc.InputFile)
} else {
actual, actualErr = ReadJWK([]byte(tc.Input))
}
if actualErr != nil {
if !strings.Contains(actualErr.Error(), tc.ExpectedErrContains) {
Diff(t, tc.ExpectedErrContains, actualErr.Error(), "Wrong error")
}
return
}
Diff(t, tc.Expected, actual, "Wrong JWK", cmpopts.IgnoreUnexported(JWK{}))
})
}
}
func TestJWKMarshalRoundTrip(t *testing.T) {
tests := []struct {
name string
inputType string
expectedErrContains string
keyid string
algorithm string
}{
{
name: "EC Key Round Trip",
inputType: "EC",
keyid: "mykey_123",
algorithm: "myalgo",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var pk crypto.PrivateKey
switch tc.inputType {
case "EC":
var err error
pk, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
}
original, err := FromPrivateKey(pk)
original.KeyID = tc.keyid
original.Algorithm = tc.algorithm
if err != nil {
if tc.expectedErrContains != "" {
if !strings.Contains(err.Error(), tc.expectedErrContains) {
t.Errorf("Expected error containing %q, got: %v", tc.expectedErrContains, err)
}
return
}
t.Fatalf("Failed to generate create JWK from private key: %v", err)
}
// Marshal JWK to JSON
jsonBytes, err := json.Marshal(original)
if err != nil {
t.Fatalf("Failed to marshal JWK: %v", err)
}
// Unmarshal back to new JWK
roundTripped, err := ReadJWK(jsonBytes)
if err != nil {
t.Fatalf("Failed to unmarshal round-tripped JWK: %v", err)
}
// Compare original and round-tripped JWKs
Diff(t, original, roundTripped, "Round-tripped JWK differs from original", cmpopts.IgnoreUnexported(JWK{}))
Diff(t, tc.keyid, roundTripped.KeyID, "Round-tripped JWK differs from original", cmpopts.IgnoreUnexported(JWK{}))
})
}
}
func Diff(t *testing.T, expected, actual any, msg string, opts ...cmp.Option) bool {
if diff := cmp.Diff(expected, actual, opts...); diff != "" {
t.Errorf("%s (-want +got):\n%s", msg, diff)
return true
}
return false
}

View file

@ -1,143 +0,0 @@
/*
Package keyutil provides some basic PEM and JWK key handling without dependencies. It is not meant as a replacement for a full key handling library.
*/
package keyutil
import (
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"fmt"
"os"
)
var (
oidPublicKeyRSAPSS = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10}
)
// MustReadPublicKeyFile reads a PEM encoded public key file or panics
func MustReadPublicKeyFile(pubkeyFile string) crypto.PublicKey {
pk, err := ReadPublicKeyFile(pubkeyFile)
if err != nil {
panic(err)
}
return pk
}
// ReadPublicKeyFile reads a PEM encdoded public key file and parses into crypto.PublicKey
func ReadPublicKeyFile(pubkeyFile string) (crypto.PublicKey, error) {
keyBytes, err := os.ReadFile(pubkeyFile)
if err != nil {
return nil, fmt.Errorf("Failed to read public key file '%s': %w", pubkeyFile, err)
}
return ReadPublicKey(keyBytes)
}
// ReadPublicKey decodes a PEM encoded public key and parses into crypto.PublicKey
func ReadPublicKey(encodedPubkey []byte) (crypto.PublicKey, error) {
block, _ := pem.Decode(encodedPubkey)
if block == nil {
return nil, fmt.Errorf("Failed to PEM decode public key")
}
var key crypto.PublicKey
var err error
switch block.Type {
case "PUBLIC KEY":
key, err = x509.ParsePKIXPublicKey(block.Bytes)
case "RSA PUBLIC KEY":
key, err = x509.ParsePKCS1PublicKey(block.Bytes)
default:
return nil, fmt.Errorf("Unsupported pubkey format '%s'", block.Type)
}
if err != nil {
return nil, fmt.Errorf("Failed to parse public key with format '%s': %w", block.Type, err)
}
return key, nil
}
// MustReadPrivateKeyFile decodes a PEM encoded private key file and parses into a crypto.PrivateKey or panics.
func MustReadPrivateKeyFile(pkFile string) crypto.PrivateKey {
pk, err := ReadPrivateKeyFile(pkFile)
if err != nil {
panic(err)
}
return pk
}
func MustReadPrivateKey(encodedPrivateKey []byte) crypto.PrivateKey {
pkey, err := ReadPrivateKey(encodedPrivateKey)
if err != nil {
panic(err)
}
return pkey
}
// ReadPrivateKeyFile opens the given file and calls ReadPrivateKey to return a crypto.PrivateKey
func ReadPrivateKeyFile(pkFile string) (crypto.PrivateKey, error) {
keyBytes, err := os.ReadFile(pkFile)
if err != nil {
return nil, fmt.Errorf("Failed to read private key file '%s': %w", pkFile, err)
}
return ReadPrivateKey(keyBytes)
}
// ReadPrivateKey decoded a PEM encoded private key and parses into a crypto.PrivateKey.
func ReadPrivateKey(encodedPrivateKey []byte) (crypto.PrivateKey, error) {
block, _ := pem.Decode(encodedPrivateKey)
if block == nil {
return nil, fmt.Errorf("Failed to PEM decode private key")
}
var key crypto.PrivateKey
var err error
switch block.Type {
case "PRIVATE KEY":
key, err = x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
// Try to handle RSAPSS
psskey, psserr := parseRSAPSS(block)
if psserr == nil {
// success
key = psskey
err = psserr
}
}
case "EC PRIVATE KEY":
key, err = x509.ParseECPrivateKey(block.Bytes)
case "RSA PRIVATE KEY":
key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("Unsupported private key format '%s'", block.Type)
}
if err != nil {
return nil, fmt.Errorf("Failed to parse private key with format '%s': %w", block.Type, err)
}
return key, nil
}
func parseRSAPSS(block *pem.Block) (crypto.PrivateKey, error) {
// The rsa-pss key is PKCS8 encoded but the golang 1.19 parser doesn't recognize the algorithm and gives 'PKCS#8 wrapping contained private key with unknown algorithm: 1.2.840.113549.1.1.10
// Instead do the asn1 unmarshaling and check here.
pkcs8 := struct {
Version int
Algo pkix.AlgorithmIdentifier
PrivateKey []byte
}{}
_, err := asn1.Unmarshal(block.Bytes, &pkcs8)
if err != nil {
return nil, fmt.Errorf("Failed to ans1 unmarshal private key: %w", err)
}
if !pkcs8.Algo.Algorithm.Equal(oidPublicKeyRSAPSS) {
return nil, fmt.Errorf("PKCS#8 wrapping contained private key with unknown algorithm: %s", pkcs8.Algo.Algorithm)
}
return x509.ParsePKCS1PrivateKey(pkcs8.PrivateKey)
}

View file

@ -1,8 +0,0 @@
{
"kty": "EC",
"crv": "P-256",
"kid": "test-key-ecc-p256",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"d": "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE"
}

View file

@ -1,5 +0,0 @@
{
"kty": "oct",
"kid": "test-symmetric-key",
"k": "QYPe3dGeFrKhEEK4CSr9_nES-ExXI9Nw"
}

View file

@ -1,217 +0,0 @@
package httpsig_test
import (
"crypto"
"testing"
"github.com/remitly-oss/httpsig-go"
"github.com/remitly-oss/httpsig-go/keyman"
"github.com/remitly-oss/httpsig-go/keyutil"
"github.com/remitly-oss/httpsig-go/sigtest"
)
// TestRoundTrip tests that the signing code can be verified by the verify code.
func TestRoundTrip(t *testing.T) {
testcases := []struct {
Name string
PrivateKey crypto.PrivateKey
MetaKeyID string
Secret []byte
SignProfile httpsig.SigningProfile
RequestFile string
Keys httpsig.KeyFetcher
Profile httpsig.VerifyProfile
ExpectedErrCodeVerify httpsig.ErrCode
}{
{
Name: "RSA-PSS",
PrivateKey: keyutil.MustReadPrivateKeyFile("testdata/test-key-rsa-pss.key"),
MetaKeyID: "test-key-rsa",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_RSA_PSS_SHA512,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-rsa-pss",
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-rsa": {
KeyID: "test-key-rsa",
Algo: httpsig.Algo_RSA_PSS_SHA512,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
}),
Profile: createVerifyProfile("tst-rsa-pss"),
},
{
Name: "RSA-v15",
PrivateKey: keyutil.MustReadPrivateKeyFile("testdata/key-rsa-v15.key"),
MetaKeyID: "test-key-rsa",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_RSA_v1_5_sha256,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-rsa-pss",
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-rsa": {
KeyID: "test-key-rsa",
Algo: httpsig.Algo_RSA_v1_5_sha256,
PubKey: keyutil.MustReadPublicKeyFile("testdata/key-rsa-v15.pub"),
},
}),
Profile: createVerifyProfile("tst-rsa-pss"),
},
{
Name: "HMAC_SHA256",
Secret: sigtest.MustReadFile("testdata/test-shared-secret"),
MetaKeyID: "test-key-shared",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_HMAC_SHA256,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-shared": {
KeyID: "test-key-shared",
Algo: httpsig.Algo_HMAC_SHA256,
Secret: sigtest.MustReadFile("testdata/test-shared-secret"),
},
}),
Profile: createVerifyProfile("sig1"),
},
{
Name: "ECDSA-p265",
PrivateKey: keyutil.MustReadPrivateKeyFile("testdata/test-key-ecc-p256.key"),
MetaKeyID: "test-key-ecdsa",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_ECDSA_P256_SHA256,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-ecdsa",
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-ecdsa": {
KeyID: "test-key-ecds",
Algo: httpsig.Algo_ECDSA_P256_SHA256,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-ecc-p256.pub"),
},
}),
Profile: createVerifyProfile("tst-ecdsa"),
},
{
Name: "ECDSA-p384",
PrivateKey: keyutil.MustReadPrivateKeyFile("testdata/test-key-ecc-p384.key"),
MetaKeyID: "test-key-ecdsa",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_ECDSA_P384_SHA384,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-ecdsa",
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-ecdsa": {
KeyID: "test-key-ecdsa",
Algo: httpsig.Algo_ECDSA_P384_SHA384,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-ecc-p384.pub"),
},
}),
Profile: createVerifyProfile("tst-ecdsa"),
},
{
Name: "ED25519",
PrivateKey: keyutil.MustReadPrivateKeyFile("testdata/test-key-ed25519.key"),
MetaKeyID: "test-key-ed",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_ED25519,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-ed",
},
RequestFile: "rfc-test-request.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-ed": {
KeyID: "test-key-ed",
Algo: httpsig.Algo_ED25519,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-ed25519.pub"),
},
}),
Profile: createVerifyProfile("tst-ed"),
},
{
Name: "BadDigest",
PrivateKey: keyutil.MustReadPrivateKeyFile("testdata/test-key-ed25519.key"),
MetaKeyID: "test-key-ed",
SignProfile: httpsig.SigningProfile{
Algorithm: httpsig.Algo_ED25519,
Fields: httpsig.DefaultRequiredFields,
Metadata: []httpsig.Metadata{httpsig.MetaCreated, httpsig.MetaKeyID},
Label: "tst-content-digest",
},
RequestFile: "request_bad_digest.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-ed": {
KeyID: "test-key-ed",
Algo: httpsig.Algo_ED25519,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-ed25519.pub"),
},
}),
Profile: createVerifyProfile("tst-content-digest"),
ExpectedErrCodeVerify: httpsig.ErrNoSigWrongDigest,
},
}
for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
var signer *httpsig.Signer
sk := httpsig.SigningKey{
Key: tc.PrivateKey,
Secret: tc.Secret,
MetaKeyID: tc.MetaKeyID,
}
signer, err := httpsig.NewSigner(tc.SignProfile, sk)
if err != nil {
t.Fatal(err)
}
req := sigtest.ReadRequest(t, tc.RequestFile)
err = signer.Sign(req)
if err != nil {
t.Fatalf("%#v", err)
}
t.Log(req.Header.Get("Signature-Input"))
t.Log(req.Header.Get("Signature"))
ver, err := httpsig.NewVerifier(tc.Keys, tc.Profile)
if err != nil {
t.Fatal(err)
}
vf, err := ver.Verify(req)
if err != nil {
if tc.ExpectedErrCodeVerify != "" {
if sigerr, ok := err.(*httpsig.SignatureError); ok {
sigtest.Diff(t, tc.ExpectedErrCodeVerify, sigerr.Code, "Wrong err code")
}
} else {
t.Fatalf("%#v", err)
}
} else if tc.ExpectedErrCodeVerify != "" {
t.Fatal("Expected error")
}
t.Logf("%+v\n", vf)
})
}
}
func createVerifyProfile(label string) httpsig.VerifyProfile {
vp := httpsig.DefaultVerifyProfile
vp.SignatureLabel = label
return vp
}

View file

@ -1,5 +0,0 @@
max_iterations = 0
max_planning_iterations = 0
[[test_commands]]
command = "go test"

View file

@ -1,89 +0,0 @@
package httpsig
import (
"errors"
"fmt"
)
// ErrCode enumerates the reasons a signing or verification can fail
type ErrCode string
const (
// Error Codes
// Errors related to not being able to extract a valid signature.
ErrNoSigInvalidHeader ErrCode = "nosig_invalid_header"
ErrNoSigUnsupportedDigest ErrCode = "nosig_unsupported_digest"
ErrNoSigWrongDigest ErrCode = "nosig_wrong_digest"
ErrNoSigMissingSignature ErrCode = "nosig_missing_signature"
ErrNoSigInvalidSignature ErrCode = "nosig_invalid_signature"
ErrNoSigMessageBody ErrCode = "nosig_message_body" // Could not read message body
// Errors related to an individual signature.
ErrSigInvalidSignature ErrCode = "sig_invalid_signature" // The signature is unparseable or in the wrong format.
ErrSigKeyFetch ErrCode = "sig_key_fetch" // Failed to the key for a signature
ErrSigVerification ErrCode = "sig_failed_algo_verification" // The signature did not verify according to the algorithm.
ErrSigPublicKey ErrCode = "sig_public_key" // The public key for the signature is invalid or missing.
ErrSigSecretKey ErrCode = "sig_secret_key" // The secret key for the signature is invalid or missing.
ErrSigUnsupportedAlgorithm ErrCode = "sig_unsupported_algorithm" // unsupported or invalid algorithm.
ErrSigProfile ErrCode = "sig_failed_profile" // The signature was valid but failed the verify profile check
// Signing
ErrInvalidSignatureOptions ErrCode = "invalid_signature_options"
ErrInvalidComponent ErrCode = "invalid_component"
ErrInvalidMetadata ErrCode = "invalid_metadata"
// Accept Signature
ErrInvalidAcceptSignature ErrCode = "invalid_accept_signature"
ErrMissingAcceptSignature ErrCode = "missing_accept_signature" // The Accept-Signature field was present but had an empty value.
// General
ErrInternal ErrCode = "internal_error"
ErrUnsupported ErrCode = "unsupported" // A particular feature of the spec is not supported
)
type SignatureError struct {
Cause error // may be nil
Code ErrCode
Message string
}
func (se *SignatureError) Error() string {
return se.Message
}
func (se *SignatureError) Unwrap() error {
return se.Cause
}
func (se *SignatureError) GoString() string {
cause := ""
if se.Cause != nil {
cause = fmt.Sprintf("Cause: %s\n", se.Cause)
}
return fmt.Sprintf("Code: %s\nMessage: %s\n%s", se.Code, se.Message, cause)
}
func newError(code ErrCode, msg string, cause ...error) *SignatureError {
var rootErr error
if len(cause) > 0 {
rootErr = cause[0]
}
return &SignatureError{
Cause: rootErr,
Code: code,
Message: msg,
}
}
func errCode(err error) (ec ErrCode) {
if err == nil {
return ""
}
var se *SignatureError
if errors.As(err, &se) {
return se.Code
}
return ""
}

View file

@ -1,332 +0,0 @@
package httpsig
import (
"crypto"
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"
"unicode"
sfv "github.com/dunglas/httpsfv"
)
type Algorithm string
type Digest string
// Metadata are the named signature metadata parameters
type Metadata string
type CreatedScheme int
type ExpiresScheme int
type NonceScheme int
const (
// Supported signing algorithms
Algo_RSA_PSS_SHA512 Algorithm = "rsa-pss-sha512"
Algo_RSA_v1_5_sha256 Algorithm = "rsa-v1_5-sha256"
Algo_HMAC_SHA256 Algorithm = "hmac-sha256"
Algo_ECDSA_P256_SHA256 Algorithm = "ecdsa-p256-sha256"
Algo_ECDSA_P384_SHA384 Algorithm = "ecdsa-p384-sha384"
Algo_ED25519 Algorithm = "ed25519"
DigestSHA256 Digest = "sha-256"
DigestSHA512 Digest = "sha-512"
// Signature metadata parameters
MetaCreated Metadata = "created"
MetaExpires Metadata = "expires"
MetaNonce Metadata = "nonce"
MetaAlgorithm Metadata = "alg"
MetaKeyID Metadata = "keyid"
MetaTag Metadata = "tag"
// DefaultSignatureLabel is the label that will be used for a signature if not label is provided in the parameters.
// A request can contain multiple signatures therefore each signature is labeled.
DefaultSignatureLabel = "sig1"
// Nonce schemes
NonceRandom32 = iota // 32 bit random nonce. Base64 encoded
)
// SigningProfile is the set of fields, metadata, and the label to include in a signature.
type SigningProfile struct {
Algorithm Algorithm
Digest Digest // The http digest algorithm to apply. Defaults to sha-256.
Fields []SignedField // Fields and Derived components to sign.
Metadata []Metadata // Metadata parameters to add to the signature.
Label string // The signature label. Defaults to DefaultSignatureLabel.
ExpiresDuration time.Duration // Current time plus this duration. Default duration 5 minutes. Used only if included in Metadata.
Nonce NonceScheme // Scheme to use for generating the nonce if included in Metadata.
}
// SignedField indicates which part of the request or response to use for signing.
// This is the 'message component' in the specification.
type SignedField struct {
Name string
Parameters map[string]any // Parameters are modifiers applied to the field that changes the way the signature is calculated.
}
// Fields turns a list of fields into the full specification. Used when the signed fields/components do not need to specify any parameters
func Fields(fields ...string) []SignedField {
all := []SignedField{}
for _, field := range fields {
all = append(all, SignedField{
Name: strings.ToLower(field),
Parameters: map[string]any{},
})
}
return all
}
type SigningKey struct {
Key crypto.PrivateKey // private key for asymmetric algorithms
Secret []byte // Secret to use for symmetric algorithms
// Meta fields
MetaKeyID string // 'keyid' - Only used if 'keyid' is set in the SigningProfile. A value must be provided if the parameter is required in the SigningProfile. Metadata.
MetaTag string // 'tag'. Only used if 'tag' is set in the SigningProfile. A value must be provided if the parameter is required in the SigningProfile.
}
type Signer struct {
profile SigningProfile
skey SigningKey
}
func NewSigner(profile SigningProfile, skey SigningKey) (*Signer, error) {
err := profile.validate(skey)
if err != nil {
return nil, err
}
opts := profile.withDefaults()
s := &Signer{
profile: opts,
skey: skey,
}
return s, nil
}
func Sign(req *http.Request, params SigningProfile, skey SigningKey) error {
s, err := NewSigner(params, skey)
if err != nil {
return err
}
return s.Sign(req)
}
// Sign signs the request and adds the signature headers to the request.
// If the signature fields includes Content-Digest and Content-Digest is not already included in the request then Sign will read the request body to calculate the digest and set the header. The request body will be replaced with a new io.ReaderCloser.
func (s *Signer) Sign(req *http.Request) error {
// Add the content-digest if covered by the signature and not already present
if signedFields(s.profile.Fields).includes("content-digest") && req.Header.Get("Content-Digest") == "" {
di, err := digestBody(s.profile.Digest, req.Body)
if err != nil {
return err
}
req.Body = di.NewBody
digestValue, err := createDigestHeader(s.profile.Digest, di.Digest)
if err != nil {
return err
}
req.Header.Set("Content-Digest", digestValue)
}
baseParams, err := s.baseParameters()
if err != nil {
return err
}
return sign(
httpMessage{
Req: req,
}, sigParameters{
Base: baseParams,
Algo: s.profile.Algorithm,
PrivateKey: s.skey.Key,
Secret: s.skey.Secret,
Label: s.profile.Label,
})
}
func (s *Signer) SignResponse(resp *http.Response) error {
baseParams, err := s.baseParameters()
if err != nil {
return err
}
return sign(
httpMessage{
IsResponse: true,
Resp: resp,
}, sigParameters{
Base: baseParams,
Algo: s.profile.Algorithm,
PrivateKey: s.skey.Key,
Secret: s.skey.Secret,
Label: s.profile.Label,
})
}
func (s *Signer) baseParameters() (sigBaseInput, error) {
bp := sigBaseInput{
Components: componentsIDs(s.profile.Fields),
MetadataParams: s.profile.Metadata,
MetadataValues: s,
}
return bp, nil
}
func (s *Signer) Created() (int, error) {
return int(time.Now().Unix()), nil
}
func (s *Signer) Expires() (int, error) {
return int(time.Now().Add(s.profile.ExpiresDuration).Unix()), nil
}
func (s *Signer) Nonce() (string, error) {
switch s.profile.Nonce {
case NonceRandom32:
return nonceRandom32()
}
return "", fmt.Errorf("Invalid nonce scheme '%d'", s.profile.Nonce)
}
func (s *Signer) Alg() (string, error) {
return string(s.profile.Algorithm), nil
}
func (s *Signer) KeyID() (string, error) {
return s.skey.MetaKeyID, nil
}
func (s *Signer) Tag() (string, error) {
return s.skey.MetaTag, nil
}
func (so SigningProfile) validate(skey SigningKey) error {
if so.Algorithm == "" {
return fmt.Errorf("Missing required signing option 'Algorithm'")
}
if so.Algorithm.symmetric() && len(skey.Secret) == 0 {
return newError(ErrInvalidSignatureOptions, "Missing required 'Secret' value in SigningKey")
}
if !so.Algorithm.symmetric() && skey.Key == nil {
return newError(ErrInvalidSignatureOptions, "Missing required 'Key' value in SigningKey")
}
if !isSafeString(so.Label) {
return fmt.Errorf("Invalid label name '%s'", so.Label)
}
for _, sf := range so.Fields {
if !isSafeString(sf.Name) {
return fmt.Errorf("Invalid signing field name '%s'", sf.Name)
}
}
for _, md := range so.Metadata {
switch md {
case MetaKeyID:
if skey.MetaKeyID == "" {
return fmt.Errorf("'keyid' metadata parameter was listed but missing MetaKeyID value'")
}
if !isSafeString(skey.MetaKeyID) {
return fmt.Errorf("'keyid' metadata parameter can only contain printable characters'")
}
case MetaTag:
if skey.MetaTag == "" {
return fmt.Errorf("'tag' metadata parameter was listed but missing MetaTag value'")
}
if !isSafeString(skey.MetaTag) {
return fmt.Errorf("'tag' metadata parameter can only contain printable characters'")
}
}
}
return nil
}
func (sp SigningProfile) withDefaults() SigningProfile {
final := SigningProfile{
Algorithm: sp.Algorithm,
Digest: sp.Digest,
Fields: sp.Fields,
Metadata: sp.Metadata,
Label: sp.Label,
ExpiresDuration: sp.ExpiresDuration,
Nonce: NonceRandom32,
}
// Defaults
if final.Label == "" {
final.Label = DefaultSignatureLabel
}
if final.ExpiresDuration == 0 {
final.ExpiresDuration = time.Minute * 5
}
if final.Digest == "" {
final.Digest = DigestSHA256
}
return final
}
func (sf SignedField) componentID() componentID {
item := sfv.NewItem(sf.Name)
for key, param := range sf.Parameters {
item.Params.Add(key, param)
}
return componentID{
Name: strings.ToLower(sf.Name),
Item: item,
}
}
type signedFields []SignedField
func (sf signedFields) includes(field string) bool {
target := strings.ToLower(field)
for _, fld := range sf {
if fld.Name == target {
return true
}
}
return false
}
func (a Algorithm) symmetric() bool {
switch a {
case Algo_HMAC_SHA256:
return true
}
return false
}
func componentsIDs(sfs []SignedField) []componentID {
cIDs := []componentID{}
for _, sf := range sfs {
cIDs = append(cIDs, sf.componentID())
}
return cIDs
}
func nonceRandom32() (string, error) {
nonce := make([]byte, 32)
n, err := rand.Read(nonce)
if err != nil || n < 32 {
return "", fmt.Errorf("could not generate nonce")
}
return base64.StdEncoding.EncodeToString(nonce), nil
}
func isSafeString(s string) bool {
for _, c := range s {
if !unicode.IsPrint(c) {
return false
}
if c > unicode.MaxASCII {
return false
}
}
return true
}

View file

@ -1,275 +0,0 @@
package httpsig
import (
"bufio"
"fmt"
"net/http"
"os"
"testing"
"github.com/remitly-oss/httpsig-go/sigtest"
)
// testcaseSigBase is a test case for signature bases
type testcaseSigBase struct {
Name string
Params sigBaseInput
IsResponse bool
SourceFile string // defaults to the specification request or response file
ExpectedFile string
ExpectedErr ErrCode
}
func TestSignatureBase(t *testing.T) {
cases := []testcaseSigBase{
{
Name: "RepeatedComponents",
Params: sigBaseInput{
Components: makeComponents("one", "two", "one", "three"),
MetadataParams: []Metadata{},
MetadataValues: emptyMeta,
},
SourceFile: "request_repeated_components.txt",
ExpectedErr: ErrInvalidSignatureOptions,
},
{
Name: "BadComponentName",
Params: sigBaseInput{
Components: makeComponents("\xd3", "two", "one", "three"),
MetadataParams: []Metadata{},
MetadataValues: emptyMeta,
},
ExpectedErr: ErrInvalidComponent,
},
{
Name: "NoMultiValueSuport",
Params: sigBaseInput{
Components: makeComponents("one"),
MetadataParams: []Metadata{},
MetadataValues: emptyMeta,
},
ExpectedErr: ErrUnsupported,
SourceFile: "request_multivalue.txt",
},
{
Name: "BadMeta-Created",
Params: sigBaseInput{
Components: makeComponents(),
MetadataParams: []Metadata{
MetaCreated,
},
MetadataValues: errorMetadataProvider{},
},
ExpectedErr: ErrInvalidMetadata,
},
{
Name: "BadMeta-Expires",
Params: sigBaseInput{
Components: makeComponents(),
MetadataParams: []Metadata{
MetaExpires,
},
MetadataValues: errorMetadataProvider{},
},
ExpectedErr: ErrInvalidMetadata,
},
{
Name: "BadMeta-Nonce",
Params: sigBaseInput{
Components: makeComponents(),
MetadataParams: []Metadata{
MetaNonce,
},
MetadataValues: errorMetadataProvider{},
},
ExpectedErr: ErrInvalidMetadata,
},
{
Name: "BadMeta-Algorithm",
Params: sigBaseInput{
Components: makeComponents(),
MetadataParams: []Metadata{
MetaAlgorithm,
},
MetadataValues: errorMetadataProvider{},
},
ExpectedErr: ErrInvalidMetadata,
},
{
Name: "BadMeta-KeyID",
Params: sigBaseInput{
Components: makeComponents(),
MetadataParams: []Metadata{
MetaKeyID,
},
MetadataValues: errorMetadataProvider{},
},
ExpectedErr: ErrInvalidMetadata,
},
{
Name: "BadMeta-Tag",
Params: sigBaseInput{
Components: makeComponents(),
MetadataParams: []Metadata{
MetaTag,
},
MetadataValues: errorMetadataProvider{},
},
ExpectedErr: ErrInvalidMetadata,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
runTestSigBase(t, tc)
})
}
}
func runTestSigBase(t *testing.T, tc testcaseSigBase) {
sourceFile := tc.SourceFile
hrr := httpMessage{
IsResponse: tc.IsResponse,
}
if tc.IsResponse {
if sourceFile == "" {
sourceFile = "rfc-test-response.txt"
}
resptxt, err := os.Open(fmt.Sprintf("testdata/%s", sourceFile))
if err != nil {
t.Fatal(err)
}
resp, err := http.ReadResponse(bufio.NewReader(resptxt), nil)
if err != nil {
t.Fatal(err)
}
hrr.Resp = resp
} else {
if sourceFile == "" {
sourceFile = "rfc-test-request.txt"
}
// request
reqtxt, err := os.Open(fmt.Sprintf("testdata/%s", sourceFile))
if err != nil {
t.Fatal(err)
}
req, err := http.ReadRequest(bufio.NewReader(reqtxt))
if err != nil {
t.Fatal(err)
}
hrr.Req = req
}
actualBase, err := calculateSignatureBase(hrr, tc.Params)
if sigtest.Diff(t, tc.ExpectedErr, errCode(err), "Wrong error code") {
return
} else if tc.ExpectedErr != "" {
// If an error is expected and the err Diff check has passed then don't continue on to test the result
return
}
t.Log(string(actualBase.base))
expectedBase, err := os.ReadFile(fmt.Sprintf("testdata/%s", tc.ExpectedFile))
if err != nil {
t.Fatal(err)
}
if sigtest.Diff(t, string(expectedBase), string(actualBase.base), "Signature base did not match") {
t.FailNow()
}
}
type errorMetadataProvider struct{}
func (fmp errorMetadataProvider) Created() (int, error) {
return 0, fmt.Errorf("No created value")
}
func (fmp errorMetadataProvider) Expires() (int, error) {
return 0, fmt.Errorf("No expires value")
}
func (fmp errorMetadataProvider) Nonce() (string, error) {
return "", fmt.Errorf("No nonce value")
}
func (fmp errorMetadataProvider) Alg() (string, error) {
return "", fmt.Errorf("No alg value")
}
func (fmp errorMetadataProvider) KeyID() (string, error) {
return "", fmt.Errorf("No keyid value")
}
func (fmp errorMetadataProvider) Tag() (string, error) {
return "", fmt.Errorf("No tag value")
}
var emptyMeta = fixedMetadataProvider{
values: map[Metadata]any{},
}
type fixedMetadataProvider struct {
values map[Metadata]any
}
func (fmp fixedMetadataProvider) Created() (int, error) {
if val, ok := fmp.values[MetaCreated]; ok {
return int(val.(int64)), nil
}
return 0, fmt.Errorf("No created value")
}
func (fmp fixedMetadataProvider) Expires() (int, error) {
if val, ok := fmp.values[MetaExpires]; ok {
return int(val.(int64)), nil
}
return 0, fmt.Errorf("No expires value")
}
func (fmp fixedMetadataProvider) Nonce() (string, error) {
if val, ok := fmp.values[MetaNonce]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No nonce value")
}
func (fmp fixedMetadataProvider) Alg() (string, error) {
if val, ok := fmp.values[MetaAlgorithm]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No alg value")
}
func (fmp fixedMetadataProvider) KeyID() (string, error) {
if val, ok := fmp.values[MetaKeyID]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No keyid value")
}
func (fmp fixedMetadataProvider) Tag() (string, error) {
if val, ok := fmp.values[MetaTag]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No tag value")
}
func makeComponents(ids ...string) []componentID {
cids := []componentID{}
for _, id := range ids {
cids = append(cids, SignedField{
Name: id,
}.componentID())
}
return cids
}
func makeComponentIDs(sfs ...SignedField) []componentID {
cids := []componentID{}
for _, sf := range sfs {
cids = append(cids, sf.componentID())
}
return cids
}

View file

@ -1,172 +0,0 @@
package httpsig
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"fmt"
"time"
sfv "github.com/dunglas/httpsfv"
)
// derived component names
type derived string
const (
sigparams derived = "@signature-params"
method derived = "@method"
path derived = "@path"
targetURI derived = "@target-uri"
authority derived = "@authority"
)
// MetadataProvider allows customized functions for metadata parameter values. Not needed for default usage.
type MetadataProvider interface {
Created() (int, error)
Expires() (int, error)
Nonce() (string, error)
Alg() (string, error)
KeyID() (string, error)
Tag() (string, error)
}
type signatureBase struct {
base []byte // The full signature base. Use this as input to signing and verification
signatureInput string // The signature-input line
}
type sigParameters struct {
Base sigBaseInput
Algo Algorithm
Label string
PrivateKey crypto.PrivateKey
Secret []byte
}
func sign(hrr httpMessage, sp sigParameters) error {
base, err := calculateSignatureBase(hrr, sp.Base)
if err != nil {
return err
}
var sigBytes []byte
switch sp.Algo {
case Algo_RSA_PSS_SHA512:
msgHash := sha512.Sum512(base.base)
opts := &rsa.PSSOptions{
SaltLength: 64,
Hash: crypto.SHA512,
}
switch rsapk := sp.PrivateKey.(type) {
case *rsa.PrivateKey:
sigBytes, err = rsa.SignPSS(rand.Reader, rsapk, crypto.SHA512, msgHash[:], opts)
if err != nil {
return err
}
case crypto.Signer:
sigBytes, err = rsapk.Sign(rand.Reader, msgHash[:], crypto.SHA512)
if err != nil {
return err
}
default:
return fmt.Errorf("Invalid private key. Requires *rsa.PrivateKey or crypto.Signer: %T", sp.PrivateKey)
}
case Algo_RSA_v1_5_sha256:
msgHash := sha256.Sum256(base.base)
switch rsapk := sp.PrivateKey.(type) {
case *rsa.PrivateKey:
sigBytes, err = rsa.SignPKCS1v15(rand.Reader, rsapk, crypto.SHA256, msgHash[:])
if err != nil {
return err
}
case crypto.Signer:
sigBytes, err = rsapk.Sign(rand.Reader, msgHash[:], crypto.SHA256)
if err != nil {
return err
}
default:
return fmt.Errorf("Invalid private key. Requires *rsa.PrivateKey or crypto.Signer: %T", sp.PrivateKey)
}
case Algo_ECDSA_P256_SHA256:
msgHash := sha256.Sum256(base.base)
switch eccpk := sp.PrivateKey.(type) {
case *ecdsa.PrivateKey:
r, s, err := ecdsa.Sign(rand.Reader, eccpk, msgHash[:])
if err != nil {
return newError(ErrInternal, "Failed to sign with ecdsa private key", err)
}
// Concatenate r and s to make the signature as per the spec. r and s are *not* encoded in ASN1 format
sigBytes = make([]byte, 64)
r.FillBytes(sigBytes[0:32])
s.FillBytes(sigBytes[32:64])
case crypto.Signer:
sigBytes, err = eccpk.Sign(rand.Reader, msgHash[:], crypto.SHA256)
if err != nil {
return newError(ErrInternal, "Failed to sign with ecdsa custom signer", err)
}
default:
return fmt.Errorf("Invalid private key. Requires *ecdsa.PrivateKey or crypto.Signer: %T", sp.PrivateKey)
}
case Algo_ECDSA_P384_SHA384:
msgHash := sha512.Sum384(base.base)
switch eccpk := sp.PrivateKey.(type) {
case *ecdsa.PrivateKey:
r, s, err := ecdsa.Sign(rand.Reader, eccpk, msgHash[:])
if err != nil {
return newError(ErrInternal, "Failed to sign with ecdsa private key", err)
}
// Concatenate r and s to make the signature as per the spec. r and s are *not* encoded in ASN1 format
sigBytes = make([]byte, 96)
r.FillBytes(sigBytes[0:48])
s.FillBytes(sigBytes[48:96])
case crypto.Signer:
sigBytes, err = eccpk.Sign(rand.Reader, msgHash[:], crypto.SHA384)
if err != nil {
return newError(ErrInternal, "Failed to sign with ecdsa custom signer", err)
}
default:
return fmt.Errorf("Invalid private key. Requires *ecdsa.PrivateKey or crypto.Signer: %T", sp.PrivateKey)
}
case Algo_ED25519:
switch edpk := sp.PrivateKey.(type) {
case ed25519.PrivateKey:
sigBytes = ed25519.Sign(edpk, base.base)
case crypto.Signer:
sigBytes, err = edpk.Sign(rand.Reader, base.base, crypto.Hash(0))
if err != nil {
return newError(ErrInternal, "Failed to sign with ed25519 custom signer", err)
}
default:
return fmt.Errorf("Invalid private key. Requires ed25519.PrivateKey or crypto.Signer: %T", sp.PrivateKey)
}
case Algo_HMAC_SHA256:
if len(sp.Secret) == 0 {
return newError(ErrInvalidSignatureOptions, fmt.Sprintf("No secret provided for symmetric algorithm '%s'", Algo_HMAC_SHA256))
}
msgHash := hmac.New(sha256.New, sp.Secret)
msgHash.Write(base.base) // write does not return an error per hash.Hash documentation
sigBytes = msgHash.Sum(nil)
default:
return newError(ErrInvalidSignatureOptions, fmt.Sprintf("Signing algorithm not supported: '%s'", sp.Algo))
}
sigField := sfv.NewDictionary()
sigField.Add(sp.Label, sfv.NewItem(sigBytes))
signature, err := sfv.Marshal(sigField)
if err != nil {
return newError(ErrInternal, fmt.Sprintf("Failed to marshal signature for label '%s'", sp.Label), err)
}
hrr.Headers().Set("Signature-Input", fmt.Sprintf("%s=%s", sp.Label, base.signatureInput))
hrr.Headers().Set("Signature", signature)
return nil
}
func timestamp(nowtime func() time.Time) int {
return int(nowtime().Unix())
}

View file

@ -1,85 +0,0 @@
package sigtest
import (
"bufio"
"bytes"
"crypto"
"encoding/base64"
"fmt"
"io"
"net/http"
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/remitly-oss/httpsig-go/keyutil"
)
func ReadRequest(t testing.TB, reqFile string) *http.Request {
reqtxt, err := os.Open(fmt.Sprintf("testdata/%s", reqFile))
if err != nil {
t.Fatal(err)
}
req, err := http.ReadRequest(bufio.NewReader(reqtxt))
if err != nil {
t.Fatal(err)
}
return req
}
func MustReadFile(file string) []byte {
data, err := os.ReadFile(file)
if err != nil {
panic(err)
}
return data
}
func MakeBody(body string) io.ReadCloser {
return io.NopCloser(bytes.NewReader([]byte(body)))
}
func ReadSharedSecret(t *testing.T, sharedSecretFile string) []byte {
secretBytes, err := os.ReadFile(fmt.Sprintf("testdata/%s", sharedSecretFile))
if err != nil {
t.Fatal(err)
}
secret, err := base64.StdEncoding.DecodeString(string(secretBytes))
if err != nil {
t.Fatal(err)
}
return secret
}
func ReadTestPubkey(t *testing.T, pubkeyFile string) crypto.PublicKey {
keybytes, err := os.ReadFile(fmt.Sprintf("testdata/%s", pubkeyFile))
if err != nil {
t.Fatal(err)
}
pubkey, err := keyutil.ReadPublicKey(keybytes)
if err != nil {
t.Fatal(err)
}
return pubkey
}
func ReadTestPrivateKey(t testing.TB, pkFile string, hint ...string) crypto.PrivateKey {
keybytes, err := os.ReadFile(fmt.Sprintf("testdata/%s", pkFile))
if err != nil {
t.Fatal(err)
}
pkey, err := keyutil.ReadPrivateKey(keybytes)
if err != nil {
t.Fatal(err)
}
return pkey
}
func Diff(t *testing.T, expected, actual any, msg string, opts ...cmp.Option) bool {
if diff := cmp.Diff(expected, actual, opts...); diff != "" {
t.Errorf("%s (-want +got):\n%s", msg, diff)
return true
}
return false
}

View file

@ -1,385 +0,0 @@
package httpsig
import (
"bufio"
"context"
"fmt"
"net/http"
"os"
"testing"
"github.com/remitly-oss/httpsig-go/sigtest"
)
/*
Test cases from https://www.rfc-editor.org/rfc/rfc9421.pdf
*/
// TestSpecVerify ensures that requests from spec can be verified
func TestSpecVerify(t *testing.T) {
cases := []struct {
Name string
IsResponse bool
Key KeySpec
SignedRequestOrResonseFile string
Skip bool
}{
{
Name: "b21",
Key: KeySpec{
KeyID: "test-key-rsa-pss",
Algo: Algo_RSA_PSS_SHA512,
PubKey: sigtest.ReadTestPubkey(t, "test-key-rsa-pss.pub"),
},
SignedRequestOrResonseFile: "b21_request_signed.txt",
},
{
Name: "b22",
Key: KeySpec{
KeyID: "test-key-rsa-pss",
Algo: Algo_RSA_PSS_SHA512,
PubKey: sigtest.ReadTestPubkey(t, "test-key-rsa-pss.pub"),
},
SignedRequestOrResonseFile: "b22_request_signed.txt",
},
{
Name: "b23",
Key: KeySpec{
KeyID: "test-key-rsa-pss",
Algo: Algo_RSA_PSS_SHA512,
PubKey: sigtest.ReadTestPubkey(t, "test-key-rsa-pss.pub"),
},
SignedRequestOrResonseFile: "b23_request_signed.txt",
},
{
Name: "b24",
IsResponse: true,
Key: KeySpec{
KeyID: "test-key-ecc-p256",
Algo: Algo_ECDSA_P256_SHA256,
PubKey: sigtest.ReadTestPubkey(t, "test-key-ecc-p256.pub"),
},
SignedRequestOrResonseFile: "b24_response_signed.txt",
},
{
Name: "b25",
Key: KeySpec{
KeyID: "test-shared-secret",
Algo: Algo_HMAC_SHA256,
Secret: sigtest.ReadSharedSecret(t, "test-shared-secret"),
},
SignedRequestOrResonseFile: "b25_request_signed.txt",
},
{
Name: "b26",
Key: KeySpec{
KeyID: "test-key-ed25519",
Algo: Algo_ED25519,
PubKey: sigtest.ReadTestPubkey(t, "test-key-ed25519.pub"),
},
SignedRequestOrResonseFile: "b26_request_signed.txt",
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
if tc.Skip {
t.Skip(fmt.Sprintf("Skipping test %s", tc.Name))
}
hrrtxt, err := os.Open(fmt.Sprintf("testdata/%s", tc.SignedRequestOrResonseFile))
if err != nil {
t.Fatal(err)
}
ver, err := NewVerifier(&fixedKeyFetch{
requiredKeyID: tc.Key.KeyID,
key: tc.Key,
}, VerifyProfile{
SignatureLabel: fmt.Sprintf("sig-%s", tc.Name),
})
var verifyErr error
if tc.IsResponse {
resp, err := http.ReadResponse(bufio.NewReader(hrrtxt), nil)
if err != nil {
t.Fatal(err)
}
_, verifyErr = ver.VerifyResponse(resp)
} else {
req, err := http.ReadRequest(bufio.NewReader(hrrtxt))
if err != nil {
t.Fatal(err)
}
_, verifyErr = ver.Verify(req)
}
if verifyErr != nil {
t.Fatalf("%#v\n", verifyErr)
}
})
}
}
// TestSpecBase test recreation of the signature bases from the spec
func TestSpecBase(t *testing.T) {
cases := []testcaseSigBase{
{
Name: "b21",
Params: sigBaseInput{
Components: []componentID{},
MetadataParams: []Metadata{
MetaCreated,
MetaKeyID,
MetaNonce,
},
MetadataValues: fixedMetadataProvider{
values: map[Metadata]any{
MetaCreated: int64(1618884473),
MetaKeyID: "test-key-rsa-pss",
MetaNonce: "b3k2pp5k7z-50gnwp.yemd",
},
},
},
ExpectedFile: "b21_request_sigbase.txt",
},
{
Name: "b22",
Params: sigBaseInput{
Components: makeComponentIDs(
SignedField{
Name: "@authority",
},
SignedField{
Name: "content-digest",
},
SignedField{
Name: "@query-param",
Parameters: map[string]any{
"name": "Pet",
},
},
),
MetadataParams: []Metadata{
MetaCreated,
MetaKeyID,
MetaTag,
},
MetadataValues: fixedMetadataProvider{
values: map[Metadata]any{
MetaCreated: int64(1618884473),
MetaKeyID: "test-key-rsa-pss",
MetaTag: "header-example",
},
},
},
ExpectedFile: "b22_request_sigbase.txt",
},
{
Name: "b23",
Params: sigBaseInput{
Components: makeComponentIDs(
SignedField{
Name: "date",
},
SignedField{
Name: "@method",
},
SignedField{
Name: "@path",
},
SignedField{
Name: "@query",
},
SignedField{
Name: "@authority",
},
SignedField{
Name: "content-type",
},
SignedField{
Name: "content-digest",
},
SignedField{
Name: "content-length",
},
),
MetadataParams: []Metadata{
MetaCreated,
MetaKeyID,
},
MetadataValues: fixedMetadataProvider{
values: map[Metadata]any{
MetaCreated: int64(1618884473),
MetaKeyID: "test-key-rsa-pss",
},
},
},
ExpectedFile: "b23_request_sigbase.txt",
},
{
Name: "b24",
IsResponse: true,
Params: sigBaseInput{
Components: componentsIDs(Fields("@status", "content-type", "content-digest", "content-length")),
MetadataParams: []Metadata{
MetaCreated,
MetaKeyID,
},
MetadataValues: fixedMetadataProvider{
values: map[Metadata]any{
MetaCreated: int64(1618884473),
MetaKeyID: "test-key-ecc-p256",
},
},
},
ExpectedFile: "b24_response_sigbase.txt",
},
{
Name: "b25",
Params: sigBaseInput{
Components: componentsIDs(Fields("date", "@authority", "content-type")),
MetadataParams: []Metadata{
MetaCreated,
MetaKeyID,
},
MetadataValues: fixedMetadataProvider{
values: map[Metadata]any{
MetaCreated: int64(1618884473),
MetaKeyID: "test-shared-secret",
},
},
},
ExpectedFile: "b25_request_sigbase.txt",
},
{
Name: "b26",
Params: sigBaseInput{
Components: componentsIDs(Fields("date", "@method", "@path", "@authority", "content-type", "content-length")),
MetadataParams: []Metadata{
MetaCreated,
MetaKeyID,
},
MetadataValues: fixedMetadataProvider{
values: map[Metadata]any{
MetaCreated: int64(1618884473),
MetaKeyID: "test-key-ed25519",
},
},
},
ExpectedFile: "b26_request_sigbase.txt",
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
runTestSigBase(t, tc)
})
}
}
type fixedKeyFetch struct {
requiredKeyID string // if not empty, Fetch will check if the input keyID matches
key KeySpec
}
func (kf fixedKeyFetch) FetchByKeyID(ctx context.Context, rh http.Header, keyID string) (KeySpecer, error) {
if kf.requiredKeyID != "" && keyID != kf.requiredKeyID {
return nil, &KeyError{
error: fmt.Errorf("Invalid key id. Wanted '%s' got '%s'", kf.requiredKeyID, keyID),
}
}
return kf.key, nil
}
func (kf fixedKeyFetch) Fetch(ctx context.Context, rh http.Header, md MetadataProvider) (KeySpecer, error) {
return nil, fmt.Errorf("Fetch without a key id not supported.")
}
// TestSpecRecreateSignature recreates the signature in the test cases.
// Algorithms that include randomness in the signing (each signature is unique) cannot be tested in this way.
func TestSpecRecreateSignature(t *testing.T) {
cases := []struct {
Name string
Params sigParameters
ExpectedFile string
}{
{
Name: "b25",
Params: sigParameters{
Base: sigBaseInput{
Components: componentsIDs(Fields("date", "@authority", "content-type")),
MetadataParams: []Metadata{
MetaCreated,
MetaKeyID,
},
MetadataValues: fixedMetadataProvider{
values: map[Metadata]any{
MetaCreated: int64(1618884473),
MetaKeyID: "test-shared-secret",
},
},
},
Algo: Algo_HMAC_SHA256,
Label: "sig-b25",
Secret: sigtest.ReadSharedSecret(t, "test-shared-secret"),
},
ExpectedFile: "b25_request_signed.txt",
},
{
Name: "b26",
Params: sigParameters{
Base: sigBaseInput{
Components: componentsIDs(Fields("date", "@method", "@path", "@authority", "content-type", "content-length")),
MetadataParams: []Metadata{
MetaCreated,
MetaKeyID,
},
MetadataValues: fixedMetadataProvider{
values: map[Metadata]any{
MetaCreated: int64(1618884473),
MetaKeyID: "test-key-ed25519",
},
},
},
Algo: Algo_ED25519,
Label: "sig-b26",
PrivateKey: sigtest.ReadTestPrivateKey(t, "test-key-ed25519.key"),
},
ExpectedFile: "b26_request_signed.txt",
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
reqtxt, err := os.Open("testdata/rfc-test-request.txt")
if err != nil {
t.Fatal(err)
}
req, err := http.ReadRequest(bufio.NewReader(reqtxt))
if err != nil {
t.Fatal(err)
}
err = sign(httpMessage{
Req: req,
}, tc.Params)
if err != nil {
t.Fatalf("%#v\n", err)
}
expectedtxt, err := os.Open(fmt.Sprintf("testdata/%s", tc.ExpectedFile))
if err != nil {
t.Fatal(err)
}
expected, err := http.ReadRequest(bufio.NewReader(expectedtxt))
if err != nil {
t.Fatal(err)
}
sigtest.Diff(t, expected.Header, req.Header, "")
})
}
}

View file

@ -1 +0,0 @@
"@signature-params": ();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"

View file

@ -1,10 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Content-Length: 18
Signature-Input: sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"
Signature: sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:
{"hello": "world"}

View file

@ -1,4 +0,0 @@
"@authority": example.com
"content-digest": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
"@query-param";name="Pet": dog
"@signature-params": ("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="test-key-rsa-pss";tag="header-example"

View file

@ -1,10 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Content-Length: 18
Signature-Input: sig-b22=("@authority" "content-digest" "@query-param";name="Pet");created=1618884473;keyid="test-key-rsa-pss";tag="header-example"
Signature: sig-b22=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQEdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SARYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoKUqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:
{"hello": "world"}

View file

@ -1,9 +0,0 @@
"date": Tue, 20 Apr 2021 02:07:55 GMT
"@method": POST
"@path": /foo
"@query": ?param=Value&Pet=dog
"@authority": example.com
"content-type": application/json
"content-digest": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
"content-length": 18
"@signature-params": ("date" "@method" "@path" "@query" "@authority" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-rsa-pss"

View file

@ -1,10 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Content-Length: 18
Signature-Input: sig-b23=("date" "@method" "@path" "@query" "@authority" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-rsa-pss"
Signature: sig-b23=:bbN8oArOxYoyylQQUU6QYwrTuaxLwjAC9fbY2F6SVWvh0yBiMIRGOnMYwZ/5MR6fb0Kh1rIRASVxFkeGt683+qRpRRU5p2voTp768ZrCUb38K0fUxN0O0iC59DzYx8DFll5GmydPxSmme9v6ULbMFkl+V5B1TP/yPViV7KsLNmvKiLJH1pFkh/aYA2HXXZzNBXmIkoQoLd7YfW91kE9o/CCoC1xMy7JA1ipwvKvfrs65ldmlu9bpG6A9BmzhuzF8Eim5f8ui9eH8LZH896+QIF61ka39VBrohr9iyMUJpvRX2Zbhl5ZJzSRxpJyoEZAFL2FUo5fTIztsDZKEgM4cUA==:
{"hello": "world"}

View file

@ -1,5 +0,0 @@
"@status": 200
"content-type": application/json
"content-digest": sha-512=:mEWXIS7MaLRuGgxOBdODa3xqM1XdEvxoYhvlCFJ41QJgJc4GTsPp29l5oGX69wWdXymyU0rjJuahq4l5aGgfLQ==:
"content-length": 23
"@signature-params": ("@status" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-ecc-p256"

View file

@ -1,9 +0,0 @@
HTTP/1.1 200 OK
Date: Tue, 20 Apr 2021 02:07:56 GMT
Content-Type: application/json
Content-Digest: sha-512=:mEWXIS7MaLRuGgxOBdODa3xqM1XdEvxoYhvlCFJ41QJgJc4GTsPp29l5oGX69wWdXymyU0rjJuahq4l5aGgfLQ==:
Content-Length: 23
Signature-Input: sig-b24=("@status" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-ecc-p256"
Signature: sig-b24=:wNmSUAhwb5LxtOtOpNa6W5xj067m5hFrj0XQ4fvpaCLx0NKocgPquLgyahnzDnDAUy5eCdlYUEkLIj+32oiasw==:
{"message": "good dog"}

View file

@ -1,4 +0,0 @@
"date": Tue, 20 Apr 2021 02:07:55 GMT
"@authority": example.com
"content-type": application/json
"@signature-params": ("date" "@authority" "content-type");created=1618884473;keyid="test-shared-secret"

View file

@ -1,10 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Content-Length: 18
Signature-Input: sig-b25=("date" "@authority" "content-type");created=1618884473;keyid="test-shared-secret"
Signature: sig-b25=:pxcQw6G3AjtMBQjwo8XzkZf/bws5LelbaMk5rGIGtE8=:
{"hello": "world"}

View file

@ -1,7 +0,0 @@
"date": Tue, 20 Apr 2021 02:07:55 GMT
"@method": POST
"@path": /foo
"@authority": example.com
"content-type": application/json
"content-length": 18
"@signature-params": ("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"

View file

@ -1,10 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Content-Length: 18
Signature-Input: sig-b26=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"
Signature: sig-b26=:wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw==:
{"hello": "world"}

View file

@ -1,3 +0,0 @@
go test fuzz v1
string("0")
string("0")

View file

@ -1,3 +0,0 @@
go test fuzz v1
string("0")
string("")

View file

@ -1,4 +0,0 @@
go test fuzz v1
string("0")
string("\x0f")
string("\x7f")

View file

@ -1,4 +0,0 @@
go test fuzz v1
string("0")
string("0")
string("\x7f")

View file

@ -1,4 +0,0 @@
go test fuzz v1
string("\n")
string("0")
string("0")

View file

@ -1,4 +0,0 @@
go test fuzz v1
string("")
string("0")
string("@")

View file

@ -1,4 +0,0 @@
go test fuzz v1
string("")
string("0")
string("\xde")

View file

@ -1,16 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAK+BnK7LFtj6kjM1
bL7HtCQQpCXj1mZtCTATz4HZS9QD5eGiu7RWdSW+dd0poGmfOE3GS2fofAt3oJip
mV8tNW9uB4NYTwq5fFb0MmL34BDQBQo6obCOAkx6UG0h8y91Y+Dp9lcKwN++O8mR
w6bHiVyfATJm3hgSHnmgiKyL0yJFAgMBAAECgYAj2Dvw8yeabyq27L1mBZGEICX2
Wx8p0jEjMZia849qINWtjLf7cAEDEXAvGFZb3Bn6wHocIb5b9TXGmDTr3GbiIWhH
glwfz/aFths6xTquHcLZJ1TEGkZqRYcKvxSN3PBUYdpq40D5hPKKjerpsvN5YPk2
b/P4V+jrCNr6futBoQJBAOS3NJuxur5vHRVdUJ3T1Me6Jv+Rs6mtK8DdJ02ARzvo
fBntb6BYEjEoSanqdFkeGHer5YyHLXqquGWA86Ghsy0CQQDEcW9pIxNZlaNbV8yq
2JaHFCalYoH8+dRbGKYqJW3izL/Kffp9jpmbTf8B7VzugHOO43lk12X+f1idxuD8
Snp5AkBCkbxjMKi88tRROpbTSSuecmUVb9AOK9QXT4c3/IU/P5yXY09hKSEqY6KF
LTNuGN9gPY0TiOjI0lXXXWAMBGeFAkEAiQTWQO9GP+Yv2zaSe1g3JmDX0+Ox51Ia
3K+Et0EENH28CPF2Fr2wRrNQe3ekqnbOI4xmz/+uFKWeme5uX4tTgQJBAMJZY53Q
kIO53yMCvxth9HY6yAXMhbDTVyW7CW6yeyscM43HtQdlNtWPYwn4XuEzxyXHJBYb
l0097RDjZs8KCHw=
-----END PRIVATE KEY-----

View file

@ -1,6 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCvgZyuyxbY+pIzNWy+x7QkEKQl
49ZmbQkwE8+B2UvUA+Xhoru0VnUlvnXdKaBpnzhNxktn6HwLd6CYqZlfLTVvbgeD
WE8KuXxW9DJi9+AQ0AUKOqGwjgJMelBtIfMvdWPg6fZXCsDfvjvJkcOmx4lcnwEy
Zt4YEh55oIisi9MiRQIDAQAB
-----END PUBLIC KEY-----

View file

@ -1,8 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Content-Length: 18
{"hello": "world"}

View file

@ -1,8 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Content-Length: 18
{"hello": "bad digest world"}

View file

@ -1,9 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
One: multivalue1
One: multivalue2
Content-Type: application/json
Content-Length: 18
{"hello": "world"}

View file

@ -1,7 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Length: 18
{"hello": "world"}

View file

@ -1,10 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
One: value1
Two: value2
Three: value2
Content-Type: application/json
Content-Length: 18
{"hello": "world"}

View file

@ -1,8 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Content-Length: 18
{"hello": "world"}

View file

@ -1,7 +0,0 @@
HTTP/1.1 200 OK
Date: Tue, 20 Apr 2021 02:07:56 GMT
Content-Type: application/json
Content-Digest: sha-512=:mEWXIS7MaLRuGgxOBdODa3xqM1XdEvxoYhvlCFJ41QJgJc4GTsPp29l5oGX69wWdXymyU0rjJuahq4l5aGgfLQ==:
Content-Length: 23
{"message": "good dog"}

View file

@ -1,5 +0,0 @@
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIFKbhfNZfpDsW43+0+JjUr9K+bTeuxopu653+hBaXGA7oAoGCCqGSM49
AwEHoUQDQgAEqIVYZVLCrPZHGHjP17CTW0/+D9Lfw0EkjqF7xB4FivAxzic30tMM
4GF+hR6Dxh71Z50VGGdldkkDXZCnTNnoXQ==
-----END EC PRIVATE KEY-----

View file

@ -1,4 +0,0 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqIVYZVLCrPZHGHjP17CTW0/+D9Lf
w0EkjqF7xB4FivAxzic30tMM4GF+hR6Dxh71Z50VGGdldkkDXZCnTNnoXQ==
-----END PUBLIC KEY-----

View file

@ -1,6 +0,0 @@
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDBlRoIHTHCg2hgrwD5cQMl1aGP8ZZ/ulSbHCDHHIC4ENlQIdjLDhOBG
JO2ZMKJK33WgBwYFK4EEACKhZANiAAS46EhdhNcOPg3ZKMhlryAFfy6eKt3M3f+w
31ikDCbu10GLZjvCoevMvSY+TQIL9EQlAHRxcJ5ciXt1ukiH2zo1NpR7S8ozmvCV
t/IvcUiS9XS1nRiHKkM1e8NMNtg3avo=
-----END EC PRIVATE KEY-----

View file

@ -1,5 +0,0 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEuOhIXYTXDj4N2SjIZa8gBX8unirdzN3/
sN9YpAwm7tdBi2Y7wqHrzL0mPk0CC/REJQB0cXCeXIl7dbpIh9s6NTaUe0vKM5rw
lbfyL3FIkvV0tZ0YhypDNXvDTDbYN2r6
-----END PUBLIC KEY-----

View file

@ -1,3 +0,0 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF
-----END PRIVATE KEY-----

View file

@ -1,3 +0,0 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=
-----END PUBLIC KEY-----

View file

@ -1,4 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADALBgkqhkiG9w0BAQoEggSqMIIEpgIBAAKCAQEAr4tmm3r20Wd/Pbqv P1s2+QEtvpuRaV8Yq40gjUR8y2Rjxa6dpG2GXHbPfvMs8ct+Lh1GH45x28Rw3Ry5 3mm+oAXjyQ86OnDkZ5N8lYbggD4O3w6M6pAvLkhk95AndTrifbIFPNU8PPMO7Oyr FAHqgDsznjPFmTOtCEcN2Z1FpWgchwuYLPL+Wokqltd11nqqzi+bJ9cvSKADYdUA AN5WUtzdpiy6LbTgSxP7ociU4Tn0g5I6aDZJ7A8Lzo0KSyZYoA485mqcO0GVAdVw 9lq4aOT9v6d+nb4bnNkQVklLQ3fVAvJm+xdDOp9LCNCN48V2pnDOkFV6+U9nV5oy c6XI2wIDAQABAoIBAQCUB8ip+kJiiZVKF8AqfB/aUP0jTAqOQewK1kKJ/iQCXBCq pbo360gvdt05H5VZ/RDVkEgO2k73VSsbulqezKs8RFs2tEmU+JgTI9MeQJPWcP6X aKy6LIYs0E2cWgp8GADgoBs8llBq0UhX0KffglIeek3n7Z6Gt4YFge2TAcW2WbN4 XfK7lupFyo6HHyWRiYHMMARQXLJeOSdTn5aMBP0PO4bQyk5ORxTUSeOciPJUFktQ HkvGbym7KryEfwH8Tks0L7WhzyP60PL3xS9FNOJi9m+zztwYIXGDQuKM2GDsITeD 2mI2oHoPMyAD0wdI7BwSVW18p1h+jgfc4dlexKYRAoGBAOVfuiEiOchGghV5vn5N RDNscAFnpHj1QgMr6/UG05RTgmcLfVsI1I4bSkbrIuVKviGGf7atlkROALOG/xRx DLadgBEeNyHL5lz6ihQaFJLVQ0u3U4SB67J0YtVO3R6lXcIjBDHuY8SjYJ7Ci6Z6 vuDcoaEujnlrtUhaMxvSfcUJAoGBAMPsCHXte1uWNAqYad2WdLjPDlKtQJK1diCm rqmB2g8QE99hDOHItjDBEdpyFBKOIP+NpVtM2KLhRajjcL9Ph8jrID6XUqikQuVi 4J9FV2m42jXMuioTT13idAILanYg8D3idvy/3isDVkON0X3UAVKrgMEne0hJpkPL FYqgetvDAoGBAKLQ6JZMbSe0pPIJkSamQhsehgL5Rs51iX4m1z7+sYFAJfhvN3Q/ OGIHDRp6HjMUcxHpHw7U+S1TETxePwKLnLKj6hw8jnX2/nZRgWHzgVcY+sPsReRx NJVf+Cfh6yOtznfX00p+JWOXdSY8glSSHJwRAMog+hFGW1AYdt7w80XBAoGBAImR NUugqapgaEA8TrFxkJmngXYaAqpA0iYRA7kv3S4QavPBUGtFJHBNULzitydkNtVZ 3w6hgce0h9YThTo/nKc+OZDZbgfN9s7cQ75x0PQCAO4fx2P91Q+mDzDUVTeG30mE t2m3S0dGe47JiJxifV9P3wNBNrZGSIF3mrORBVNDAoGBAI0QKn2Iv7Sgo4T/XjND dl2kZTXqGAk8dOhpUiw/HdM3OGWbhHj2NdCzBliOmPyQtAr770GITWvbAI+IRYyF S7Fnk6ZVVVHsxjtaHy1uJGFlaZzKR4AGNaUTOJMs6NadzCmGPAxNQQOCqoUjn4XR
rOjr9w349JooGXhOxbu8nOxX
-----END PRIVATE KEY-----

View file

@ -1,4 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr4tmm3r20Wd/PbqvP1s2 +QEtvpuRaV8Yq40gjUR8y2Rjxa6dpG2GXHbPfvMs8ct+Lh1GH45x28Rw3Ry53mm+ oAXjyQ86OnDkZ5N8lYbggD4O3w6M6pAvLkhk95AndTrifbIFPNU8PPMO7OyrFAHq gDsznjPFmTOtCEcN2Z1FpWgchwuYLPL+Wokqltd11nqqzi+bJ9cvSKADYdUAAN5W Utzdpiy6LbTgSxP7ociU4Tn0g5I6aDZJ7A8Lzo0KSyZYoA485mqcO0GVAdVw9lq4 aOT9v6d+nb4bnNkQVklLQ3fVAvJm+xdDOp9LCNCN48V2pnDOkFV6+U9nV5oyc6XI
2wIDAQAB
-----END PUBLIC KEY-----

View file

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEqAIBAAKCAQEAhAKYdtoeoy8zcAcR874L8cnZxKzAGwd7v36APp7Pv6Q2jdsP
BRrwWEBnez6d0UDKDwGbc6nxfEXAy5mbhgajzrw3MOEt8uA5txSKobBpKDeBLOsd
JKFqMGmXCQvEG7YemcxDTRPxAleIAgYYRjTSd/QBwVW9OwNFhekro3RtlinV0a75
jfZgkne/YiktSvLG34lw2zqXBDTC5NHROUqGTlML4PlNZS5Ri2U4aCNx2rUPRcKI
lE0PuKxI4T+HIaFpv8+rdV6eUgOrB2xeI1dSFFn/nnv5OoZJEIB+VmuKn3DCUcCZ
SFlQPSXSfBDiUGhwOw76WuSSsf1D4b/vLoJ10wIDAQABAoIBAG/JZuSWdoVHbi56
vjgCgkjg3lkO1KrO3nrdm6nrgA9P9qaPjxuKoWaKO1cBQlE1pSWp/cKncYgD5WxE
CpAnRUXG2pG4zdkzCYzAh1i+c34L6oZoHsirK6oNcEnHveydfzJL5934egm6p8DW
+m1RQ70yUt4uRc0YSor+q1LGJvGQHReF0WmJBZHrhz5e63Pq7lE0gIwuBqL8SMaA
yRXtK+JGxZpImTq+NHvEWWCu09SCq0r838ceQI55SvzmTkwqtC+8AT2zFviMZkKR
Qo6SPsrqItxZWRty2izawTF0Bf5S2VAx7O+6t3wBsQ1sLptoSgX3QblELY5asI0J
YFz7LJECgYkAsqeUJmqXE3LP8tYoIjMIAKiTm9o6psPlc8CrLI9CH0UbuaA2JCOM
cCNq8SyYbTqgnWlB9ZfcAm/cFpA8tYci9m5vYK8HNxQr+8FS3Qo8N9RJ8d0U5Csw
DzMYfRghAfUGwmlWj5hp1pQzAuhwbOXFtxKHVsMPhz1IBtF9Y8jvgqgYHLbmyiu1
mwJ5AL0pYF0G7x81prlARURwHo0Yf52kEw1dxpx+JXER7hQRWQki5/NsUEtv+8RT
qn2m6qte5DXLyn83b1qRscSdnCCwKtKWUug5q2ZbwVOCJCtmRwmnP131lWRYfj67
B/xJ1ZA6X3GEf4sNReNAtaucPEelgR2nsN0gKQKBiGoqHWbK1qYvBxX2X3kbPDkv
9C+celgZd2PW7aGYLCHq7nPbmfDV0yHcWjOhXZ8jRMjmANVR/eLQ2EfsRLdW69bn
f3ZD7JS1fwGnO3exGmHO3HZG+6AvberKYVYNHahNFEw5TsAcQWDLRpkGybBcxqZo
81YCqlqidwfeO5YtlO7etx1xLyqa2NsCeG9A86UjG+aeNnXEIDk1PDK+EuiThIUa
/2IxKzJKWl1BKr2d4xAfR0ZnEYuRrbeDQYgTImOlfW6/GuYIxKYgEKCFHFqJATAG
IxHrq1PDOiSwXd2GmVVYyEmhZnbcp8CxaEMQoevxAta0ssMK3w6UsDtvUvYvF22m
qQKBiD5GwESzsFPy3Ga0MvZpn3D6EJQLgsnrtUPZx+z2Ep2x0xc5orneB5fGyF1P
WtP+fG5Q6Dpdz3LRfm+KwBCWFKQjg7uTxcjerhBWEYPmEMKYwTJF5PBG9/ddvHLQ
EQeNC8fHGg4UXU8mhHnSBt3EA10qQJfRDs15M38eG2cYwB1PZpDHScDnDA0=
-----END RSA PRIVATE KEY-----

View file

@ -1,8 +0,0 @@
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAhAKYdtoeoy8zcAcR874L8cnZxKzAGwd7v36APp7Pv6Q2jdsPBRrw
WEBnez6d0UDKDwGbc6nxfEXAy5mbhgajzrw3MOEt8uA5txSKobBpKDeBLOsdJKFq
MGmXCQvEG7YemcxDTRPxAleIAgYYRjTSd/QBwVW9OwNFhekro3RtlinV0a75jfZg
kne/YiktSvLG34lw2zqXBDTC5NHROUqGTlML4PlNZS5Ri2U4aCNx2rUPRcKIlE0P
uKxI4T+HIaFpv8+rdV6eUgOrB2xeI1dSFFn/nnv5OoZJEIB+VmuKn3DCUcCZSFlQ
PSXSfBDiUGhwOw76WuSSsf1D4b/vLoJ10wIDAQAB
-----END RSA PUBLIC KEY-----

View file

@ -1 +0,0 @@
uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==

View file

@ -1,10 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Content-Length: 18
Signature-Input: sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"
Signature: sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:
{"hello": "world"}

View file

@ -1,10 +0,0 @@
POST /foo?param=Value&Pet=dog HTTP/1.1
Host: example.com
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
Content-Length: 18
Signature-Input: sig-b21=();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd",bad-sig=("@authority");created=1618884473;keyid="test-key-rsa-pss"
Signature: sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:,bad-sig=:pxcQw6G3AjtMBQjwo8XzkZf/bws5LelbaMk5rGIGtE8=:
{"hello": "world"}

View file

@ -1,476 +0,0 @@
package httpsig
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/hmac"
"crypto/rsa"
"crypto/sha256"
"crypto/sha512"
"fmt"
"math/big"
"net/http"
"slices"
"time"
sfv "github.com/dunglas/httpsfv"
)
var (
DefaultVerifyProfile = VerifyProfile{
SignatureLabel: DefaultSignatureLabel,
AllowedAlgorithms: []Algorithm{Algo_ECDSA_P256_SHA256, Algo_ECDSA_P384_SHA384, Algo_ED25519, Algo_HMAC_SHA256},
RequiredFields: DefaultRequiredFields,
RequiredMetadata: []Metadata{MetaCreated, MetaKeyID},
DisallowedMetadata: []Metadata{MetaAlgorithm}, // The algorithm should be looked up from the keyid not an explicit setting.
CreatedValidDuration: time.Minute * 5, // Signatures must have been created within within the last 5 minutes
DateFieldSkew: time.Minute, // If the created parameter is present, the Date header cannot be more than a minute off.
}
// DefaultRequiredFields covers the request body with 'content-digest' the method and full URI.
// As per the specification Date is not covered in favor of using the 'created' metadata parameter.
DefaultRequiredFields = Fields("content-digest", "@method", "@target-uri")
ctxKeyAddDebug = struct{}{}
)
// KeySpec is the per-key information needed to verify a signature.
type KeySpec struct {
KeyID string
Algo Algorithm
PubKey crypto.PublicKey
Secret []byte // shared secret for symmetric algorithms
}
// KeySpec implements KeySpecer
func (ks KeySpec) KeySpec() (KeySpec, error) {
return ks, nil
}
// KeySpecer should be implemented by your key/credential store
type KeySpecer interface {
KeySpec() (KeySpec, error)
}
type KeyErrorReason string
type KeyError struct {
error
Reason KeyErrorReason
Message string
}
type KeyFetcher interface {
// FetchByKeyID looks up a KeySpec from the 'keyid' metadata parameter on the signature.
FetchByKeyID(ctx context.Context, rh http.Header, keyID string) (KeySpecer, error)
// Fetch looks up a KeySpec when the keyid is not in the signature.
Fetch(ctx context.Context, rh http.Header, md MetadataProvider) (KeySpecer, error)
}
// VerifyProfile sets the parameters for a fully valid request or response.
// A valid signature is a relatively easy accomplishment. Did the signature include all the important parts of the request? Did it use a strong enough algorithm? Was it signed 41 days ago? There are choices to make about what constitutes a valid signed request or response beyond just a verified signature.
type VerifyProfile struct {
SignatureLabel string // Which signature this profile applies to. '*' applies to all
RequiredFields []SignedField
RequiredMetadata []Metadata
DisallowedMetadata []Metadata
AllowedAlgorithms []Algorithm // Which algorithms are allowed, either from keyid meta or in the KeySpec
// Timing enforcement options
DisableTimeEnforcement bool // If true do no time enforcement on any fields
DisableExpirationEnforcement bool // If expiration is present default to enforce
CreatedValidDuration time.Duration // Duration allowed for between time.Now and the created time
ExpiredSkew time.Duration // Maximum duration allowed between time.Now and the expired time
DateFieldSkew time.Duration // Maximum duration allowed between Date field and created
}
type VerifyResult struct {
Verified bool
Label string
KeySpecer KeySpecer
DebugInfo VerifyDebugInfo // Present if the verifier debug flag is set and the signature was valid.
MetadataProvider
}
type VerifyDebugInfo struct {
SignatureBase string // The signature base derived from the request.
}
type Verifier struct {
keys KeyFetcher
profile VerifyProfile
}
// Verify validates the signatures in a request and ensured the signature meets the required profile.
func Verify(req *http.Request, kf KeyFetcher, profile VerifyProfile) (VerifyResult, error) {
ver, err := NewVerifier(kf, profile)
if err != nil {
return VerifyResult{}, err
}
return ver.Verify(req)
}
func VerifyResponse(resp *http.Response, kf KeyFetcher, profile VerifyProfile) (VerifyResult, error) {
ver, err := NewVerifier(kf, profile)
if err != nil {
return VerifyResult{}, err
}
return ver.VerifyResponse(resp)
}
func NewVerifier(kf KeyFetcher, profile VerifyProfile) (*Verifier, error) {
if kf == nil {
return nil, newError(ErrSigKeyFetch, "KeyFetcher cannot be nil")
}
return &Verifier{
keys: kf,
profile: profile,
}, nil
}
// Verify verifies the signature(s) in an http request. Any invalid signature will return an error.
// A valid VerifyResult is returned even if error is also returned.
func (ver *Verifier) Verify(req *http.Request) (VerifyResult, error) {
return ver.verify(httpMessage{
Req: req,
})
}
func (ver *Verifier) VerifyResponse(resp *http.Response) (VerifyResult, error) {
return ver.verify(httpMessage{
IsResponse: true,
Resp: resp,
})
}
// verify verifies the request or response.
func (ver *Verifier) verify(hrr httpMessage) (VerifyResult, error) {
vr := VerifyResult{}
/* calculate content digest if needed */
if hrr.Headers().Get("Content-Digest") != "" {
digestAlgo, expectedDigest, err := getSupportedDigestFromHeader(hrr.Headers().Values("Content-Digest"))
if err != nil {
return vr, err
}
di, err := digestBody(digestAlgo, hrr.Body())
if err != nil {
return vr, err
}
hrr.SetBody(di.NewBody)
if !bytes.Equal(expectedDigest, di.Digest) {
return vr, newError(ErrNoSigWrongDigest, "Digest does not match")
}
}
/* parse and extract the signature */
sigsfv, err := parseSignaturesFromRequest(hrr.Headers())
if err != nil {
return vr, err
}
sig, err := unmarshalSignature(sigsfv, ver.profile.SignatureLabel)
if err != nil {
return vr, err
}
vr.Label = sig.Label
vr.MetadataProvider = sig.Input.MetadataValues
/* verify and validate */
base, err := calculateSignatureBase(hrr, sig.Input)
if err != nil {
return vr, err
}
if hrr.isDebug() {
vr.DebugInfo = VerifyDebugInfo{
SignatureBase: string(base.base),
}
}
keyspec, err := ver.verifySignature(hrr, sig, base)
vr.KeySpecer = keyspec
if err != nil {
return vr, err
}
if err := ver.profile.validate(sig); err != nil {
return vr, err
}
return VerifyResult{
Verified: true,
Label: sig.Label,
KeySpecer: keyspec,
MetadataProvider: sig.Input.MetadataValues,
DebugInfo: vr.DebugInfo,
}, nil
}
type extractedSignature struct {
Label string
Signature []byte
Input sigBaseInput
}
// signaturesSFV is the structured field value representation of all the signatures of a request.
type signaturesSFV struct {
SigInputs *sfv.Dictionary
Sigs *sfv.Dictionary
}
func parseSignaturesFromRequest(headers http.Header) (signaturesSFV, error) {
psd := signaturesSFV{}
/* Pull signature and signature-input header */
sigHeader := headers.Get("signature")
if sigHeader == "" {
return psd, newError(ErrNoSigMissingSignature, "Missing signature header")
}
sigInputHeader := headers.Get("signature-input")
if sigInputHeader == "" {
return psd, newError(ErrNoSigMissingSignature, "Missing signature-input header")
}
/* Parse headers into their appropriate HTTP structured field values */
// signature-input must be a HTTP structured field value of type Dictionary.
var err error
psd.SigInputs, err = sfv.UnmarshalDictionary([]string{sigInputHeader})
if err != nil {
return psd, newError(ErrNoSigInvalidSignature, "Invalid signature-input header. Not a valid Dictionary", err)
}
// signature must be a HTTP structured field value of type Dictionary.
psd.Sigs, err = sfv.UnmarshalDictionary([]string{sigHeader})
if err != nil {
return psd, newError(ErrNoSigInvalidSignature, "Invalid signature header. Not a valid Dictionary", err)
}
return psd, nil
}
// unmarshalSignature unmarshals a signature from hhtp structured field value (sfv) format.
func unmarshalSignature(sigs signaturesSFV, label string) (extractedSignature, error) {
sigInfo := extractedSignature{
Label: label,
}
sigMember, found := sigs.Sigs.Get(label)
if !found {
return sigInfo, newError(ErrNoSigMissingSignature, fmt.Sprintf("The signature for label '%s' not found", label))
}
sigInputMember, found := sigs.SigInputs.Get(label)
if !found {
return sigInfo, newError(ErrNoSigMissingSignature, fmt.Sprintf("The signature-input for label '%s' not found", label))
}
// The signature must be of sfv type 'Item'
sigItem, isItem := sigMember.(sfv.Item)
if !isItem {
return sigInfo, newError(ErrSigInvalidSignature, fmt.Sprintf("The signature for label '%s' must be type Item. It was type %T", label, sigMember))
}
// Signatures must be byte sequences. The sfv library uses []byte for byte sequences.
sigBytes, isByteSequence := sigItem.Value.([]byte)
if !isByteSequence {
return sigInfo, newError(ErrSigInvalidSignature, fmt.Sprintf("The signature for label '%s' was not a byte sequence. It was type %T", label, sigItem.Value))
}
sigInfo.Signature = sigBytes
// The signature input must be of sfv type InnerList
sigInputList, isList := sigInputMember.(sfv.InnerList)
if !isList {
return sigInfo, newError(ErrSigInvalidSignature, fmt.Sprintf("The signature-input for label '%s' must be type InnerList. It was type '%T'.", label, sigInputMember))
}
cIDs := []componentID{}
for _, componentItem := range sigInputList.Items {
name, ok := componentItem.Value.(string)
if !ok {
return sigInfo, newError(ErrSigInvalidSignature, fmt.Sprintf("signature components must be string types"))
}
cIDs = append(cIDs, componentID{
Name: name,
Item: componentItem,
})
}
mds := []Metadata{}
for _, name := range sigInputList.Params.Names() {
mds = append(mds, Metadata(name))
}
sigInfo.Input = sigBaseInput{
Components: cIDs,
MetadataParams: mds,
MetadataValues: metadataProviderFromParams{sigInputList.Params},
}
return sigInfo, nil
}
func (ver *Verifier) verifySignature(r httpMessage, sig extractedSignature, base signatureBase) (KeySpecer, error) {
var specer KeySpecer
var ks KeySpec
var err error
// Get keyspec
if slices.Contains(sig.Input.MetadataParams, MetaKeyID) {
keyid, err := sig.Input.MetadataValues.KeyID()
if err != nil {
return nil, newError(ErrSigKeyFetch, "Could not get keyid from signature metadata", err)
}
specer, err = ver.keys.FetchByKeyID(r.Context(), r.Headers(), keyid)
if err != nil {
return nil, newError(ErrSigKeyFetch, fmt.Sprintf("Failed to fetch key for keyid '%s'", keyid), err)
}
ks, err = specer.KeySpec()
if err != nil {
return nil, newError(ErrSigKeyFetch, fmt.Sprintf("Failed to fetch key for keyid '%s'", keyid), err)
}
} else {
specer, err = ver.keys.Fetch(r.Context(), r.Headers(), sig.Input.MetadataValues)
if err != nil {
return specer, newError(ErrSigKeyFetch, fmt.Sprintf("Failed to fetch key for signature without a keyid and with label '%s'\n", sig.Label), err)
}
ks, err = specer.KeySpec()
if err != nil {
return specer, newError(ErrSigKeyFetch, fmt.Sprintf("Failed to fetch key for signature without a keyid and with label '%s'\n", sig.Label), err)
}
}
switch ks.Algo {
case Algo_RSA_PSS_SHA512:
if rsapub, ok := ks.PubKey.(*rsa.PublicKey); ok {
opts := &rsa.PSSOptions{
SaltLength: 64,
Hash: crypto.SHA512,
}
msgHash := sha512.Sum512(base.base)
err := rsa.VerifyPSS(rsapub, crypto.SHA512, msgHash[:], sig.Signature, opts)
if err != nil {
return specer, newError(ErrSigVerification, fmt.Sprintf("Signature did not verify for algo '%s'", ks.Algo), err)
}
return specer, nil
} else {
return specer, newError(ErrSigPublicKey, fmt.Sprintf("Invalid public key. Requires rsa.PublicKey but got type: %T", ks.PubKey))
}
case Algo_RSA_v1_5_sha256:
if rsapub, ok := ks.PubKey.(*rsa.PublicKey); ok {
msgHash := sha256.Sum256(base.base)
err := rsa.VerifyPKCS1v15(rsapub, crypto.SHA256, msgHash[:], sig.Signature)
if err != nil {
return specer, newError(ErrSigVerification, fmt.Sprintf("Signature did not verify for algo '%s'", ks.Algo), err)
}
return specer, nil
} else {
return specer, newError(ErrSigPublicKey, fmt.Sprintf("Invalid public key. Requires rsa.PublicKey but got type: %T", ks.PubKey))
}
case Algo_HMAC_SHA256:
if len(ks.Secret) == 0 {
return specer, newError(ErrInvalidSignatureOptions, fmt.Sprintf("No secret provided for symmetric algorithm '%s'", Algo_HMAC_SHA256))
}
msgHash := hmac.New(sha256.New, ks.Secret)
msgHash.Write(base.base) // write does not return an error per hash.Hash documentation
calcualtedSignature := msgHash.Sum(nil)
if !hmac.Equal(calcualtedSignature, sig.Signature) {
return specer, newError(ErrSigVerification, fmt.Sprintf("Signature did not verify for algo '%s'", ks.Algo), err)
}
case Algo_ECDSA_P256_SHA256:
if epub, ok := ks.PubKey.(*ecdsa.PublicKey); ok {
if len(sig.Signature) != 64 {
return specer, newError(ErrSigInvalidSignature, fmt.Sprintf("Signature must be 64 bytes for algorithm '%s'", Algo_ECDSA_P256_SHA256))
}
msgHash := sha256.Sum256(base.base)
// Concatenate r and s to form the signature as per the spec. r and s and *not* ANS1 encoded.
r := new(big.Int)
r.SetBytes(sig.Signature[0:32])
s := new(big.Int)
s.SetBytes(sig.Signature[32:64])
if !ecdsa.Verify(epub, msgHash[:], r, s) {
return specer, newError(ErrSigVerification, fmt.Sprintf("Signature did not verify for algo '%s'", ks.Algo), err)
}
} else {
return specer, newError(ErrSigPublicKey, fmt.Sprintf("Invalid public key. Requires *ecdsa.PublicKey but got type: %T", ks.PubKey))
}
case Algo_ECDSA_P384_SHA384:
if epub, ok := ks.PubKey.(*ecdsa.PublicKey); ok {
if len(sig.Signature) != 96 {
return specer, newError(ErrSigInvalidSignature, fmt.Sprintf("Signature must be 96 bytes for algorithm '%s'", Algo_ECDSA_P256_SHA256))
}
msgHash := sha512.Sum384(base.base)
// Concatenate r and s to form the signature as per the spec. r and s and *not* ANS1 encoded.
r := new(big.Int)
r.SetBytes(sig.Signature[0:48])
s := new(big.Int)
s.SetBytes(sig.Signature[48:96])
if !ecdsa.Verify(epub, msgHash[:], r, s) {
return specer, newError(ErrSigVerification, fmt.Sprintf("Signature did not verify for algo '%s'", ks.Algo), err)
}
} else {
return specer, newError(ErrSigPublicKey, fmt.Sprintf("Invalid public key. Requires *ecdsa.PublicKey but got type: %T", ks.PubKey))
}
case Algo_ED25519:
if edpubkey, ok := ks.PubKey.(ed25519.PublicKey); ok {
if !ed25519.Verify(edpubkey, base.base, sig.Signature) {
return specer, newError(ErrSigVerification, fmt.Sprintf("Signature did not verify for algo '%s'", ks.Algo), err)
}
} else {
return specer, newError(ErrSigPublicKey, fmt.Sprintf("Invalid public key. Requires ed25519.PublicKey but got type: %T", ks.PubKey))
}
default:
return specer, newError(ErrSigUnsupportedAlgorithm, fmt.Sprintf("Invalid verification algorithm '%s'", ks.Algo))
}
return specer, nil
}
func (vp VerifyProfile) validate(sig extractedSignature) error {
return nil
}
type metadataProviderFromParams struct {
Params *sfv.Params
}
func (mp metadataProviderFromParams) Created() (int, error) {
if val, ok := mp.Params.Get(string(MetaCreated)); ok {
return int(val.(int64)), nil
}
return 0, fmt.Errorf("No created value")
}
func (mp metadataProviderFromParams) Expires() (int, error) {
if val, ok := mp.Params.Get(string(MetaExpires)); ok {
return int(val.(int64)), nil
}
return 0, fmt.Errorf("No expires value")
}
func (mp metadataProviderFromParams) Nonce() (string, error) {
if val, ok := mp.Params.Get(string(MetaNonce)); ok {
return val.(string), nil
}
return "", fmt.Errorf("No nonce value")
}
func (mp metadataProviderFromParams) Alg() (string, error) {
if val, ok := mp.Params.Get(string(MetaAlgorithm)); ok {
return val.(string), nil
}
return "", fmt.Errorf("No alg value")
}
func (mp metadataProviderFromParams) KeyID() (string, error) {
if val, ok := mp.Params.Get(string(MetaKeyID)); ok {
return val.(string), nil
}
return "", fmt.Errorf("No keyid value")
}
func (mp metadataProviderFromParams) Tag() (string, error) {
if val, ok := mp.Params.Get(string(MetaTag)); ok {
return val.(string), nil
}
return "", fmt.Errorf("No tag value")
}
func SetAddDebugInfo(ctx context.Context) context.Context {
return context.WithValue(ctx, ctxKeyAddDebug, true)
}

View file

@ -1,233 +0,0 @@
package httpsig_test
import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/remitly-oss/httpsig-go"
"github.com/remitly-oss/httpsig-go/keyman"
"github.com/remitly-oss/httpsig-go/keyutil"
"github.com/remitly-oss/httpsig-go/sigtest"
)
func TestVerify(t *testing.T) {
testcases := []struct {
Name string
RequestFile string
Label string
AddDebugInfo bool
Keys httpsig.KeyFetcher
Expected httpsig.VerifyResult
}{
{
Name: "OneValid",
Label: "sig-b21",
RequestFile: "verify_request1.txt",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-rsa-pss": {
KeyID: "test-key-rsa-pss",
Algo: httpsig.Algo_RSA_PSS_SHA512,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
}),
Expected: httpsig.VerifyResult{
Verified: true,
Label: "sig-b21",
MetadataProvider: &fixedMetadataProvider{map[httpsig.Metadata]any{
httpsig.MetaKeyID: "test-key-rsa-pss",
httpsig.MetaCreated: int64(1618884473),
httpsig.MetaNonce: "b3k2pp5k7z-50gnwp.yemd",
}},
KeySpecer: httpsig.KeySpec{
KeyID: "test-key-rsa-pss",
Algo: httpsig.Algo_RSA_PSS_SHA512,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
},
},
{
Name: "OneValidDebug",
Label: "sig-b21",
RequestFile: "verify_request1.txt",
AddDebugInfo: true,
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-rsa-pss": {
KeyID: "test-key-rsa-pss",
Algo: httpsig.Algo_RSA_PSS_SHA512,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
}),
Expected: httpsig.VerifyResult{
Verified: true,
Label: "sig-b21",
MetadataProvider: &fixedMetadataProvider{map[httpsig.Metadata]any{
httpsig.MetaKeyID: "test-key-rsa-pss",
httpsig.MetaCreated: int64(1618884473),
httpsig.MetaNonce: "b3k2pp5k7z-50gnwp.yemd",
}},
KeySpecer: httpsig.KeySpec{
KeyID: "test-key-rsa-pss",
Algo: httpsig.Algo_RSA_PSS_SHA512,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
DebugInfo: httpsig.VerifyDebugInfo{
SignatureBase: `"@signature-params": ();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"`,
},
},
},
}
for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
req := sigtest.ReadRequest(t, tc.RequestFile)
if tc.AddDebugInfo {
req = req.WithContext(httpsig.SetAddDebugInfo(req.Context()))
}
actual, err := httpsig.Verify(req, tc.Keys, httpsig.VerifyProfile{SignatureLabel: tc.Label})
if err != nil {
t.Fatal(err)
}
// VerifyResult is returned even when error is also returned.
// Because VerifryResult embed Metadataprovider we first need diff ignoring the MetadataProvider
sigtest.Diff(t, tc.Expected, actual, "Did not match",
cmp.FilterPath(func(p cmp.Path) bool {
return p.String() == "MetadataProvider"
}, cmp.Ignore()))
// Then diff the metadata provider
sigtest.Diff(t, tc.Expected, actual, "Did not match", getCmdOpts()...)
})
}
}
func TestVerifyInvalid(t *testing.T) {
testcases := []struct {
Name string
RequestFile string
Label string
Keys httpsig.KeyFetcher
Expected httpsig.ErrCode
}{
{
Name: "SignatureVerificationFailure",
RequestFile: "verify_request2.txt",
Label: "bad-sig",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{
"test-key-rsa-pss": {
KeyID: "test-key-rsa-pss",
Algo: httpsig.Algo_RSA_PSS_SHA512,
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
}),
Expected: httpsig.ErrSigVerification,
},
{
Name: "KeyFetchError",
RequestFile: "verify_request2.txt",
Label: "sig-b21",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{}),
Expected: httpsig.ErrSigKeyFetch,
},
{
Name: "KeyFetchError2",
RequestFile: "verify_request2.txt",
Label: "bad-sig",
Keys: keyman.NewKeyFetchInMemory(map[string]httpsig.KeySpec{}),
Expected: httpsig.ErrSigKeyFetch,
},
}
for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
_, err := httpsig.Verify(sigtest.ReadRequest(t, tc.RequestFile), tc.Keys, httpsig.VerifyProfile{SignatureLabel: tc.Label})
if err == nil {
t.Fatal("Expected err")
}
if sigerr, ok := err.(*httpsig.SignatureError); ok {
sigtest.Diff(t, tc.Expected, sigerr.Code, "Did not match")
} else {
sigtest.Diff(t, tc.Expected, sigerr, "Did not match")
}
})
}
}
type fixedMetadataProvider struct {
values map[httpsig.Metadata]any
}
func (fmp fixedMetadataProvider) Created() (int, error) {
if val, ok := fmp.values[httpsig.MetaCreated]; ok {
return int(val.(int64)), nil
}
return 0, fmt.Errorf("No created value")
}
func (fmp fixedMetadataProvider) Expires() (int, error) {
if val, ok := fmp.values[httpsig.MetaExpires]; ok {
return int(val.(int64)), nil
}
return 0, fmt.Errorf("No expires value")
}
func (fmp fixedMetadataProvider) Nonce() (string, error) {
if val, ok := fmp.values[httpsig.MetaNonce]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No nonce value")
}
func (fmp fixedMetadataProvider) Alg() (string, error) {
if val, ok := fmp.values[httpsig.MetaAlgorithm]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No alg value")
}
func (fmp fixedMetadataProvider) KeyID() (string, error) {
if val, ok := fmp.values[httpsig.MetaKeyID]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No keyid value")
}
func (fmp fixedMetadataProvider) Tag() (string, error) {
if val, ok := fmp.values[httpsig.MetaTag]; ok {
return val.(string), nil
}
return "", fmt.Errorf("No tag value")
}
func metaVal[E comparable](f1 func() (E, error)) any {
val, err := f1()
if err != nil {
return err.Error()
}
return val
}
func getCmdOpts() []cmp.Option {
return []cmp.Option{
// This gets used for *ANY* struct assignable to MetadataProvider including other structres
// that embed it!
cmp.Transformer("MetadataProvider", TransformMeta),
}
}
func TransformMeta(md httpsig.MetadataProvider) map[string]any {
out := map[string]any{}
if md == nil {
return out
}
out[string(httpsig.MetaCreated)] = metaVal(md.Created)
out[string(httpsig.MetaExpires)] = metaVal(md.Expires)
out[string(httpsig.MetaNonce)] = metaVal(md.Nonce)
out[string(httpsig.MetaAlgorithm)] = metaVal(md.Alg)
out[string(httpsig.MetaKeyID)] = metaVal(md.KeyID)
out[string(httpsig.MetaTag)] = metaVal(md.Tag)
return out
}