mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Added a vendored version of httpsig-go. (#30820)
For #30473 This change adds a vendored `httpsig-go` library to our repo. We cannot use the upstream library because it has not merged the change we need: https://github.com/remitly-oss/httpsig-go/pull/25 Thus, we need our own copy at this point. The instructions for keeping this library up to date (if needed) are in `UPDATE_INSTRUCTIONS`. None of the coderabbitai review comments are relevant to the code/features we are going to use for HTTP message signatures. We will use this library in subsequent PRs for the TPM-backed HTTP message signature feature. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a Go library for HTTP message signing and verification, supporting multiple cryptographic algorithms (RSA, ECDSA, Ed25519, HMAC). * Added utilities for key management, including JWK and PEM key handling. * Provided HTTP client and server helpers for automatic request signing and signature verification. * Implemented structured error handling and metadata extraction for signatures. * **Documentation** * Added comprehensive README, usage examples, and update instructions. * Included license and configuration files for third-party and testing tools. * **Tests** * Added extensive unit, integration, and fuzz tests covering signing, verification, and key handling. * Included official RFC test vectors and various test data files for robust validation. * **Chores** * Integrated continuous integration workflows and ignore files for code quality and security analysis. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
1528da68f4
commit
c25fed2492
75 changed files with 4494 additions and 0 deletions
84
third_party/httpsig-go/.github/workflows/codeql.yml
vendored
Normal file
84
third_party/httpsig-go/.github/workflows/codeql.yml
vendored
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# 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}}"
|
||||
14
third_party/httpsig-go/.github/workflows/wf.yml
vendored
Normal file
14
third_party/httpsig-go/.github/workflows/wf.yml
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
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 ./...
|
||||
1
third_party/httpsig-go/.semgrepignore
vendored
Normal file
1
third_party/httpsig-go/.semgrepignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
testdata/*.key
|
||||
49
third_party/httpsig-go/.sideignore
vendored
Normal file
49
third_party/httpsig-go/.sideignore
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# 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/
|
||||
21
third_party/httpsig-go/LICENSE
vendored
Normal file
21
third_party/httpsig-go/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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.
|
||||
65
third_party/httpsig-go/README.md
vendored
Normal file
65
third_party/httpsig-go/README.md
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# HTTP Message Signatures
|
||||
|
||||
[](https://pkg.go.dev/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/)
|
||||
42
third_party/httpsig-go/UPDATE_INSTRUCTIONS
vendored
Normal file
42
third_party/httpsig-go/UPDATE_INSTRUCTIONS
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
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"
|
||||
```
|
||||
1
third_party/httpsig-go/UPSTREAM_COMMIT
vendored
Normal file
1
third_party/httpsig-go/UPSTREAM_COMMIT
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
d68e2e99a37987076d8588fafe2aa34147abcc24
|
||||
68
third_party/httpsig-go/accept.go
vendored
Normal file
68
third_party/httpsig-go/accept.go
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
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
|
||||
|
||||
}
|
||||
64
third_party/httpsig-go/accept_test.go
vendored
Normal file
64
third_party/httpsig-go/accept_test.go
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
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")
|
||||
})
|
||||
}
|
||||
}
|
||||
248
third_party/httpsig-go/base.go
vendored
Normal file
248
third_party/httpsig-go/base.go
vendored
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
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)
|
||||
}
|
||||
123
third_party/httpsig-go/digest.go
vendored
Normal file
123
third_party/httpsig-go/digest.go
vendored
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
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
|
||||
}
|
||||
127
third_party/httpsig-go/digest_test.go
vendored
Normal file
127
third_party/httpsig-go/digest_test.go
vendored
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
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
|
||||
}
|
||||
104
third_party/httpsig-go/examples_test.go
vendored
Normal file
104
third_party/httpsig-go/examples_test.go
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
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")
|
||||
}
|
||||
200
third_party/httpsig-go/fz_test.go
vendored
Normal file
200
third_party/httpsig-go/fz_test.go
vendored
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
8
third_party/httpsig-go/go.mod
vendored
Normal file
8
third_party/httpsig-go/go.mod
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
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
|
||||
)
|
||||
4
third_party/httpsig-go/go.sum
vendored
Normal file
4
third_party/httpsig-go/go.sum
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
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=
|
||||
102
third_party/httpsig-go/http.go
vendored
Normal file
102
third_party/httpsig-go/http.go
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
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
|
||||
}
|
||||
34
third_party/httpsig-go/keyman/keyman.go
vendored
Normal file
34
third_party/httpsig-go/keyman/keyman.go
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// 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")
|
||||
}
|
||||
284
third_party/httpsig-go/keyutil/jwk.go
vendored
Normal file
284
third_party/httpsig-go/keyutil/jwk.go
vendored
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
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)
|
||||
}
|
||||
142
third_party/httpsig-go/keyutil/jwk_test.go
vendored
Normal file
142
third_party/httpsig-go/keyutil/jwk_test.go
vendored
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
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
|
||||
}
|
||||
143
third_party/httpsig-go/keyutil/keyutil.go
vendored
Normal file
143
third_party/httpsig-go/keyutil/keyutil.go
vendored
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
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)
|
||||
}
|
||||
8
third_party/httpsig-go/keyutil/testdata/test-jwk-ec.json
vendored
Normal file
8
third_party/httpsig-go/keyutil/testdata/test-jwk-ec.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"kty": "EC",
|
||||
"crv": "P-256",
|
||||
"kid": "test-key-ecc-p256",
|
||||
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
|
||||
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
|
||||
"d": "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE"
|
||||
}
|
||||
5
third_party/httpsig-go/keyutil/testdata/test-jwk-symmetric.json
vendored
Normal file
5
third_party/httpsig-go/keyutil/testdata/test-jwk-symmetric.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"kty": "oct",
|
||||
"kid": "test-symmetric-key",
|
||||
"k": "QYPe3dGeFrKhEEK4CSr9_nES-ExXI9Nw"
|
||||
}
|
||||
217
third_party/httpsig-go/roundtrip_test.go
vendored
Normal file
217
third_party/httpsig-go/roundtrip_test.go
vendored
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
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
|
||||
}
|
||||
5
third_party/httpsig-go/side.toml
vendored
Normal file
5
third_party/httpsig-go/side.toml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
max_iterations = 0
|
||||
max_planning_iterations = 0
|
||||
|
||||
[[test_commands]]
|
||||
command = "go test"
|
||||
89
third_party/httpsig-go/sigerrors.go
vendored
Normal file
89
third_party/httpsig-go/sigerrors.go
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
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 ""
|
||||
}
|
||||
332
third_party/httpsig-go/sign.go
vendored
Normal file
332
third_party/httpsig-go/sign.go
vendored
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
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
|
||||
}
|
||||
275
third_party/httpsig-go/sign_test.go
vendored
Normal file
275
third_party/httpsig-go/sign_test.go
vendored
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
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
|
||||
}
|
||||
172
third_party/httpsig-go/signatures.go
vendored
Normal file
172
third_party/httpsig-go/signatures.go
vendored
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
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())
|
||||
}
|
||||
85
third_party/httpsig-go/sigtest/helpers.go
vendored
Normal file
85
third_party/httpsig-go/sigtest/helpers.go
vendored
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
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
|
||||
}
|
||||
385
third_party/httpsig-go/spec_test.go
vendored
Normal file
385
third_party/httpsig-go/spec_test.go
vendored
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
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, "")
|
||||
})
|
||||
}
|
||||
}
|
||||
1
third_party/httpsig-go/testdata/b21_request_sigbase.txt
vendored
Normal file
1
third_party/httpsig-go/testdata/b21_request_sigbase.txt
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
"@signature-params": ();created=1618884473;keyid="test-key-rsa-pss";nonce="b3k2pp5k7z-50gnwp.yemd"
|
||||
10
third_party/httpsig-go/testdata/b21_request_signed.txt
vendored
Normal file
10
third_party/httpsig-go/testdata/b21_request_signed.txt
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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"}
|
||||
4
third_party/httpsig-go/testdata/b22_request_sigbase.txt
vendored
Normal file
4
third_party/httpsig-go/testdata/b22_request_sigbase.txt
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
"@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"
|
||||
10
third_party/httpsig-go/testdata/b22_request_signed.txt
vendored
Normal file
10
third_party/httpsig-go/testdata/b22_request_signed.txt
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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"}
|
||||
9
third_party/httpsig-go/testdata/b23_request_sigbase.txt
vendored
Normal file
9
third_party/httpsig-go/testdata/b23_request_sigbase.txt
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"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"
|
||||
10
third_party/httpsig-go/testdata/b23_request_signed.txt
vendored
Normal file
10
third_party/httpsig-go/testdata/b23_request_signed.txt
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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"}
|
||||
5
third_party/httpsig-go/testdata/b24_response_sigbase.txt
vendored
Normal file
5
third_party/httpsig-go/testdata/b24_response_sigbase.txt
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"@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"
|
||||
9
third_party/httpsig-go/testdata/b24_response_signed.txt
vendored
Normal file
9
third_party/httpsig-go/testdata/b24_response_signed.txt
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
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"}
|
||||
4
third_party/httpsig-go/testdata/b25_request_sigbase.txt
vendored
Normal file
4
third_party/httpsig-go/testdata/b25_request_sigbase.txt
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
"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"
|
||||
10
third_party/httpsig-go/testdata/b25_request_signed.txt
vendored
Normal file
10
third_party/httpsig-go/testdata/b25_request_signed.txt
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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"}
|
||||
7
third_party/httpsig-go/testdata/b26_request_sigbase.txt
vendored
Normal file
7
third_party/httpsig-go/testdata/b26_request_sigbase.txt
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"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"
|
||||
10
third_party/httpsig-go/testdata/b26_request_signed.txt
vendored
Normal file
10
third_party/httpsig-go/testdata/b26_request_signed.txt
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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"}
|
||||
3
third_party/httpsig-go/testdata/fuzz/FuzzExtractSignatures/00e15d22123489fd
vendored
Normal file
3
third_party/httpsig-go/testdata/fuzz/FuzzExtractSignatures/00e15d22123489fd
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
go test fuzz v1
|
||||
string("0")
|
||||
string("0")
|
||||
3
third_party/httpsig-go/testdata/fuzz/FuzzExtractSignatures/85649d45641911d0
vendored
Normal file
3
third_party/httpsig-go/testdata/fuzz/FuzzExtractSignatures/85649d45641911d0
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
go test fuzz v1
|
||||
string("0")
|
||||
string("")
|
||||
4
third_party/httpsig-go/testdata/fuzz/FuzzSign/01c151dfb31ce1b1
vendored
Normal file
4
third_party/httpsig-go/testdata/fuzz/FuzzSign/01c151dfb31ce1b1
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
go test fuzz v1
|
||||
string("0")
|
||||
string("\x0f")
|
||||
string("\x7f")
|
||||
4
third_party/httpsig-go/testdata/fuzz/FuzzSign/2da5aa149625a656
vendored
Normal file
4
third_party/httpsig-go/testdata/fuzz/FuzzSign/2da5aa149625a656
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
go test fuzz v1
|
||||
string("0")
|
||||
string("0")
|
||||
string("\x7f")
|
||||
4
third_party/httpsig-go/testdata/fuzz/FuzzSign/58358ab8a686cc26
vendored
Normal file
4
third_party/httpsig-go/testdata/fuzz/FuzzSign/58358ab8a686cc26
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
go test fuzz v1
|
||||
string("\n")
|
||||
string("0")
|
||||
string("0")
|
||||
4
third_party/httpsig-go/testdata/fuzz/FuzzSign/ecf6af09b9da5a62
vendored
Normal file
4
third_party/httpsig-go/testdata/fuzz/FuzzSign/ecf6af09b9da5a62
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
go test fuzz v1
|
||||
string("")
|
||||
string("0")
|
||||
string("@")
|
||||
4
third_party/httpsig-go/testdata/fuzz/FuzzSign/ff08bb6e0efa59be
vendored
Normal file
4
third_party/httpsig-go/testdata/fuzz/FuzzSign/ff08bb6e0efa59be
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
go test fuzz v1
|
||||
string("")
|
||||
string("0")
|
||||
string("\xde")
|
||||
16
third_party/httpsig-go/testdata/key-rsa-v15.key
vendored
Normal file
16
third_party/httpsig-go/testdata/key-rsa-v15.key
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-----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-----
|
||||
6
third_party/httpsig-go/testdata/key-rsa-v15.pub
vendored
Normal file
6
third_party/httpsig-go/testdata/key-rsa-v15.pub
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCvgZyuyxbY+pIzNWy+x7QkEKQl
|
||||
49ZmbQkwE8+B2UvUA+Xhoru0VnUlvnXdKaBpnzhNxktn6HwLd6CYqZlfLTVvbgeD
|
||||
WE8KuXxW9DJi9+AQ0AUKOqGwjgJMelBtIfMvdWPg6fZXCsDfvjvJkcOmx4lcnwEy
|
||||
Zt4YEh55oIisi9MiRQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
8
third_party/httpsig-go/testdata/request1.txt
vendored
Normal file
8
third_party/httpsig-go/testdata/request1.txt
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
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"}
|
||||
8
third_party/httpsig-go/testdata/request_bad_digest.txt
vendored
Normal file
8
third_party/httpsig-go/testdata/request_bad_digest.txt
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
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"}
|
||||
9
third_party/httpsig-go/testdata/request_multivalue.txt
vendored
Normal file
9
third_party/httpsig-go/testdata/request_multivalue.txt
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
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"}
|
||||
7
third_party/httpsig-go/testdata/request_no_digest.txt
vendored
Normal file
7
third_party/httpsig-go/testdata/request_no_digest.txt
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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"}
|
||||
10
third_party/httpsig-go/testdata/request_repeated_components.txt
vendored
Normal file
10
third_party/httpsig-go/testdata/request_repeated_components.txt
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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"}
|
||||
8
third_party/httpsig-go/testdata/rfc-test-request.txt
vendored
Normal file
8
third_party/httpsig-go/testdata/rfc-test-request.txt
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
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"}
|
||||
7
third_party/httpsig-go/testdata/rfc-test-response.txt
vendored
Normal file
7
third_party/httpsig-go/testdata/rfc-test-response.txt
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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"}
|
||||
5
third_party/httpsig-go/testdata/test-key-ecc-p256.key
vendored
Normal file
5
third_party/httpsig-go/testdata/test-key-ecc-p256.key
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIFKbhfNZfpDsW43+0+JjUr9K+bTeuxopu653+hBaXGA7oAoGCCqGSM49
|
||||
AwEHoUQDQgAEqIVYZVLCrPZHGHjP17CTW0/+D9Lfw0EkjqF7xB4FivAxzic30tMM
|
||||
4GF+hR6Dxh71Z50VGGdldkkDXZCnTNnoXQ==
|
||||
-----END EC PRIVATE KEY-----
|
||||
4
third_party/httpsig-go/testdata/test-key-ecc-p256.pub
vendored
Normal file
4
third_party/httpsig-go/testdata/test-key-ecc-p256.pub
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqIVYZVLCrPZHGHjP17CTW0/+D9Lf
|
||||
w0EkjqF7xB4FivAxzic30tMM4GF+hR6Dxh71Z50VGGdldkkDXZCnTNnoXQ==
|
||||
-----END PUBLIC KEY-----
|
||||
6
third_party/httpsig-go/testdata/test-key-ecc-p384.key
vendored
Normal file
6
third_party/httpsig-go/testdata/test-key-ecc-p384.key
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDBlRoIHTHCg2hgrwD5cQMl1aGP8ZZ/ulSbHCDHHIC4ENlQIdjLDhOBG
|
||||
JO2ZMKJK33WgBwYFK4EEACKhZANiAAS46EhdhNcOPg3ZKMhlryAFfy6eKt3M3f+w
|
||||
31ikDCbu10GLZjvCoevMvSY+TQIL9EQlAHRxcJ5ciXt1ukiH2zo1NpR7S8ozmvCV
|
||||
t/IvcUiS9XS1nRiHKkM1e8NMNtg3avo=
|
||||
-----END EC PRIVATE KEY-----
|
||||
5
third_party/httpsig-go/testdata/test-key-ecc-p384.pub
vendored
Normal file
5
third_party/httpsig-go/testdata/test-key-ecc-p384.pub
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEuOhIXYTXDj4N2SjIZa8gBX8unirdzN3/
|
||||
sN9YpAwm7tdBi2Y7wqHrzL0mPk0CC/REJQB0cXCeXIl7dbpIh9s6NTaUe0vKM5rw
|
||||
lbfyL3FIkvV0tZ0YhypDNXvDTDbYN2r6
|
||||
-----END PUBLIC KEY-----
|
||||
3
third_party/httpsig-go/testdata/test-key-ed25519.key
vendored
Normal file
3
third_party/httpsig-go/testdata/test-key-ed25519.key
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikTmiCqirVv9mWG9qfSnF
|
||||
-----END PRIVATE KEY-----
|
||||
3
third_party/httpsig-go/testdata/test-key-ed25519.pub
vendored
Normal file
3
third_party/httpsig-go/testdata/test-key-ed25519.pub
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEAJrQLj5P/89iXES9+vFgrIy29clF9CC/oPPsw3c5D0bs=
|
||||
-----END PUBLIC KEY-----
|
||||
4
third_party/httpsig-go/testdata/test-key-rsa-pss.key
vendored
Normal file
4
third_party/httpsig-go/testdata/test-key-rsa-pss.key
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
-----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-----
|
||||
4
third_party/httpsig-go/testdata/test-key-rsa-pss.pub
vendored
Normal file
4
third_party/httpsig-go/testdata/test-key-rsa-pss.pub
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr4tmm3r20Wd/PbqvP1s2 +QEtvpuRaV8Yq40gjUR8y2Rjxa6dpG2GXHbPfvMs8ct+Lh1GH45x28Rw3Ry53mm+ oAXjyQ86OnDkZ5N8lYbggD4O3w6M6pAvLkhk95AndTrifbIFPNU8PPMO7OyrFAHq gDsznjPFmTOtCEcN2Z1FpWgchwuYLPL+Wokqltd11nqqzi+bJ9cvSKADYdUAAN5W Utzdpiy6LbTgSxP7ociU4Tn0g5I6aDZJ7A8Lzo0KSyZYoA485mqcO0GVAdVw9lq4 aOT9v6d+nb4bnNkQVklLQ3fVAvJm+xdDOp9LCNCN48V2pnDOkFV6+U9nV5oyc6XI
|
||||
2wIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
27
third_party/httpsig-go/testdata/test-key-rsa.key
vendored
Normal file
27
third_party/httpsig-go/testdata/test-key-rsa.key
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-----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-----
|
||||
8
third_party/httpsig-go/testdata/test-key-rsa.pub
vendored
Normal file
8
third_party/httpsig-go/testdata/test-key-rsa.pub
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-----BEGIN RSA PUBLIC KEY-----
|
||||
MIIBCgKCAQEAhAKYdtoeoy8zcAcR874L8cnZxKzAGwd7v36APp7Pv6Q2jdsPBRrw
|
||||
WEBnez6d0UDKDwGbc6nxfEXAy5mbhgajzrw3MOEt8uA5txSKobBpKDeBLOsdJKFq
|
||||
MGmXCQvEG7YemcxDTRPxAleIAgYYRjTSd/QBwVW9OwNFhekro3RtlinV0a75jfZg
|
||||
kne/YiktSvLG34lw2zqXBDTC5NHROUqGTlML4PlNZS5Ri2U4aCNx2rUPRcKIlE0P
|
||||
uKxI4T+HIaFpv8+rdV6eUgOrB2xeI1dSFFn/nnv5OoZJEIB+VmuKn3DCUcCZSFlQ
|
||||
PSXSfBDiUGhwOw76WuSSsf1D4b/vLoJ10wIDAQAB
|
||||
-----END RSA PUBLIC KEY-----
|
||||
1
third_party/httpsig-go/testdata/test-shared-secret
vendored
Normal file
1
third_party/httpsig-go/testdata/test-shared-secret
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==
|
||||
10
third_party/httpsig-go/testdata/verify_request1.txt
vendored
Normal file
10
third_party/httpsig-go/testdata/verify_request1.txt
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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"}
|
||||
10
third_party/httpsig-go/testdata/verify_request2.txt
vendored
Normal file
10
third_party/httpsig-go/testdata/verify_request2.txt
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
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"}
|
||||
476
third_party/httpsig-go/verify.go
vendored
Normal file
476
third_party/httpsig-go/verify.go
vendored
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
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)
|
||||
}
|
||||
233
third_party/httpsig-go/verify_test.go
vendored
Normal file
233
third_party/httpsig-go/verify_test.go
vendored
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in a new issue