Skip to content

Commit

Permalink
algs, transport and middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
denpeshkov committed Oct 6, 2024
1 parent 891b2d8 commit cd395a9
Show file tree
Hide file tree
Showing 17 changed files with 878 additions and 25 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ jobs:
uses: actions/checkout@v4

- name: Setup Go Environment
uses: actions/setup-go@v4.1.0
uses: actions/setup-go@v5.0.2
with:
go-version: 1.21
go-version: 1.22
cache: false # managed by golangci-lint

- name: Download Dependencies
run: go mod download -x

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

Expand Down
34 changes: 34 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
run:
concurrency: 4
timeout: 2m
issues-exit-code: 1

output:
formats:
- format: colored-line-number
print-issued-lines: true
print-linter-name: true

linters:
# We'll track the golangci-lint default linters manually
# instead of letting them change without our control.
disable-all: true
enable:
# golangci-lint defaults:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused

# Our own extras:
- bodyclose
- exhaustive
- goimports
- gosec
- misspell
- prealloc
- unconvert
- unparam
21 changes: 0 additions & 21 deletions Dockerfile

This file was deleted.

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ tidy: ## Tidy

.PHONY: lint
lint: ## Lint
golangci-lint run
docker run -t --rm -v .:/app -v ~/.cache/golangci-lint/v1.61.0:/root/.cache -w /app golangci/golangci-lint:v1.61.0 golangci-lint run -v -c .golangci.yml

.PHONY: test
test: ## Test
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# httpsig

`httpsig` provides utilities for creating, encoding, and verifying signatures within HTTP requests.
13 changes: 13 additions & 0 deletions alg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package httpsig

// Signer signs messages.
// It must be safe for concurrent use by multiple goroutines.
type Signer interface {
Sign(message []byte) ([]byte, error)
}

// Verifier verifies message signatures.
// It must be safe for concurrent use by multiple goroutines.
type Verifier interface {
Verify(message []byte, signature []byte) (bool, error)
}
65 changes: 65 additions & 0 deletions ecdsa/ecdsa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Package ecdsa provides utilities for signing and verifying messages using ECDSA.
package ecdsa

import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"errors"
"io"
)

// ErrHashUnavailable is returned when the hash function is not linked into the binary.
var ErrHashUnavailable = errors.New("ecdsa: requested hash function is unavailable")

// Signer signs messages using ECDSA.
// It is safe for concurrent use by multiple goroutines.
type Signer struct {
Verifier
Rand io.Reader // Defaults to crypto/rand.Reader if not set.

priv *ecdsa.PrivateKey
}

// NewSigner returns a new [Signer] for the provided private key and hash algorithm.
func NewSigner(priv *ecdsa.PrivateKey, hash crypto.Hash) (*Signer, error) {
if !hash.Available() {
return nil, ErrHashUnavailable
}
return &Signer{
Rand: rand.Reader,
priv: priv,
Verifier: Verifier{pub: &priv.PublicKey, hash: hash},
}, nil
}

// Sign signs a message using the private key.
func (s *Signer) Sign(message []byte) ([]byte, error) {
return ecdsa.SignASN1(s.Rand, s.priv, s.digest(message))
}

// Verifier verifies ECDSA message signatures.
// It is safe for concurrent use by multiple goroutines.
type Verifier struct {
pub *ecdsa.PublicKey
hash crypto.Hash
}

// NewVerifier returns a new [Verifier] for the provided public key and hash algorithm.
func NewVerifier(pub *ecdsa.PublicKey, hash crypto.Hash) (*Verifier, error) {
if !hash.Available() {
return nil, ErrHashUnavailable
}
return &Verifier{pub: pub, hash: hash}, nil
}

// Verify verifies the signature of a message using the public key.
func (v *Verifier) Verify(message []byte, signature []byte) (bool, error) {
return ecdsa.VerifyASN1(v.pub, v.digest(message), signature), nil
}

func (v *Verifier) digest(msg []byte) []byte {
h := v.hash.New()
_, _ = h.Write(msg) // never returns an error
return h.Sum(nil)
}
59 changes: 59 additions & 0 deletions ecdsa/ecdsa_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ecdsa

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
_ "crypto/sha256"
"testing"
)

func TestSignVerify(t *testing.T) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("GenerateKey() error: %v", err)
}
hash := crypto.SHA256
sig, err := NewSigner(key, hash)
if err != nil {
t.Fatalf("NewSigner() error: %v", err)
}
ver, err := NewVerifier(&key.PublicKey, hash)
if err != nil {
t.Fatalf("NewVerifier() error: %v", err)
}

msg := []byte("test")

testf := func() {
for i := range 3 {
t.Logf("Iteration %d", i)
sign, err := sig.Sign(msg)
if err != nil {
t.Fatalf("Signer.Sign(%s) error: %v", msg, err)
}
if ok, err := sig.Verify(msg, sign); err != nil {
t.Fatalf("Signer.Verify(%s, %x) error: %v", msg, sign, err)
} else if !ok {
t.Errorf("Signed message not verified by Signer")
}

if ok, err := ver.Verify(msg, sign); err != nil {
t.Fatalf("Verifier.Verify(%s, %x) error: %v", msg, sign, err)
} else if !ok {
t.Errorf("Signed message not verified by Verifier")
}
}
}

// Test for concurrency safety using the -race flag.
t.Run("g1", func(t *testing.T) {
t.Parallel()
testf()
})
t.Run("g2", func(t *testing.T) {
t.Parallel()
testf()
})
}
52 changes: 52 additions & 0 deletions ed25519/ed25519.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Package ed25519 provides utilities for signing and verifying messages using Ed25519.
package ed25519

import (
"crypto/ed25519"
"errors"
)

var ErrInvalidKey = errors.New("ed25519: bad key length")

// Signer signs messages using Ed25519.
// It is safe for concurrent use by multiple goroutines.
type Signer struct {
Verifier

priv ed25519.PrivateKey
}

// NewSigner returns a new [Signer] for the provided private key and hash algorithm.
func NewSigner(priv ed25519.PrivateKey) (*Signer, error) {
if len(priv) != ed25519.PrivateKeySize {
return nil, ErrInvalidKey
}
return &Signer{
priv: priv,
Verifier: Verifier{pub: priv.Public().(ed25519.PublicKey)},
}, nil
}

// Sign signs a message using the private key.
func (s *Signer) Sign(message []byte) ([]byte, error) {
return ed25519.Sign(s.priv, message), nil
}

// Verifier verifies Ed25519 message signatures.
// It is safe for concurrent use by multiple goroutines.
type Verifier struct {
pub ed25519.PublicKey
}

// NewVerifier returns a new [Verifier] for the provided public key and hash algorithm.
func NewVerifier(pub ed25519.PublicKey) (*Verifier, error) {
if len(pub) != ed25519.PublicKeySize {
return nil, ErrInvalidKey
}
return &Verifier{pub: pub}, nil
}

// Verify verifies the signature of a message using the public key.
func (v *Verifier) Verify(message []byte, signature []byte) (bool, error) {
return ed25519.Verify(v.pub, message, signature), nil
}
55 changes: 55 additions & 0 deletions ed25519/ed25519_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package ed25519

import (
"crypto/ed25519"
"crypto/rand"
"testing"
)

func TestSignVerify(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("GenerateKey() error: %v", err)
}
sig, err := NewSigner(priv)
if err != nil {
t.Fatalf("NewSigner() error: %v", err)
}
ver, err := NewVerifier(pub)
if err != nil {
t.Fatalf("NewVerifier() error: %v", err)
}

msg := []byte("test")

testf := func() {
for i := range 3 {
t.Logf("Iteration %d", i)
sign, err := sig.Sign(msg)
if err != nil {
t.Fatalf("Signer.Sign(%s) error: %v", msg, err)
}
if ok, err := sig.Verify(msg, sign); err != nil {
t.Fatalf("Signer.Verify(%s, %x) error: %v", msg, sign, err)
} else if !ok {
t.Errorf("Signed message not verified by Signer")
}

if ok, err := ver.Verify(msg, sign); err != nil {
t.Fatalf("Verifier.Verify(%s, %x) error: %v", msg, sign, err)
} else if !ok {
t.Errorf("Signed message not verified by Verifier")
}
}
}

// Test for concurrency safety using the -race flag.
t.Run("g1", func(t *testing.T) {
t.Parallel()
testf()
})
t.Run("g2", func(t *testing.T) {
t.Parallel()
testf()
})
}
42 changes: 42 additions & 0 deletions hmac/hmac.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Package hmac provides utilities for signing and verifying messages using HMAC.
package hmac

import (
"crypto"
"crypto/hmac"
"errors"
)

// ErrHashUnavailable is returned when the hash function is not linked into the binary.
var ErrHashUnavailable = errors.New("hmac: requested hash function is unavailable")

// HMAC signs messages and verifies message signatures using HMAC.
// It is safe for concurrent use by multiple goroutines.
type HMAC struct {
key []byte
hash crypto.Hash
}

// New returns a new [HMAC] for the provided key and hash algorithm.
func New(key []byte, hash crypto.Hash) (*HMAC, error) {
if !hash.Available() {
return nil, ErrHashUnavailable
}
return &HMAC{key: key, hash: hash}, nil
}

// Sign signs a message using the key.
func (h HMAC) Sign(message []byte) ([]byte, error) {
return h.digest(message), nil
}

// Verify verifies the signature of a message using the key.
func (h HMAC) Verify(message []byte, signature []byte) (bool, error) {
return hmac.Equal(signature, h.digest(message)), nil
}

func (h HMAC) digest(msg []byte) []byte {
hash := hmac.New(h.hash.New, h.key)
_, _ = hash.Write(msg) // never returns an error
return hash.Sum(nil)
}
Loading

0 comments on commit cd395a9

Please sign in to comment.