Skip to content

Commit

Permalink
feat: eliminate crypto.SignerOpts from Sign/Verify
Browse files Browse the repository at this point in the history
Eliminate io.Reader and crypto.SignerOpts arguments from the
Signator/Verifier interfaces to avoid the need to pass crypto.hash(0) in
multiple places.

For symmetry with httpsignature.Ed25519PubKey add Ed25519PrivKey wrapper
for ed25519.PrivateKey that implements the updated Verifier interface.
In addition the wrapper provides Public() and PublicHex() methods to
simplify the common operations.

Close #2635
  • Loading branch information
ibukanov committed Aug 7, 2024
1 parent 9943700 commit e916ad1
Show file tree
Hide file tree
Showing 24 changed files with 222 additions and 268 deletions.
49 changes: 38 additions & 11 deletions libs/httpsignature/ed25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,60 @@ package httpsignature

import (
"crypto"
"crypto/ed25519"
"encoding/hex"
"errors"
"fmt"
"io"
"strconv"

"golang.org/x/crypto/ed25519"
)

// Ed25519PubKey a wrapper type around ed25519.PublicKey to fulfill interface Verifier
type Ed25519PubKey ed25519.PublicKey

// Ed25519PubKey a wrapper type around ed25519.PublicKey to fulfill interface Signator
type Ed25519PrivKey ed25519.PrivateKey

// ED25519-specific signator with access to the corresponding public key
type Ed25519Signator interface {
Signator
Public() Ed25519PubKey
}

// Verify the signature sig for message using the ed25519 public key pk
// Returns true if the signature is valid, false if not and error if the key provided is not valid
func (pk Ed25519PubKey) Verify(message, sig []byte, opts crypto.SignerOpts) (bool, error) {
func (pk Ed25519PubKey) VerifySignature(message, sig []byte) error {
if l := len(pk); l != ed25519.PublicKeySize {
return false, errors.New("ed25519: bad public key length: " + strconv.Itoa(l))
return fmt.Errorf("ed25519: bad public key length: %d", l)
}

return ed25519.Verify(ed25519.PublicKey(pk), message, sig), nil
if !ed25519.Verify(ed25519.PublicKey(pk), message, sig) {
return ErrBadSignature
}
return nil
}

func (pk Ed25519PubKey) String() string {
return hex.EncodeToString(pk)
}

// GenerateEd25519Key generate an ed25519 keypair and return it
func GenerateEd25519Key(rand io.Reader) (Ed25519PubKey, ed25519.PrivateKey, error) {
publicKey, privateKey, err := ed25519.GenerateKey(nil)
return Ed25519PubKey(publicKey), privateKey, err
func (privKey Ed25519PrivKey) SignMessage(
message []byte,
) (signature []byte, err error) {
return ed25519.PrivateKey(privKey).Sign(nil, message, crypto.Hash(0))
}

func (privKey Ed25519PrivKey) Public() Ed25519PubKey {
pubKey := ed25519.PrivateKey(privKey).Public().(ed25519.PublicKey)
return Ed25519PubKey(pubKey)
}

// Get the public key encoded as hexadecimal string
func (privKey Ed25519PrivKey) PublicHex() string {
pubKey := ed25519.PrivateKey(privKey).Public().(ed25519.PublicKey)
return hex.EncodeToString(pubKey)
}

// GenerateEd25519Key generate an ed25519 private key
func GenerateEd25519Key(rand io.Reader) (Ed25519PrivKey, error) {
_, privateKey, err := ed25519.GenerateKey(nil)
return Ed25519PrivKey(privateKey), err
}
15 changes: 8 additions & 7 deletions libs/httpsignature/hmac.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package httpsignature

import (
"crypto"
"crypto/hmac"
"crypto/sha512"
"crypto/subtle"
"io"
)

// HMACKey is a symmetric key that can be used for HMAC-SHA512 request signing and verification
type HMACKey string

// Sign the message using the hmac key
func (key HMACKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) {
func (key HMACKey) SignMessage(message []byte) (signature []byte, err error) {
hhash := hmac.New(sha512.New, []byte(key))
// writing the message (HTTP signing string) to it
_, err = hhash.Write(message)
Expand All @@ -24,14 +22,17 @@ func (key HMACKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts)
}

// Verify the signature sig for message using the hmac key
func (key HMACKey) Verify(message, sig []byte, opts crypto.SignerOpts) (bool, error) {
hashSum, err := key.Sign(nil, message, nil)
func (key HMACKey) VerifySignature(message, sig []byte) error {
hashSum, err := key.SignMessage(message)
if err != nil {
return false, err
return err
}
// Return bool by checking whether or not the calculated hash is equal to
// sig pulled out of the header. Check if returned int is equal to 1 to return a bool
return subtle.ConstantTimeCompare(hashSum, sig) == 1, nil
if subtle.ConstantTimeCompare(hashSum, sig) != 1 {
return ErrBadSignature
}
return nil
}

func (key HMACKey) String() string {
Expand Down
40 changes: 18 additions & 22 deletions libs/httpsignature/httpsignature.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import (
"bytes"
"context"
"crypto"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"regexp"
"strings"
Expand All @@ -37,20 +35,21 @@ type signature struct {
// Signator is an interface for cryptographic signature creation
// NOTE that this is a subset of the crypto.Signer interface
type Signator interface {
Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error)
SignMessage(message []byte) (signature []byte, err error)
}

// Verifier is an interface for cryptographic signature verification
type Verifier interface {
Verify(message, sig []byte, opts crypto.SignerOpts) (bool, error)
// Should return ErrBadSignature if the verification fails due to signature
// mismatch.
VerifySignature(message, sig []byte) error
String() string
}

// ParameterizedSignator contains the parameters / options needed to create signatures and a signator
type ParameterizedSignator struct {
SignatureParams
Signator Signator
Opts crypto.SignerOpts
}

// Keystore provides a way to lookup a public key based on the keyID a request was signed with
Expand All @@ -68,7 +67,6 @@ type StaticKeystore struct {
type ParameterizedKeystoreVerifier struct {
SignatureParams
Keystore Keystore
Opts crypto.SignerOpts
}

const (
Expand All @@ -82,6 +80,8 @@ const (

var (
signatureRegex = regexp.MustCompile(`(\w+)="([^"]*)"`)

ErrBadSignature = errors.New("Signature mismatch")
)

// LookupVerifier by returning a static verifier
Expand Down Expand Up @@ -140,7 +140,7 @@ func (sp *SignatureParams) BuildSigningString(req *http.Request) (out []byte, er
if err != nil {
return out, err
}
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
req.Body = io.NopCloser(bytes.NewBuffer(body))
d.Update(body)
}
req.Header.Add("Digest", d.String())
Expand All @@ -167,13 +167,13 @@ func (sp *SignatureParams) BuildSigningString(req *http.Request) (out []byte, er
return out, nil
}

// Sign the included HTTP request req using signator and options opts
func (sp *SignatureParams) Sign(signator Signator, opts crypto.SignerOpts, req *http.Request) error {
// Sign the included HTTP request req using the signator
func (sp *SignatureParams) SignRequest(signator Signator, req *http.Request) error {
ss, err := sp.BuildSigningString(req)
if err != nil {
return err
}
sig, err := signator.Sign(rand.Reader, ss, opts)
sig, err := signator.SignMessage(ss)
if err != nil {
return err
}
Expand All @@ -190,29 +190,29 @@ func (sp *SignatureParams) Sign(signator Signator, opts crypto.SignerOpts, req *
return nil
}

// SignRequest using signator and options opts in the parameterized signator
// SignRequest using signator in the parameterized signator
func (p *ParameterizedSignator) SignRequest(req *http.Request) error {
return p.SignatureParams.Sign(p.Signator, p.Opts, req)
return p.SignatureParams.SignRequest(p.Signator, req)
}

// Verify the HTTP signature s over HTTP request req using verifier with options opts
func (sp *SignatureParams) Verify(verifier Verifier, opts crypto.SignerOpts, req *http.Request) (bool, error) {
func (sp *SignatureParams) VerifyRequest(verifier Verifier, req *http.Request) error {
signingStr, err := sp.BuildSigningString(req)
if err != nil {
return false, err
return err
}

var tmp signature
err = tmp.UnmarshalText([]byte(req.Header.Get("Signature")))
if err != nil {
return false, err
return err
}

sig, err := base64.StdEncoding.DecodeString(tmp.Sig)
if err != nil {
return false, err
return err
}
return verifier.Verify(signingStr, sig, opts)
return verifier.VerifySignature(signingStr, sig)
}

// VerifyRequest using keystore to lookup verifier with options opts
Expand All @@ -236,13 +236,9 @@ func (pkv *ParameterizedKeystoreVerifier) VerifyRequest(req *http.Request) (cont
sp.Algorithm = pkv.SignatureParams.Algorithm
sp.Headers = pkv.SignatureParams.Headers

valid, err := sp.Verify(verifier, pkv.Opts, req)
if err != nil {
if err := sp.VerifyRequest(verifier, req); err != nil {
return nil, "", err
}
if !valid {
return nil, "", errors.New("signature is not valid")
}

return ctx, sp.KeyID, nil
}
Expand Down
53 changes: 14 additions & 39 deletions libs/httpsignature/httpsignature_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package httpsignature

import (
"crypto"
"encoding/hex"
"net/http"
"reflect"
"testing"

"golang.org/x/crypto/ed25519"
"github.com/stretchr/testify/assert"
)

func TestBuildSigningString(t *testing.T) {
Expand Down Expand Up @@ -48,7 +47,7 @@ func TestBuildSigningString(t *testing.T) {

func TestSign(t *testing.T) {
// ED25519 Test
var privKey ed25519.PrivateKey
var privKey Ed25519PrivKey
privHex := "96aa9ec42242a9a62196281045705196a64e12b15e9160bbb630e38385b82700e7876fd5cc3a228dad634816f4ec4b80a258b2a552467e5d26f30003211bc45d"
privKey, err := hex.DecodeString(privHex)
if err != nil {
Expand All @@ -66,7 +65,7 @@ func TestSign(t *testing.T) {
}
r.Header.Set("Foo", "bar")

err = s.Sign(privKey, crypto.Hash(0), r)
err = s.SignRequest(privKey, r)
if err != nil {
t.Error("Unexpected error while building ED25519 signing string:", err)
}
Expand All @@ -83,7 +82,7 @@ func TestSign(t *testing.T) {

func TestSignRequest(t *testing.T) {
// ED25519 Test
var privKey ed25519.PrivateKey
var privKey Ed25519PrivKey
privHex := "96aa9ec42242a9a62196281045705196a64e12b15e9160bbb630e38385b82700e7876fd5cc3a228dad634816f4ec4b80a258b2a552467e5d26f30003211bc45d"
privKey, err := hex.DecodeString(privHex)
if err != nil {
Expand All @@ -98,7 +97,6 @@ func TestSignRequest(t *testing.T) {
ps := ParameterizedSignator{
SignatureParams: sp,
Signator: privKey,
Opts: crypto.Hash(0),
}

r, err := http.NewRequest("GET", "http://example.org/foo", nil)
Expand Down Expand Up @@ -131,7 +129,6 @@ func TestSignRequest(t *testing.T) {
ps2 := ParameterizedSignator{
SignatureParams: sp2,
Signator: HMACKey(privHex),
Opts: crypto.Hash(0),
}

r2, reqErr := http.NewRequest("GET", "http://example.org/foo2", nil)
Expand Down Expand Up @@ -178,24 +175,14 @@ func TestVerify(t *testing.T) {
r.Header.Set("Foo", "bar")
r.Header.Set("Signature", `keyId="primary",algorithm="ed25519",headers="digest",signature="`+s.Sig+`"`)

valid, err := s.Verify(pubKey, crypto.Hash(0), r)
if err != nil {
t.Error("Unexpected error while building signing string")
}
if !valid {
t.Error("The signature should be valid")
}
err = s.VerifyRequest(pubKey, r)
assert.NoError(t, err)

s.Sig = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
r.Header.Set("Signature", `keyId="primary",algorithm="ed25519",headers="digest",signature="`+s.Sig+`"`)

valid, err = s.Verify(pubKey, crypto.Hash(0), r)
if err != nil {
t.Error("Unexpected error while building signing string")
}
if valid {
t.Error("The signature should be invalid")
}
err = s.VerifyRequest(pubKey, r)
assert.ErrorIs(t, err, ErrBadSignature)

var hmacVerifier HMACKey = "yyqz64U$eG?eUAp24Pm!Fn!Cn"
var s2 signature
Expand All @@ -212,24 +199,14 @@ func TestVerify(t *testing.T) {
req.Header.Set("Foo", "bar")
req.Header.Set("Signature", `keyId="secondary",algorithm="hs2019",headers="digest",signature="`+sig+`"`)

valid, err = s2.Verify(hmacVerifier, nil, req)
if err != nil {
t.Error("Unexpected error while building signing string:", err)
}
if !valid {
t.Error("The signature should be valid")
}
err = s2.VerifyRequest(hmacVerifier, req)
assert.NoError(t, err)

sig = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
req.Header.Set("Signature", `keyId="secondary",algorithm="hs2019",headers="digest",signature="`+sig+`"`)

valid, err = s2.Verify(hmacVerifier, nil, req)
if err != nil {
t.Error("Unexpected error while building signing string")
}
if valid {
t.Error("The signature should be invalid")
}
err = s2.VerifyRequest(hmacVerifier, req)
assert.ErrorIs(t, err, ErrBadSignature)
}

func TestVerifyRequest(t *testing.T) {
Expand All @@ -247,7 +224,6 @@ func TestVerifyRequest(t *testing.T) {
pkv := ParameterizedKeystoreVerifier{
SignatureParams: sp,
Keystore: &StaticKeystore{pubKey},
Opts: crypto.Hash(0),
}

sig := "RbGSX1MttcKCpCkq9nsPGkdJGUZsAU+0TpiXJYkwde+0ZwxEp9dXO3v17DwyGLXjv385253RdGI7URbrI7J6DQ=="
Expand Down Expand Up @@ -288,7 +264,6 @@ func TestVerifyRequest(t *testing.T) {
pkv2 := ParameterizedKeystoreVerifier{
SignatureParams: sp2,
Keystore: &StaticKeystore{hmacVerifier},
Opts: crypto.Hash(0),
}

sig = "3RCLz6TH2I32nj1NY5YaUWDSCNPiKsAVIXjX4merDeNvrGondy7+f3sWQQJWRwEo90FCrthWrrVcgHqqFevS9Q=="
Expand Down Expand Up @@ -402,7 +377,7 @@ func TestTextUnmarshal(t *testing.T) {
}

func TestSignatureParamsFromRequest(t *testing.T) {
var privKey ed25519.PrivateKey
var privKey Ed25519PrivKey
privHex := "96aa9ec42242a9a62196281045705196a64e12b15e9160bbb630e38385b82700e7876fd5cc3a228dad634816f4ec4b80a258b2a552467e5d26f30003211bc45d"
privKey, err := hex.DecodeString(privHex)
if err != nil {
Expand All @@ -420,7 +395,7 @@ func TestSignatureParamsFromRequest(t *testing.T) {
}
r.Header.Set("Foo", "bar")

err = s.Sign(privKey, crypto.Hash(0), r)
err = s.SignRequest(privKey, r)
if err != nil {
t.Error("Unexpected error while building ED25519 signing string:", err)
}
Expand Down
Loading

0 comments on commit e916ad1

Please sign in to comment.