Skip to content

Commit

Permalink
Add LoadSigner/Verifier WithOpts functions for more flexibility
Browse files Browse the repository at this point in the history
Before this commit, the Signer/Verifier to load was determined
exclusively by the public/private key type, however there may be
multiple Signers/Verifiers available, like in the case of RSA and
ED25519.

This commit adds LoadVerifierWithOpts, LoadSignerWithOpts, and
LoadSignerVerifierWithOpts to give clients more flexibility, allowing
the user of the API to choose between the available options by using
options.

Signed-off-by: Riccardo Schirone <riccardo.schirone@trailofbits.com>
  • Loading branch information
ret2libc committed Jan 25, 2024
1 parent a0cdfe8 commit f3ffaba
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 6 deletions.
70 changes: 70 additions & 0 deletions pkg/signature/options/signerverifier_options.go
@@ -0,0 +1,70 @@
//
// Copyright 2024 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package signature

import (
"crypto"
"crypto/rsa"
)

type loadOpts struct {
hashFunc crypto.Hash
useED25519ph bool
rsaPSSOptions *rsa.PSSOptions
}

// LoadOption specifies options to be used when creating a Signer/Verifier
type LoadOption func(*loadOpts)

// WithHash specifies the hash function to be used for the Signer/Verifier
func WithHash(hashFunc crypto.Hash) LoadOption {
return func(o *loadOpts) {
o.hashFunc = hashFunc
}
}

// WithED25519ph specifies that the ED25519ph algorithm should be used when a ED25519 key is used
func WithED25519ph() LoadOption {
return func(o *loadOpts) {
o.useED25519ph = true
}
}

// WithRSAPSS specifies that the RSAPSS algorithm should be used when a RSA key is used
// Note that the RSA PSSOptions contains an hash algorithm, which will override
// the hash function specified with WithHash.
func WithRSAPSS(opts *rsa.PSSOptions) LoadOption {
return func(o *loadOpts) {
o.rsaPSSOptions = opts
}
}

// GetSignerVerifierOptionHash returns the hash function specified in the options
func GetSignerVerifierOptionHash(opts ...LoadOption) crypto.Hash {
o := makeSignerVerifierOpts(opts...)
return o.hashFunc
}

func makeSignerVerifierOpts(opts ...LoadOption) *loadOpts {
o := &loadOpts{
hashFunc: crypto.SHA256,
}

for _, opt := range opts {
opt(o)
}
return o
}
32 changes: 30 additions & 2 deletions pkg/signature/signer.go
Expand Up @@ -59,12 +59,26 @@ func (s SignerOpts) HashFunc() crypto.Hash {
// If privateKey is an RSA key, a RSAPKCS1v15Signer will be returned. If a
// RSAPSSSigner is desired instead, use the LoadRSAPSSSigner() method directly.
func LoadSigner(privateKey crypto.PrivateKey, hashFunc crypto.Hash) (Signer, error) {
return LoadSignerWithOpts(privateKey, WithHash(hashFunc))
}

// LoadSignerWithOpts returns a signature.Signer based on the algorithm of the private key
// provided.
func LoadSignerWithOpts(privateKey crypto.PrivateKey, opts ...LoadOption) (Signer, error) {
o := makeSignerVerifierOpts(opts...)

switch pk := privateKey.(type) {
case *rsa.PrivateKey:
return LoadRSAPKCS1v15Signer(pk, hashFunc)
if o.rsaPSSOptions != nil {
return LoadRSAPSSSigner(pk, o.hashFunc, o.rsaPSSOptions)
}
return LoadRSAPKCS1v15Signer(pk, o.hashFunc)
case *ecdsa.PrivateKey:
return LoadECDSASigner(pk, hashFunc)
return LoadECDSASigner(pk, o.hashFunc)
case ed25519.PrivateKey:
if o.useED25519ph {
return LoadED25519phSigner(pk)
}
return LoadED25519Signer(pk)
}
return nil, errors.New("unsupported public key type")
Expand All @@ -87,3 +101,17 @@ func LoadSignerFromPEMFile(path string, hashFunc crypto.Hash, pf cryptoutils.Pas
}
return LoadSigner(priv, hashFunc)
}

// LoadSignerFromPEMFileWithOpts returns a signature.Signer based on the algorithm of the private key
// in the file. The Signer will use the hash function specified in the options when computing digests.
func LoadSignerFromPEMFileWithOpts(path string, pf cryptoutils.PassFunc, opts ...LoadOption) (Signer, error) {
fileBytes, err := os.ReadFile(filepath.Clean(path))
if err != nil {
return nil, err
}
priv, err := cryptoutils.UnmarshalPEMToPrivateKey(fileBytes, pf)
if err != nil {
return nil, err
}
return LoadSignerWithOpts(priv, opts...)
}
79 changes: 79 additions & 0 deletions pkg/signature/signer_test.go
@@ -0,0 +1,79 @@
// Copyright 2024 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package signature

import (
"bytes"
"crypto"
"crypto/ed25519"
"encoding/base64"
"testing"

"github.com/sigstore/sigstore/pkg/cryptoutils"
)

func TestLoadEd25519Signer(t *testing.T) {
privateKey, err := cryptoutils.UnmarshalPEMToPrivateKey([]byte(ed25519Priv), cryptoutils.SkipPassword)
if err != nil {
t.Fatalf("unexpected error unmarshalling public key: %v", err)
}
edPriv, ok := privateKey.(ed25519.PrivateKey)
if !ok {
t.Fatalf("expected ed25519.PrivateKey")
}

signer, err := LoadSigner(edPriv, crypto.SHA256)
if err != nil {
t.Fatalf("unexpected error loading verifier: %v", err)
}

msg := []byte("sign me")
sig, err := signer.SignMessage(bytes.NewReader(msg))
if err != nil {
t.Fatalf("unexpected error signing message: %v", err)
}

expectedSig, _ := base64.StdEncoding.DecodeString("cnafwd8DKq2nQ564eN66ckYV8anVFGFi5vaYiQg2aal7ej/J0/OE0PPdKHLHe9wdzWRMFy5MpurRD/2cGXGLBQ==")
if !bytes.Equal(sig, expectedSig) {
t.Fatalf("signature was not as expected")
}
}

func TestLoadEd25519phSigner(t *testing.T) {
privateKey, err := cryptoutils.UnmarshalPEMToPrivateKey([]byte(ed25519Priv), cryptoutils.SkipPassword)
if err != nil {
t.Fatalf("unexpected error unmarshalling public key: %v", err)
}
edPriv, ok := privateKey.(ed25519.PrivateKey)
if !ok {
t.Fatalf("expected ed25519.PrivateKey")
}

signer, err := LoadSignerWithOpts(edPriv, WithED25519ph(), WithHash(crypto.SHA512))
if err != nil {
t.Fatalf("unexpected error loading verifier: %v", err)
}

msg := []byte("sign me")
sig, err := signer.SignMessage(bytes.NewReader(msg))
if err != nil {
t.Fatalf("unexpected error signing message: %v", err)
}

expectedSig, _ := base64.StdEncoding.DecodeString("9D4pA8jutZnbqKy4fFRl+kDsVUCO50qrOD1lxmsiUFk6NX+7OXUK5BCMkE2KYPRDxjkDFBzbDZEQhaFdDV5tDg==")
if !bytes.Equal(sig, expectedSig) {
t.Fatalf("signature was not as expected")
}
}
32 changes: 30 additions & 2 deletions pkg/signature/signerverifier.go
Expand Up @@ -39,12 +39,26 @@ type SignerVerifier interface {
// If privateKey is an RSA key, a RSAPKCS1v15SignerVerifier will be returned. If a
// RSAPSSSignerVerifier is desired instead, use the LoadRSAPSSSignerVerifier() method directly.
func LoadSignerVerifier(privateKey crypto.PrivateKey, hashFunc crypto.Hash) (SignerVerifier, error) {
return LoadSignerVerifierWithOpts(privateKey, WithHash(hashFunc))
}

// LoadSignerVerifierWithOpts returns a signature.SignerVerifier based on the
// algorithm of the private key provided and the user's choice.
func LoadSignerVerifierWithOpts(privateKey crypto.PrivateKey, opts ...LoadOption) (SignerVerifier, error) {
o := makeSignerVerifierOpts(opts...)

switch pk := privateKey.(type) {
case *rsa.PrivateKey:
return LoadRSAPKCS1v15SignerVerifier(pk, hashFunc)
if o.rsaPSSOptions != nil {
return LoadRSAPSSSignerVerifier(pk, o.hashFunc, o.rsaPSSOptions)
}
return LoadRSAPKCS1v15SignerVerifier(pk, o.hashFunc)
case *ecdsa.PrivateKey:
return LoadECDSASignerVerifier(pk, hashFunc)
return LoadECDSASignerVerifier(pk, o.hashFunc)
case ed25519.PrivateKey:
if o.useED25519ph {
return LoadED25519phSignerVerifier(pk)
}
return LoadED25519SignerVerifier(pk)
}
return nil, errors.New("unsupported public key type")
Expand All @@ -67,3 +81,17 @@ func LoadSignerVerifierFromPEMFile(path string, hashFunc crypto.Hash, pf cryptou
}
return LoadSignerVerifier(priv, hashFunc)
}

// LoadSignerVerifierFromPEMFileWithOpts returns a signature.SignerVerifier based on the algorithm of the private key
// in the file. The SignerVerifier will use the hash function specified in the options when computing digests.
func LoadSignerVerifierFromPEMFileWithOpts(path string, pf cryptoutils.PassFunc, opts ...LoadOption) (SignerVerifier, error) {
fileBytes, err := os.ReadFile(filepath.Clean(path))
if err != nil {
return nil, err
}
priv, err := cryptoutils.UnmarshalPEMToPrivateKey(fileBytes, pf)
if err != nil {
return nil, err
}
return LoadSignerVerifierWithOpts(priv, opts...)
}
54 changes: 54 additions & 0 deletions pkg/signature/signerverifier_test.go
@@ -0,0 +1,54 @@
// Copyright 2024 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package signature

import (
"bytes"
"crypto"
"crypto/rsa"
"encoding/base64"
"testing"

"github.com/sigstore/sigstore/pkg/cryptoutils"
)

func TestLoadRSAPSSSignerVerifier(t *testing.T) {
opts := &rsa.PSSOptions{
Hash: crypto.SHA256,
}

privateKey, err := cryptoutils.UnmarshalPEMToPrivateKey([]byte(rsaKey), cryptoutils.SkipPassword)
if err != nil {
t.Errorf("unexpected error unmarshalling private key: %v", err)
}
sv, err := LoadSignerVerifierWithOpts(privateKey, WithHash(crypto.SHA256), WithED25519ph(), WithRSAPSS(opts))
if err != nil {
t.Errorf("unexpected error creating signer/verifier: %v", err)
}

message := []byte("sign me")
sig, err := sv.SignMessage(bytes.NewReader(message))
if err != nil {
t.Fatalf("unexpected error signing message: %v", err)
}
if err := sv.VerifySignature(bytes.NewReader(sig), bytes.NewReader(message)); err != nil {
t.Fatalf("unexpected error verifying calculated signature: %v", err)
}

expectedSig, _ := base64.StdEncoding.DecodeString("UyouJxmgAKdm/Qfi9YA7aK71/eqyLcytmDN8CQqSCgcbGSln7S5fgIAmrwUfGp1tcxKjuNjLScn11+fqawiG9y66740VEC6GfS1hgElC2k3i/v8ly2mlt+4JYs3euzYxtWnxwQr4csc7Jy2V2cjoeQm6GTxkR4E6TRJM8/UxXvjKtp3rxRD8OuyfuGFkI0lU48vjKLgbuZKQqQdWuNUOnsPvtrHxvGRY/F1C0Ig3b7SoTyAjWSXQG42faKsFT+W1L/UdRK+m73TYdxMleI4uIGtl0k0Weui1/gK7Uh2FUP5+/F1ZoQRYk/DMz0M4QPmPsYLGwc8oduoF6JvNMGKymg==")
if err := sv.VerifySignature(bytes.NewReader(expectedSig), bytes.NewReader(message)); err != nil {
t.Fatalf("unexpected error verifying expected signature: %v", err)
}
}
34 changes: 32 additions & 2 deletions pkg/signature/verifier.go
Expand Up @@ -40,12 +40,26 @@ type Verifier interface {
// If publicKey is an RSA key, a RSAPKCS1v15Verifier will be returned. If a
// RSAPSSVerifier is desired instead, use the LoadRSAPSSVerifier() method directly.
func LoadVerifier(publicKey crypto.PublicKey, hashFunc crypto.Hash) (Verifier, error) {
return LoadVerifierWithOpts(publicKey, WithHash(hashFunc))
}

// LoadVerifierWithOpts returns a signature.Verifier based on the algorithm of the public key
// provided that will use the hash function specified when computing digests.
func LoadVerifierWithOpts(publicKey crypto.PublicKey, opts ...LoadOption) (Verifier, error) {
o := makeSignerVerifierOpts(opts...)

switch pk := publicKey.(type) {
case *rsa.PublicKey:
return LoadRSAPKCS1v15Verifier(pk, hashFunc)
if o.rsaPSSOptions != nil {
return LoadRSAPSSVerifier(pk, o.hashFunc, o.rsaPSSOptions)
}
return LoadRSAPKCS1v15Verifier(pk, o.hashFunc)
case *ecdsa.PublicKey:
return LoadECDSAVerifier(pk, hashFunc)
return LoadECDSAVerifier(pk, o.hashFunc)
case ed25519.PublicKey:
if o.useED25519ph {
return LoadED25519phVerifier(pk)
}
return LoadED25519Verifier(pk)
}
return nil, errors.New("unsupported public key type")
Expand Down Expand Up @@ -98,3 +112,19 @@ func LoadVerifierFromPEMFile(path string, hashFunc crypto.Hash) (Verifier, error

return LoadVerifier(pubKey, hashFunc)
}

// LoadVerifierFromPEMFileWithOpts returns a signature.Verifier based on the contents of a
// file located at path. The Verifier wil use the hash function specified in the options when computing digests.
func LoadVerifierFromPEMFileWithOpts(path string, opts ...LoadOption) (Verifier, error) {
fileBytes, err := os.ReadFile(filepath.Clean(path))
if err != nil {
return nil, err
}

pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(fileBytes)
if err != nil {
return nil, err
}

return LoadVerifierWithOpts(pubKey, opts...)
}
16 changes: 16 additions & 0 deletions pkg/signature/verifier_test.go
Expand Up @@ -15,6 +15,7 @@
package signature

import (
"crypto"
"crypto/rand"
"crypto/rsa"
"testing"
Expand All @@ -34,3 +35,18 @@ func TestLoadUnsafeVerifier(t *testing.T) {
t.Fatalf("public keys were not equal")
}
}

func TestLoadVerifier(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("unexpected error generating key: %v", err)
}
verifier, err := LoadVerifier(key.Public(), crypto.SHA256)
if err != nil {
t.Fatalf("unexpected error loading verifier: %v", err)
}
pubKey, _ := verifier.PublicKey()
if !key.PublicKey.Equal(pubKey) {
t.Fatalf("public keys were not equal")
}
}

0 comments on commit f3ffaba

Please sign in to comment.