From 307123d0827a6cf28bd506c9482e0ec350acc40f Mon Sep 17 00:00:00 2001 From: Aditya Sirish Date: Sun, 30 Apr 2023 13:29:03 -0400 Subject: [PATCH 1/8] Add ED25519 SignerVerifier Signed-off-by: Aditya Sirish --- signerverifier/ed25519.go | 79 ++++++++++++++++ signerverifier/ed25519_test.go | 94 +++++++++++++++++++ signerverifier/signerverifier.go | 72 ++++++++++++++ signerverifier/test-data/ed25519-test-key | 1 + signerverifier/test-data/ed25519-test-key.pub | 1 + 5 files changed, 247 insertions(+) create mode 100644 signerverifier/ed25519.go create mode 100644 signerverifier/ed25519_test.go create mode 100644 signerverifier/signerverifier.go create mode 100644 signerverifier/test-data/ed25519-test-key create mode 100644 signerverifier/test-data/ed25519-test-key.pub diff --git a/signerverifier/ed25519.go b/signerverifier/ed25519.go new file mode 100644 index 0000000..a606e32 --- /dev/null +++ b/signerverifier/ed25519.go @@ -0,0 +1,79 @@ +package signerverifier + +import ( + "context" + "crypto" + "crypto/ed25519" + "encoding/hex" +) + +const Ed25519KeyType = "ed25519" + +type Ed25519SignerVerifier struct { + keyID string + private ed25519.PrivateKey + public ed25519.PublicKey +} + +// NewEd25519SignerVerifierFromSSLibKey creates an Ed25519SignerVerifier from an +// SSLibKey. +func NewEd25519SignerVerifierFromSSLibKey(key *SSLibKey) (*Ed25519SignerVerifier, error) { + public, err := hex.DecodeString(key.KeyVal.Public) + if err != nil { + return nil, err + } + + var private []byte + if len(key.KeyVal.Private) > 0 { + private, err = hex.DecodeString(key.KeyVal.Private) + if err != nil { + return nil, err + } + + // python-securesystemslib provides an interface to generate ed25519 + // keys but it differs slightly in how it serializes the key to disk. + // Specifically, the keyval.private field includes _only_ the private + // portion of the key while libraries such as crypto/ed25519 also expect + // the public portion. So, if the private portion is half of what we + // expect, we append the public portion as well. + if len(private) == ed25519.PrivateKeySize/2 { + private = append(private, public...) + } + } + + return &Ed25519SignerVerifier{ + keyID: key.KeyID(), + public: ed25519.PublicKey(public), + private: ed25519.PrivateKey(private), + }, nil +} + +// Sign creates a signature for `data`. +func (sv *Ed25519SignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) { + if len(sv.private) == 0 { + return nil, ErrNotPrivateKey + } + + signature := ed25519.Sign(sv.private, data) + return signature, nil +} + +// Verify verifies the `sig` value passed in against `data`. +func (sv Ed25519SignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error { + if ok := ed25519.Verify(sv.public, data, sig); ok { + return nil + } + return ErrSignatureVerificationFailed +} + +// KeyID returns the identifier of the key used to create the +// Ed25519SignerVerifier instance. +func (sv Ed25519SignerVerifier) KeyID() (string, error) { + return sv.keyID, nil +} + +// Public returns the public portion of the key used to create the +// Ed25519SignerVerifier instance. +func (sv Ed25519SignerVerifier) Public() crypto.PublicKey { + return sv.public +} diff --git a/signerverifier/ed25519_test.go b/signerverifier/ed25519_test.go new file mode 100644 index 0000000..a8f6737 --- /dev/null +++ b/signerverifier/ed25519_test.go @@ -0,0 +1,94 @@ +package signerverifier + +import ( + "context" + "crypto/ed25519" + "encoding/hex" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewEd25519SignerVerifierFromSSLibKey(t *testing.T) { + key, err := LoadKeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) + if err != nil { + t.Error(err) + } + + sv, err := NewEd25519SignerVerifierFromSSLibKey(key) + if err != nil { + t.Error(err) + } + + expectedPublicString := "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f" + expectedPublicKey := ed25519.PublicKey(hexDecode(t, expectedPublicString)) + + assert.Equal(t, "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", sv.keyID) + assert.Equal(t, expectedPublicKey, sv.public) + assert.Nil(t, sv.private) +} + +func TestEd25519SignerVerifierSign(t *testing.T) { + key, err := LoadKeyFromFile(filepath.Join("test-data", "ed25519-test-key")) + if err != nil { + t.Fatal(err) + } + + sv, err := NewEd25519SignerVerifierFromSSLibKey(key) + if err != nil { + t.Error(err) + } + + message := []byte("test message") + + signature, err := sv.Sign(context.Background(), message) + if err != nil { + t.Error(err) + } + + expectedSignature := []byte{0x80, 0x72, 0xb4, 0x31, 0xc5, 0xa3, 0x7e, 0xc, 0xf3, 0x91, 0x22, 0x3, 0x60, 0xbf, 0x92, 0xa4, 0x46, 0x31, 0x84, 0x83, 0xf1, 0x31, 0x3, 0xdc, 0xbc, 0x5, 0x6f, 0xab, 0x84, 0xe4, 0xdc, 0xe9, 0xf5, 0x1c, 0xa9, 0xb3, 0x95, 0xa5, 0xa0, 0x16, 0xd3, 0xaa, 0x4d, 0xe7, 0xde, 0xaf, 0xc2, 0x5e, 0x1e, 0x9a, 0x9d, 0xc8, 0xb2, 0x5c, 0x1c, 0x68, 0xf7, 0x28, 0xb4, 0x1, 0x4d, 0x9f, 0xc8, 0x4} + assert.Equal(t, expectedSignature, signature) + + key, err = LoadKeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) + if err != nil { + t.Fatal(err) + } + + sv, err = NewEd25519SignerVerifierFromSSLibKey(key) + if err != nil { + t.Error(err) + } + + _, err = sv.Sign(context.Background(), message) + assert.ErrorIs(t, err, ErrNotPrivateKey) +} + +func TestEd25519SignerVerifierVerify(t *testing.T) { + key, err := LoadKeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) + if err != nil { + t.Fatal(err) + } + + sv, err := NewEd25519SignerVerifierFromSSLibKey(key) + if err != nil { + t.Error(err) + } + + message := []byte("test message") + signature := []byte{0x80, 0x72, 0xb4, 0x31, 0xc5, 0xa3, 0x7e, 0xc, 0xf3, 0x91, 0x22, 0x3, 0x60, 0xbf, 0x92, 0xa4, 0x46, 0x31, 0x84, 0x83, 0xf1, 0x31, 0x3, 0xdc, 0xbc, 0x5, 0x6f, 0xab, 0x84, 0xe4, 0xdc, 0xe9, 0xf5, 0x1c, 0xa9, 0xb3, 0x95, 0xa5, 0xa0, 0x16, 0xd3, 0xaa, 0x4d, 0xe7, 0xde, 0xaf, 0xc2, 0x5e, 0x1e, 0x9a, 0x9d, 0xc8, 0xb2, 0x5c, 0x1c, 0x68, 0xf7, 0x28, 0xb4, 0x1, 0x4d, 0x9f, 0xc8, 0x4} + assert.Nil(t, sv.Verify(context.Background(), message, signature)) + + message = []byte("corrupted message") + err = sv.Verify(context.Background(), message, signature) + assert.ErrorIs(t, err, ErrSignatureVerificationFailed) +} + +func hexDecode(t *testing.T, data string) []byte { + t.Helper() + b, err := hex.DecodeString(data) + if err != nil { + t.Fatal(err) + } + return b +} diff --git a/signerverifier/signerverifier.go b/signerverifier/signerverifier.go new file mode 100644 index 0000000..2563c75 --- /dev/null +++ b/signerverifier/signerverifier.go @@ -0,0 +1,72 @@ +package signerverifier + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "os" + + "github.com/secure-systems-lab/go-securesystemslib/cjson" +) + +var ( + ErrNotPrivateKey = errors.New("loaded key is not a private key") + ErrSignatureVerificationFailed = errors.New("failed to verify signature") + ErrUnknownKeyType = errors.New("unknown key type") + ErrInvalidThreshold = errors.New("threshold is either less than 1 or greater than number of provided public keys") +) + +type SSLibKey struct { + KeyIDHashAlgorithms []string `json:"keyid_hash_algorithms"` + KeyType string `json:"keytype"` + KeyVal KeyVal `json:"keyval"` + Scheme string `json:"scheme"` + keyID string +} + +func (k *SSLibKey) KeyID() string { + return k.keyID +} + +type KeyVal struct { + Private string `json:"private,omitempty"` + Public string `json:"public"` + Certificate string `json:"certificate,omitempty"` +} + +// LoadKeyFromBytes returns a pointer to a Key instance created from the +// contents of the bytes. The key contents are expected to be in the custom +// securesystemslib format. +func LoadKeyFromBytes(contents []byte) (*SSLibKey, error) { + var key *SSLibKey + if err := json.Unmarshal(contents, &key); err != nil { + return nil, err + } + + keyID, err := calculateKeyID(key) + if err != nil { + return nil, err + } + key.keyID = keyID + + return key, nil +} + +func LoadKeyFromFile(path string) (*SSLibKey, error) { + contents, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + return LoadKeyFromBytes(contents) +} + +func calculateKeyID(k *SSLibKey) (string, error) { + canonical, err := cjson.EncodeCanonical(k) + if err != nil { + return "", err + } + digest := sha256.Sum256(canonical) + return hex.EncodeToString(digest[:]), nil +} diff --git a/signerverifier/test-data/ed25519-test-key b/signerverifier/test-data/ed25519-test-key new file mode 100644 index 0000000..4d6a130 --- /dev/null +++ b/signerverifier/test-data/ed25519-test-key @@ -0,0 +1 @@ +{"keytype": "ed25519", "scheme": "ed25519", "keyid": "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f", "private": "66f6ebad4aeb949b91c84c9cfd6ee351fc4fd544744bab6e30fb400ba13c6e9a"}} \ No newline at end of file diff --git a/signerverifier/test-data/ed25519-test-key.pub b/signerverifier/test-data/ed25519-test-key.pub new file mode 100644 index 0000000..48e944e --- /dev/null +++ b/signerverifier/test-data/ed25519-test-key.pub @@ -0,0 +1 @@ +{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f"}} \ No newline at end of file From 0581e17e4ee4fd6e8a744f4300c07b5c6b43d1a0 Mon Sep 17 00:00:00 2001 From: Aditya Sirish Date: Sun, 30 Apr 2023 17:55:29 -0400 Subject: [PATCH 2/8] Add RSS PSS SignerVerifier Signed-off-by: Aditya Sirish --- go.mod | 2 +- signerverifier/ed25519.go | 10 ++ signerverifier/ed25519_test.go | 8 +- signerverifier/rsa.go | 129 ++++++++++++++++++++++ signerverifier/rsa_test.go | 69 ++++++++++++ signerverifier/signerverifier.go | 44 +------- signerverifier/test-data/rsa-test-key | 39 +++++++ signerverifier/test-data/rsa-test-key.pub | 11 ++ signerverifier/utils.go | 122 ++++++++++++++++++++ 9 files changed, 387 insertions(+), 47 deletions(-) create mode 100644 signerverifier/rsa.go create mode 100644 signerverifier/rsa_test.go create mode 100644 signerverifier/test-data/rsa-test-key create mode 100644 signerverifier/test-data/rsa-test-key.pub create mode 100644 signerverifier/utils.go diff --git a/go.mod b/go.mod index 7067a49..c14e85f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/secure-systems-lab/go-securesystemslib -go 1.17 +go 1.20 require ( github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb diff --git a/signerverifier/ed25519.go b/signerverifier/ed25519.go index a606e32..a947bfe 100644 --- a/signerverifier/ed25519.go +++ b/signerverifier/ed25519.go @@ -5,6 +5,7 @@ import ( "crypto" "crypto/ed25519" "encoding/hex" + "os" ) const Ed25519KeyType = "ed25519" @@ -77,3 +78,12 @@ func (sv Ed25519SignerVerifier) KeyID() (string, error) { func (sv Ed25519SignerVerifier) Public() crypto.PublicKey { return sv.public } + +func LoadEd25519KeyFromFile(path string) (*SSLibKey, error) { + contents, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + return loadKeyFromSSLibBytes(contents) +} diff --git a/signerverifier/ed25519_test.go b/signerverifier/ed25519_test.go index a8f6737..8d028f1 100644 --- a/signerverifier/ed25519_test.go +++ b/signerverifier/ed25519_test.go @@ -11,7 +11,7 @@ import ( ) func TestNewEd25519SignerVerifierFromSSLibKey(t *testing.T) { - key, err := LoadKeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) + key, err := LoadEd25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) if err != nil { t.Error(err) } @@ -30,7 +30,7 @@ func TestNewEd25519SignerVerifierFromSSLibKey(t *testing.T) { } func TestEd25519SignerVerifierSign(t *testing.T) { - key, err := LoadKeyFromFile(filepath.Join("test-data", "ed25519-test-key")) + key, err := LoadEd25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key")) if err != nil { t.Fatal(err) } @@ -50,7 +50,7 @@ func TestEd25519SignerVerifierSign(t *testing.T) { expectedSignature := []byte{0x80, 0x72, 0xb4, 0x31, 0xc5, 0xa3, 0x7e, 0xc, 0xf3, 0x91, 0x22, 0x3, 0x60, 0xbf, 0x92, 0xa4, 0x46, 0x31, 0x84, 0x83, 0xf1, 0x31, 0x3, 0xdc, 0xbc, 0x5, 0x6f, 0xab, 0x84, 0xe4, 0xdc, 0xe9, 0xf5, 0x1c, 0xa9, 0xb3, 0x95, 0xa5, 0xa0, 0x16, 0xd3, 0xaa, 0x4d, 0xe7, 0xde, 0xaf, 0xc2, 0x5e, 0x1e, 0x9a, 0x9d, 0xc8, 0xb2, 0x5c, 0x1c, 0x68, 0xf7, 0x28, 0xb4, 0x1, 0x4d, 0x9f, 0xc8, 0x4} assert.Equal(t, expectedSignature, signature) - key, err = LoadKeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) + key, err = LoadEd25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) if err != nil { t.Fatal(err) } @@ -65,7 +65,7 @@ func TestEd25519SignerVerifierSign(t *testing.T) { } func TestEd25519SignerVerifierVerify(t *testing.T) { - key, err := LoadKeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) + key, err := LoadEd25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) if err != nil { t.Fatal(err) } diff --git a/signerverifier/rsa.go b/signerverifier/rsa.go new file mode 100644 index 0000000..306b7df --- /dev/null +++ b/signerverifier/rsa.go @@ -0,0 +1,129 @@ +package signerverifier + +import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "errors" + "os" +) + +// ErrNoPEMBlock gets triggered when there is no PEM block in the provided file +var ErrNoPEMBlock = errors.New("failed to decode the data as PEM block (are you sure this is a pem file?)") + +// ErrFailedPEMParsing gets returned when PKCS1, PKCS8 or PKIX key parsing fails +var ErrFailedPEMParsing = errors.New("failed parsing the PEM block: unsupported PEM type") + +const ( + RSAKeyType = "rsa" + RSAKeyScheme = "rsassa-pss-sha256" + RSAPublicKeyPEM = "PUBLIC KEY" + RSAPrivateKeyPEM = "RSA PRIVATE KEY" +) + +type RSAPSSSignerVerifier struct { + keyID string + private *rsa.PrivateKey + public *rsa.PublicKey +} + +func NewRSAPSSSignerVerifierFromSSLibKey(key *SSLibKey) (*RSAPSSSignerVerifier, error) { + _, publicParsedKey, err := decodeAndParsePEM([]byte(key.KeyVal.Public)) + if err != nil { + return nil, err + } + + if len(key.KeyVal.Private) > 0 { + _, privateParsedKey, err := decodeAndParsePEM([]byte(key.KeyVal.Private)) + if err != nil { + return nil, err + } + + return &RSAPSSSignerVerifier{ + keyID: key.KeyID(), + public: publicParsedKey.(*rsa.PublicKey), + private: privateParsedKey.(*rsa.PrivateKey), + }, nil + } + + return &RSAPSSSignerVerifier{ + keyID: key.KeyID(), + public: publicParsedKey.(*rsa.PublicKey), + private: nil, + }, nil +} + +func (sv *RSAPSSSignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) { + if sv.private == nil { + return nil, ErrNotPrivateKey + } + + hashedData := hashBeforeSigning(data) + + return rsa.SignPSS(rand.Reader, sv.private, crypto.SHA256, hashedData, &rsa.PSSOptions{SaltLength: sha256.Size, Hash: crypto.SHA256}) +} + +func (sv RSAPSSSignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error { + hashedData := hashBeforeSigning(data) + + if err := rsa.VerifyPSS(sv.public, crypto.SHA256, hashedData, sig, &rsa.PSSOptions{SaltLength: sha256.Size, Hash: crypto.SHA256}); err != nil { + return ErrSignatureVerificationFailed + } + + return nil +} + +func (sv RSAPSSSignerVerifier) KeyID() (string, error) { + return sv.keyID, nil +} + +func (sv RSAPSSSignerVerifier) Public() crypto.PublicKey { + return sv.public +} + +func LoadRSAPSSKeyFromFile(path string) (*SSLibKey, error) { + contents, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + pemData, keyObj, err := decodeAndParsePEM(contents) + if err != nil { + return nil, err + } + + key := &SSLibKey{ + KeyType: RSAKeyType, + Scheme: RSAKeyScheme, + KeyIDHashAlgorithms: KeyIDHashAlgorithms, + KeyVal: KeyVal{}, + } + + switch k := keyObj.(type) { + case *rsa.PublicKey: + pubKeyBytes, err := x509.MarshalPKIXPublicKey(k) + if err != nil { + return nil, err + } + key.KeyVal.Public = string(generatePEMBlock(pubKeyBytes, RSAPublicKeyPEM)) + + case *rsa.PrivateKey: + pubKeyBytes, err := x509.MarshalPKIXPublicKey(k.Public()) + if err != nil { + return nil, err + } + key.KeyVal.Public = string(generatePEMBlock(pubKeyBytes, RSAPublicKeyPEM)) + key.KeyVal.Private = string(generatePEMBlock(pemData.Bytes, RSAPrivateKeyPEM)) + } + + keyID, err := calculateKeyID(key) + if err != nil { + return nil, err + } + key.keyID = keyID + + return key, nil +} diff --git a/signerverifier/rsa_test.go b/signerverifier/rsa_test.go new file mode 100644 index 0000000..72a9325 --- /dev/null +++ b/signerverifier/rsa_test.go @@ -0,0 +1,69 @@ +package signerverifier + +import ( + "context" + "crypto/rsa" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewRSAPSSSignerVerifierFromSSLibKey(t *testing.T) { + key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key.pub")) + if err != nil { + t.Error(err) + } + + sv, err := NewRSAPSSSignerVerifierFromSSLibKey(key) + if err != nil { + t.Error(err) + } + + expectedPublicString := "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA04egZRic+dZMVtiQc56D\nejU4FF1q3aOkUKnD+Q4lTbj1zp6ODKJTcktupmrad68jqtMiSGG8he6ELFs377q8\nbbgEUMWgAf+06Q8oFvUSfOXzZNFI7H5SMPOJY5aDWIMIEZ8DlcO7TfkA7D3iAEJX\nxxTOVS3UAIk5umO7Y7t7yXr8O/C4u78krGazCnoblcekMLJZV4O/5BloWNAe/B1c\nvZdaZUf3brD4ZZrxEtXw/tefhn1aHsSUajVW2wwjSpKhqj7Z0XS3bDS3T95/3xsN\n6+hlS6A7rJfiWpKIRHj0vh2SXLDmmhQl1In8TD/aiycTUyWcBRHVPlYFgYPt6SaT\nVQSgMzSxC43/2fINb2fyt8SbUHJ3Ct+mzRzd/1AQikWhBdstJLxInewzjYE/sb+c\n2CmCxMPQG2BwmAWXaaumeJcXVPBlMgAcjMatM8bPByTbXpKDnQslOE7g/gswDIwn\nEm53T13mZzYUvbLJ0q3aljZVLIC3IZn3ZwA2yCWchBkVAgMBAAE=\n-----END PUBLIC KEY-----" + _, expectedPublicKey, err := decodeAndParsePEM([]byte(expectedPublicString)) + assert.Nil(t, err) + + assert.Equal(t, "966c5d84ba73ccded42eb473c939d77336e4def253ffaf6739f8e983ef73dad8", sv.keyID) // FIXME: mismatch? + assert.Equal(t, expectedPublicKey.(*rsa.PublicKey), sv.public) + assert.Nil(t, sv.private) +} + +func TestRSAPSSSignerVerifierSignAndVerify(t *testing.T) { + t.Run("using valid key", func(t *testing.T) { + key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key")) + if err != nil { + t.Error(err) + } + + sv, err := NewRSAPSSSignerVerifierFromSSLibKey(key) + if err != nil { + t.Error(err) + } + + message := []byte("test message") + + signature, err := sv.Sign(context.Background(), message) + assert.Nil(t, err) + + err = sv.Verify(context.Background(), message, signature) + assert.Nil(t, err) + }) + + t.Run("using invalid key", func(t *testing.T) { + key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key.pub")) + if err != nil { + t.Error(err) + } + + sv, err := NewRSAPSSSignerVerifierFromSSLibKey(key) + if err != nil { + t.Error(err) + } + + message := []byte("test message") + + _, err = sv.Sign(context.Background(), message) + assert.ErrorIs(t, err, ErrNotPrivateKey) + }) +} diff --git a/signerverifier/signerverifier.go b/signerverifier/signerverifier.go index 2563c75..da2f8a7 100644 --- a/signerverifier/signerverifier.go +++ b/signerverifier/signerverifier.go @@ -1,15 +1,11 @@ package signerverifier import ( - "crypto/sha256" - "encoding/hex" - "encoding/json" "errors" - "os" - - "github.com/secure-systems-lab/go-securesystemslib/cjson" ) +var KeyIDHashAlgorithms = []string{"sha256", "sha512"} + var ( ErrNotPrivateKey = errors.New("loaded key is not a private key") ErrSignatureVerificationFailed = errors.New("failed to verify signature") @@ -34,39 +30,3 @@ type KeyVal struct { Public string `json:"public"` Certificate string `json:"certificate,omitempty"` } - -// LoadKeyFromBytes returns a pointer to a Key instance created from the -// contents of the bytes. The key contents are expected to be in the custom -// securesystemslib format. -func LoadKeyFromBytes(contents []byte) (*SSLibKey, error) { - var key *SSLibKey - if err := json.Unmarshal(contents, &key); err != nil { - return nil, err - } - - keyID, err := calculateKeyID(key) - if err != nil { - return nil, err - } - key.keyID = keyID - - return key, nil -} - -func LoadKeyFromFile(path string) (*SSLibKey, error) { - contents, err := os.ReadFile(path) - if err != nil { - return nil, err - } - - return LoadKeyFromBytes(contents) -} - -func calculateKeyID(k *SSLibKey) (string, error) { - canonical, err := cjson.EncodeCanonical(k) - if err != nil { - return "", err - } - digest := sha256.Sum256(canonical) - return hex.EncodeToString(digest[:]), nil -} diff --git a/signerverifier/test-data/rsa-test-key b/signerverifier/test-data/rsa-test-key new file mode 100644 index 0000000..a3c7ba9 --- /dev/null +++ b/signerverifier/test-data/rsa-test-key @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/signerverifier/test-data/rsa-test-key.pub b/signerverifier/test-data/rsa-test-key.pub new file mode 100644 index 0000000..ea6ac89 --- /dev/null +++ b/signerverifier/test-data/rsa-test-key.pub @@ -0,0 +1,11 @@ +-----BEGIN PUBLIC KEY----- +MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA04egZRic+dZMVtiQc56D +ejU4FF1q3aOkUKnD+Q4lTbj1zp6ODKJTcktupmrad68jqtMiSGG8he6ELFs377q8 +bbgEUMWgAf+06Q8oFvUSfOXzZNFI7H5SMPOJY5aDWIMIEZ8DlcO7TfkA7D3iAEJX +xxTOVS3UAIk5umO7Y7t7yXr8O/C4u78krGazCnoblcekMLJZV4O/5BloWNAe/B1c +vZdaZUf3brD4ZZrxEtXw/tefhn1aHsSUajVW2wwjSpKhqj7Z0XS3bDS3T95/3xsN +6+hlS6A7rJfiWpKIRHj0vh2SXLDmmhQl1In8TD/aiycTUyWcBRHVPlYFgYPt6SaT +VQSgMzSxC43/2fINb2fyt8SbUHJ3Ct+mzRzd/1AQikWhBdstJLxInewzjYE/sb+c +2CmCxMPQG2BwmAWXaaumeJcXVPBlMgAcjMatM8bPByTbXpKDnQslOE7g/gswDIwn +Em53T13mZzYUvbLJ0q3aljZVLIC3IZn3ZwA2yCWchBkVAgMBAAE= +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/signerverifier/utils.go b/signerverifier/utils.go new file mode 100644 index 0000000..1cabe22 --- /dev/null +++ b/signerverifier/utils.go @@ -0,0 +1,122 @@ +package signerverifier + +import ( + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/json" + "encoding/pem" + + "github.com/secure-systems-lab/go-securesystemslib/cjson" +) + +// loadKeyFromSSLibBytes returns a pointer to a Key instance created from the +// contents of the bytes. The key contents are expected to be in the custom +// securesystemslib format. +func loadKeyFromSSLibBytes(contents []byte) (*SSLibKey, error) { + var key *SSLibKey + if err := json.Unmarshal(contents, &key); err != nil { + return nil, err + } + + keyID, err := calculateKeyID(key) + if err != nil { + return nil, err + } + key.keyID = keyID + + return key, nil +} + +func calculateKeyID(k *SSLibKey) (string, error) { + key := map[string]any{ + "keytype": k.KeyType, + "scheme": k.Scheme, + "keyid_hash_algorithms": k.KeyIDHashAlgorithms, + "keyval": map[string]string{ + "public": k.KeyVal.Public, + }, + } + canonical, err := cjson.EncodeCanonical(key) + if err != nil { + return "", err + } + digest := sha256.Sum256(canonical) + return hex.EncodeToString(digest[:]), nil +} + +/* +generatePEMBlock creates a PEM block from scratch via the keyBytes and the pemType. +If successful it returns a PEM block as []byte slice. This function should always +succeed, if keyBytes is empty the PEM block will have an empty byte block. +Therefore only header and footer will exist. +*/ +func generatePEMBlock(keyBytes []byte, pemType string) []byte { + // construct PEM block + pemBlock := &pem.Block{ + Type: pemType, + Headers: nil, + Bytes: keyBytes, + } + return pem.EncodeToMemory(pemBlock) +} + +/* +decodeAndParsePEM receives potential PEM bytes decodes them via pem.Decode +and pushes them to parseKey. If any error occurs during this process, +the function will return nil and an error (either ErrFailedPEMParsing +or ErrNoPEMBlock). On success it will return the decoded pemData, the +key object interface and nil as error. We need the decoded pemData, +because LoadKey relies on decoded pemData for operating system +interoperability. +*/ +func decodeAndParsePEM(pemBytes []byte) (*pem.Block, any, error) { + // pem.Decode returns the parsed pem block and a rest. + // The rest is everything, that could not be parsed as PEM block. + // Therefore we can drop this via using the blank identifier "_" + data, _ := pem.Decode(pemBytes) + if data == nil { + return nil, nil, ErrNoPEMBlock + } + + // Try to load private key, if this fails try to load + // key as public key + key, err := parsePEMKey(data.Bytes) + if err != nil { + return nil, nil, err + } + return data, key, nil +} + +/* +parseKey tries to parse a PEM []byte slice. Using the following standards +in the given order: + + - PKCS8 + - PKCS1 + - PKIX + +On success it returns the parsed key and nil. +On failure it returns nil and the error ErrFailedPEMParsing +*/ +func parsePEMKey(data []byte) (any, error) { + key, err := x509.ParsePKCS8PrivateKey(data) + if err == nil { + return key, nil + } + key, err = x509.ParsePKCS1PrivateKey(data) + if err == nil { + return key, nil + } + key, err = x509.ParsePKIXPublicKey(data) + if err == nil { + return key, nil + } + return nil, ErrFailedPEMParsing +} + +func hashBeforeSigning(data []byte) []byte { + h := sha256.New() + h.Write(data) + return h.Sum(nil) +} From 6b0d6ed6c8b9d5b45e68294f3602a218919f934a Mon Sep 17 00:00:00 2001 From: Aditya Sirish Date: Sun, 30 Apr 2023 18:41:59 -0400 Subject: [PATCH 3/8] Add ECDSA SignerVerifier Signed-off-by: Aditya Sirish --- signerverifier/ecdsa.go | 94 ++++++++++++++++ signerverifier/ecdsa_test.go | 118 ++++++++++++++++++++ signerverifier/rsa.go | 9 +- signerverifier/signerverifier.go | 5 + signerverifier/test-data/ecdsa-test-key | 1 + signerverifier/test-data/ecdsa-test-key.pub | 1 + signerverifier/utils.go | 8 +- 7 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 signerverifier/ecdsa.go create mode 100644 signerverifier/ecdsa_test.go create mode 100644 signerverifier/test-data/ecdsa-test-key create mode 100755 signerverifier/test-data/ecdsa-test-key.pub diff --git a/signerverifier/ecdsa.go b/signerverifier/ecdsa.go new file mode 100644 index 0000000..47097bb --- /dev/null +++ b/signerverifier/ecdsa.go @@ -0,0 +1,94 @@ +package signerverifier + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "os" +) + +const ECDSAKeyType = "ecdsa" + +type ECDSASignerVerifier struct { + keyID string + curveSize int + private *ecdsa.PrivateKey + public *ecdsa.PublicKey +} + +func NewECDSASignerVerifierFromSSLibKey(key *SSLibKey) (*ECDSASignerVerifier, error) { + _, publicParsedKey, err := decodeAndParsePEM([]byte(key.KeyVal.Public)) + if err != nil { + return nil, err + } + + sv := &ECDSASignerVerifier{ + keyID: key.KeyID(), + curveSize: publicParsedKey.(*ecdsa.PublicKey).Params().BitSize, + public: publicParsedKey.(*ecdsa.PublicKey), + private: nil, + } + + if len(key.KeyVal.Private) > 0 { + _, privateParsedKey, err := decodeAndParsePEM([]byte(key.KeyVal.Private)) + if err != nil { + return nil, err + } + + sv.private = privateParsedKey.(*ecdsa.PrivateKey) + } + + return sv, nil +} + +func (sv *ECDSASignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) { + if sv.private == nil { + return nil, ErrNotPrivateKey + } + + hashedData := getECDSAHashedData(data, sv.curveSize) + + return ecdsa.SignASN1(rand.Reader, sv.private, hashedData) +} + +func (sv *ECDSASignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error { + hashedData := getECDSAHashedData(data, sv.curveSize) + + if ok := ecdsa.VerifyASN1(sv.public, hashedData, sig); !ok { + return ErrSignatureVerificationFailed + } + + return nil +} + +func (sv *ECDSASignerVerifier) KeyID() (string, error) { + return sv.keyID, nil +} + +func (sv *ECDSASignerVerifier) Public() crypto.PublicKey { + return sv.public +} + +func LoadECDSAKeyFromFile(path string) (*SSLibKey, error) { + contents, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + return loadKeyFromSSLibBytes(contents) +} + +func getECDSAHashedData(data []byte, curveSize int) []byte { + switch { + case curveSize <= 256: + return hashBeforeSigning(data, sha256.New()) + case 256 < curveSize && curveSize <= 384: + return hashBeforeSigning(data, sha512.New384()) + case curveSize > 384: + return hashBeforeSigning(data, sha512.New()) + } + return []byte{} +} diff --git a/signerverifier/ecdsa_test.go b/signerverifier/ecdsa_test.go new file mode 100644 index 0000000..22a4226 --- /dev/null +++ b/signerverifier/ecdsa_test.go @@ -0,0 +1,118 @@ +package signerverifier + +import ( + "context" + "path/filepath" + "testing" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/stretchr/testify/assert" +) + +func TestNewECDSASignerVerifierFromSSLibKey(t *testing.T) { + key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key.pub")) + if err != nil { + t.Fatal(err) + } + + sv, err := NewECDSASignerVerifierFromSSLibKey(key) + if err != nil { + t.Fatal(err) + } + + expectedPublicString := "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----" + _, expectedPublicKey, err := decodeAndParsePEM([]byte(expectedPublicString)) + assert.Nil(t, err) + + assert.Equal(t, "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", sv.keyID) + assert.Equal(t, expectedPublicKey, sv.public) + assert.Nil(t, sv.private) +} + +func TestECDSASignerVerifierSign(t *testing.T) { + t.Run("using valid key", func(t *testing.T) { + key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key")) + if err != nil { + t.Fatal(err) + } + + sv, err := NewECDSASignerVerifierFromSSLibKey(key) + if err != nil { + t.Fatal(err) + } + + message := []byte("test message") + + signature, err := sv.Sign(context.Background(), message) + assert.Nil(t, err) + + err = sv.Verify(context.Background(), message, signature) + assert.Nil(t, err) + }) + + t.Run("using invalid key", func(t *testing.T) { + key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key.pub")) + if err != nil { + t.Fatal(err) + } + + sv, err := NewECDSASignerVerifierFromSSLibKey(key) + if err != nil { + t.Fatal(err) + } + + message := []byte("test message") + + _, err = sv.Sign(context.Background(), message) + assert.ErrorIs(t, err, ErrNotPrivateKey) + }) +} + +func TestECDSASignerVerifierWithDSSEEnvelope(t *testing.T) { + key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key")) + if err != nil { + t.Fatal(err) + } + + sv, err := NewECDSASignerVerifierFromSSLibKey(key) + if err != nil { + t.Fatal(err) + } + + payloadType := "application/vnd.dsse+json" + payload := []byte("test message") + + es, err := dsse.NewEnvelopeSigner(sv) + if err != nil { + t.Error(err) + } + + env, err := es.SignPayload(context.Background(), payloadType, payload) + if err != nil { + t.Error(err) + } + + assert.Equal(t, "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", env.Signatures[0].KeyID) + envPayload, err := env.DecodeB64Payload() + assert.Equal(t, payload, envPayload) + assert.Nil(t, err) + + key, err = LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key.pub")) + if err != nil { + t.Fatal(err) + } + + sv, err = NewECDSASignerVerifierFromSSLibKey(key) + if err != nil { + t.Fatal(err) + } + + ev, err := dsse.NewEnvelopeVerifier(sv) + if err != nil { + t.Error(err) + } + + acceptedKeys, err := ev.Verify(context.Background(), env) + assert.Nil(t, err) + assert.Equal(t, "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", acceptedKeys[0].KeyID) +} diff --git a/signerverifier/rsa.go b/signerverifier/rsa.go index 306b7df..59e2f65 100644 --- a/signerverifier/rsa.go +++ b/signerverifier/rsa.go @@ -20,7 +20,6 @@ var ErrFailedPEMParsing = errors.New("failed parsing the PEM block: unsupported const ( RSAKeyType = "rsa" RSAKeyScheme = "rsassa-pss-sha256" - RSAPublicKeyPEM = "PUBLIC KEY" RSAPrivateKeyPEM = "RSA PRIVATE KEY" ) @@ -61,13 +60,13 @@ func (sv *RSAPSSSignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, return nil, ErrNotPrivateKey } - hashedData := hashBeforeSigning(data) + hashedData := hashBeforeSigning(data, sha256.New()) return rsa.SignPSS(rand.Reader, sv.private, crypto.SHA256, hashedData, &rsa.PSSOptions{SaltLength: sha256.Size, Hash: crypto.SHA256}) } func (sv RSAPSSSignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error { - hashedData := hashBeforeSigning(data) + hashedData := hashBeforeSigning(data, sha256.New()) if err := rsa.VerifyPSS(sv.public, crypto.SHA256, hashedData, sig, &rsa.PSSOptions{SaltLength: sha256.Size, Hash: crypto.SHA256}); err != nil { return ErrSignatureVerificationFailed @@ -108,14 +107,14 @@ func LoadRSAPSSKeyFromFile(path string) (*SSLibKey, error) { if err != nil { return nil, err } - key.KeyVal.Public = string(generatePEMBlock(pubKeyBytes, RSAPublicKeyPEM)) + key.KeyVal.Public = string(generatePEMBlock(pubKeyBytes, PublicKeyPEM)) case *rsa.PrivateKey: pubKeyBytes, err := x509.MarshalPKIXPublicKey(k.Public()) if err != nil { return nil, err } - key.KeyVal.Public = string(generatePEMBlock(pubKeyBytes, RSAPublicKeyPEM)) + key.KeyVal.Public = string(generatePEMBlock(pubKeyBytes, PublicKeyPEM)) key.KeyVal.Private = string(generatePEMBlock(pemData.Bytes, RSAPrivateKeyPEM)) } diff --git a/signerverifier/signerverifier.go b/signerverifier/signerverifier.go index da2f8a7..6dd1d97 100644 --- a/signerverifier/signerverifier.go +++ b/signerverifier/signerverifier.go @@ -13,6 +13,11 @@ var ( ErrInvalidThreshold = errors.New("threshold is either less than 1 or greater than number of provided public keys") ) +const ( + PublicKeyPEM = "PUBLIC KEY" + PrivateKeyPEM = "PRIVATE KEY" +) + type SSLibKey struct { KeyIDHashAlgorithms []string `json:"keyid_hash_algorithms"` KeyType string `json:"keytype"` diff --git a/signerverifier/test-data/ecdsa-test-key b/signerverifier/test-data/ecdsa-test-key new file mode 100644 index 0000000..1feacfc --- /dev/null +++ b/signerverifier/test-data/ecdsa-test-key @@ -0,0 +1 @@ +{"keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid": "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----", "private": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAo6DxXlgqYy+TkvocIOyWlqA3KVtp6dlSY7lS3kkeEMoAoGCCqGSM49\nAwEHoUQDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1XM36oXymJ9wxpM68nCqkrZCV\nnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END EC PRIVATE KEY-----"}, "keyid_hash_algorithms": ["sha256", "sha512"]} \ No newline at end of file diff --git a/signerverifier/test-data/ecdsa-test-key.pub b/signerverifier/test-data/ecdsa-test-key.pub new file mode 100755 index 0000000..4d0e048 --- /dev/null +++ b/signerverifier/test-data/ecdsa-test-key.pub @@ -0,0 +1 @@ +{"keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----"}} \ No newline at end of file diff --git a/signerverifier/utils.go b/signerverifier/utils.go index 1cabe22..1555799 100644 --- a/signerverifier/utils.go +++ b/signerverifier/utils.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "encoding/json" "encoding/pem" + "hash" "github.com/secure-systems-lab/go-securesystemslib/cjson" ) @@ -112,11 +113,14 @@ func parsePEMKey(data []byte) (any, error) { if err == nil { return key, nil } + key, err = x509.ParseECPrivateKey(data) + if err == nil { + return key, nil + } return nil, ErrFailedPEMParsing } -func hashBeforeSigning(data []byte) []byte { - h := sha256.New() +func hashBeforeSigning(data []byte, h hash.Hash) []byte { h.Write(data) return h.Sum(nil) } From e545a0040776f7999f087b6aaefeb7db6ec25d28 Mon Sep 17 00:00:00 2001 From: Aditya Sirish Date: Sun, 30 Apr 2023 19:16:35 -0400 Subject: [PATCH 4/8] ed25519: Fix method receivers, add DSSE test Signed-off-by: Aditya Sirish --- signerverifier/ed25519.go | 24 ++++++------ signerverifier/ed25519_test.go | 72 ++++++++++++++++++++++++++++------ 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/signerverifier/ed25519.go b/signerverifier/ed25519.go index a947bfe..4d20143 100644 --- a/signerverifier/ed25519.go +++ b/signerverifier/ed25519.go @@ -8,17 +8,17 @@ import ( "os" ) -const Ed25519KeyType = "ed25519" +const ED25519KeyType = "ed25519" -type Ed25519SignerVerifier struct { +type ED25519SignerVerifier struct { keyID string private ed25519.PrivateKey public ed25519.PublicKey } -// NewEd25519SignerVerifierFromSSLibKey creates an Ed25519SignerVerifier from an +// NewED25519SignerVerifierFromSSLibKey creates an Ed25519SignerVerifier from an // SSLibKey. -func NewEd25519SignerVerifierFromSSLibKey(key *SSLibKey) (*Ed25519SignerVerifier, error) { +func NewED25519SignerVerifierFromSSLibKey(key *SSLibKey) (*ED25519SignerVerifier, error) { public, err := hex.DecodeString(key.KeyVal.Public) if err != nil { return nil, err @@ -42,7 +42,7 @@ func NewEd25519SignerVerifierFromSSLibKey(key *SSLibKey) (*Ed25519SignerVerifier } } - return &Ed25519SignerVerifier{ + return &ED25519SignerVerifier{ keyID: key.KeyID(), public: ed25519.PublicKey(public), private: ed25519.PrivateKey(private), @@ -50,7 +50,7 @@ func NewEd25519SignerVerifierFromSSLibKey(key *SSLibKey) (*Ed25519SignerVerifier } // Sign creates a signature for `data`. -func (sv *Ed25519SignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) { +func (sv *ED25519SignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) { if len(sv.private) == 0 { return nil, ErrNotPrivateKey } @@ -60,7 +60,7 @@ func (sv *Ed25519SignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, } // Verify verifies the `sig` value passed in against `data`. -func (sv Ed25519SignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error { +func (sv *ED25519SignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error { if ok := ed25519.Verify(sv.public, data, sig); ok { return nil } @@ -68,18 +68,18 @@ func (sv Ed25519SignerVerifier) Verify(ctx context.Context, data []byte, sig []b } // KeyID returns the identifier of the key used to create the -// Ed25519SignerVerifier instance. -func (sv Ed25519SignerVerifier) KeyID() (string, error) { +// ED25519SignerVerifier instance. +func (sv *ED25519SignerVerifier) KeyID() (string, error) { return sv.keyID, nil } // Public returns the public portion of the key used to create the -// Ed25519SignerVerifier instance. -func (sv Ed25519SignerVerifier) Public() crypto.PublicKey { +// ED25519SignerVerifier instance. +func (sv *ED25519SignerVerifier) Public() crypto.PublicKey { return sv.public } -func LoadEd25519KeyFromFile(path string) (*SSLibKey, error) { +func LoadED25519KeyFromFile(path string) (*SSLibKey, error) { contents, err := os.ReadFile(path) if err != nil { return nil, err diff --git a/signerverifier/ed25519_test.go b/signerverifier/ed25519_test.go index 8d028f1..1f122fb 100644 --- a/signerverifier/ed25519_test.go +++ b/signerverifier/ed25519_test.go @@ -7,16 +7,17 @@ import ( "path/filepath" "testing" + "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/stretchr/testify/assert" ) -func TestNewEd25519SignerVerifierFromSSLibKey(t *testing.T) { - key, err := LoadEd25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) +func TestNewED25519SignerVerifierFromSSLibKey(t *testing.T) { + key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) if err != nil { t.Error(err) } - sv, err := NewEd25519SignerVerifierFromSSLibKey(key) + sv, err := NewED25519SignerVerifierFromSSLibKey(key) if err != nil { t.Error(err) } @@ -29,13 +30,13 @@ func TestNewEd25519SignerVerifierFromSSLibKey(t *testing.T) { assert.Nil(t, sv.private) } -func TestEd25519SignerVerifierSign(t *testing.T) { - key, err := LoadEd25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key")) +func TestED25519SignerVerifierSign(t *testing.T) { + key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key")) if err != nil { t.Fatal(err) } - sv, err := NewEd25519SignerVerifierFromSSLibKey(key) + sv, err := NewED25519SignerVerifierFromSSLibKey(key) if err != nil { t.Error(err) } @@ -50,12 +51,12 @@ func TestEd25519SignerVerifierSign(t *testing.T) { expectedSignature := []byte{0x80, 0x72, 0xb4, 0x31, 0xc5, 0xa3, 0x7e, 0xc, 0xf3, 0x91, 0x22, 0x3, 0x60, 0xbf, 0x92, 0xa4, 0x46, 0x31, 0x84, 0x83, 0xf1, 0x31, 0x3, 0xdc, 0xbc, 0x5, 0x6f, 0xab, 0x84, 0xe4, 0xdc, 0xe9, 0xf5, 0x1c, 0xa9, 0xb3, 0x95, 0xa5, 0xa0, 0x16, 0xd3, 0xaa, 0x4d, 0xe7, 0xde, 0xaf, 0xc2, 0x5e, 0x1e, 0x9a, 0x9d, 0xc8, 0xb2, 0x5c, 0x1c, 0x68, 0xf7, 0x28, 0xb4, 0x1, 0x4d, 0x9f, 0xc8, 0x4} assert.Equal(t, expectedSignature, signature) - key, err = LoadEd25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) + key, err = LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) if err != nil { t.Fatal(err) } - sv, err = NewEd25519SignerVerifierFromSSLibKey(key) + sv, err = NewED25519SignerVerifierFromSSLibKey(key) if err != nil { t.Error(err) } @@ -64,13 +65,13 @@ func TestEd25519SignerVerifierSign(t *testing.T) { assert.ErrorIs(t, err, ErrNotPrivateKey) } -func TestEd25519SignerVerifierVerify(t *testing.T) { - key, err := LoadEd25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) +func TestED25519SignerVerifierVerify(t *testing.T) { + key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) if err != nil { t.Fatal(err) } - sv, err := NewEd25519SignerVerifierFromSSLibKey(key) + sv, err := NewED25519SignerVerifierFromSSLibKey(key) if err != nil { t.Error(err) } @@ -92,3 +93,52 @@ func hexDecode(t *testing.T, data string) []byte { } return b } + +func TestED25519SignerVerifierWithDSSEEnvelope(t *testing.T) { + key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key")) + if err != nil { + t.Fatal(err) + } + + sv, err := NewED25519SignerVerifierFromSSLibKey(key) + if err != nil { + t.Fatal(err) + } + + payloadType := "application/vnd.dsse+json" + payload := []byte("test message") + + es, err := dsse.NewEnvelopeSigner(sv) + if err != nil { + t.Error(err) + } + + env, err := es.SignPayload(context.Background(), payloadType, payload) + if err != nil { + t.Error(err) + } + + assert.Equal(t, "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", env.Signatures[0].KeyID) + envPayload, err := env.DecodeB64Payload() + assert.Equal(t, payload, envPayload) + assert.Nil(t, err) + + key, err = LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) + if err != nil { + t.Fatal(err) + } + + sv, err = NewED25519SignerVerifierFromSSLibKey(key) + if err != nil { + t.Fatal(err) + } + + ev, err := dsse.NewEnvelopeVerifier(sv) + if err != nil { + t.Error(err) + } + + acceptedKeys, err := ev.Verify(context.Background(), env) + assert.Nil(t, err) + assert.Equal(t, "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", acceptedKeys[0].KeyID) +} From 6fdab8554bfe7e74d19e8ffb675b6df7f1fc0740 Mon Sep 17 00:00:00 2001 From: Aditya Sirish Date: Sun, 30 Apr 2023 19:16:47 -0400 Subject: [PATCH 5/8] rsa: Fix method receivers, add DSSE test Signed-off-by: Aditya Sirish --- signerverifier/rsa.go | 6 ++--- signerverifier/rsa_test.go | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/signerverifier/rsa.go b/signerverifier/rsa.go index 59e2f65..19cb393 100644 --- a/signerverifier/rsa.go +++ b/signerverifier/rsa.go @@ -65,7 +65,7 @@ func (sv *RSAPSSSignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, return rsa.SignPSS(rand.Reader, sv.private, crypto.SHA256, hashedData, &rsa.PSSOptions{SaltLength: sha256.Size, Hash: crypto.SHA256}) } -func (sv RSAPSSSignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error { +func (sv *RSAPSSSignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error { hashedData := hashBeforeSigning(data, sha256.New()) if err := rsa.VerifyPSS(sv.public, crypto.SHA256, hashedData, sig, &rsa.PSSOptions{SaltLength: sha256.Size, Hash: crypto.SHA256}); err != nil { @@ -75,11 +75,11 @@ func (sv RSAPSSSignerVerifier) Verify(ctx context.Context, data []byte, sig []by return nil } -func (sv RSAPSSSignerVerifier) KeyID() (string, error) { +func (sv *RSAPSSSignerVerifier) KeyID() (string, error) { return sv.keyID, nil } -func (sv RSAPSSSignerVerifier) Public() crypto.PublicKey { +func (sv *RSAPSSSignerVerifier) Public() crypto.PublicKey { return sv.public } diff --git a/signerverifier/rsa_test.go b/signerverifier/rsa_test.go index 72a9325..789713b 100644 --- a/signerverifier/rsa_test.go +++ b/signerverifier/rsa_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "testing" + "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/stretchr/testify/assert" ) @@ -67,3 +68,52 @@ func TestRSAPSSSignerVerifierSignAndVerify(t *testing.T) { assert.ErrorIs(t, err, ErrNotPrivateKey) }) } + +func TestRSAPSSSignerVerifierWithDSSEEnvelope(t *testing.T) { + key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key")) + if err != nil { + t.Fatal(err) + } + + sv, err := NewRSAPSSSignerVerifierFromSSLibKey(key) + if err != nil { + t.Fatal(err) + } + + payloadType := "application/vnd.dsse+json" + payload := []byte("test message") + + es, err := dsse.NewEnvelopeSigner(sv) + if err != nil { + t.Error(err) + } + + env, err := es.SignPayload(context.Background(), payloadType, payload) + if err != nil { + t.Error(err) + } + + assert.Equal(t, "966c5d84ba73ccded42eb473c939d77336e4def253ffaf6739f8e983ef73dad8", env.Signatures[0].KeyID) + envPayload, err := env.DecodeB64Payload() + assert.Equal(t, payload, envPayload) + assert.Nil(t, err) + + key, err = LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key.pub")) + if err != nil { + t.Fatal(err) + } + + sv, err = NewRSAPSSSignerVerifierFromSSLibKey(key) + if err != nil { + t.Fatal(err) + } + + ev, err := dsse.NewEnvelopeVerifier(sv) + if err != nil { + t.Error(err) + } + + acceptedKeys, err := ev.Verify(context.Background(), env) + assert.Nil(t, err) + assert.Equal(t, "966c5d84ba73ccded42eb473c939d77336e4def253ffaf6739f8e983ef73dad8", acceptedKeys[0].KeyID) +} From 7d8d7bcf3ad4e4e6dbbef7b5be27ee9ab1f2531b Mon Sep 17 00:00:00 2001 From: Aditya Sirish Date: Sun, 30 Apr 2023 19:23:07 -0400 Subject: [PATCH 6/8] Drop Go 1.17.x Signed-off-by: Aditya Sirish --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2285d10..a14de0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ jobs: test: strategy: matrix: - go-version: [1.17.x, 1.18.x, 1.19.x, 1.20.x] + go-version: [1.18.x, 1.19.x, 1.20.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: From 1c3e073fcfccb79f511f192abbd85fb2bc84006c Mon Sep 17 00:00:00 2001 From: Aditya Sirish Date: Mon, 1 May 2023 10:09:10 -0400 Subject: [PATCH 7/8] Multiple changes * Address review comments from @pxp928 * Add docstrings * Add credits for code from in-toto-golang * Check if KeyVal is missing when creating signerverifiers * Make KeyID public and calculate key ID while loading only if it's missing Signed-off-by: Aditya Sirish --- signerverifier/ecdsa.go | 25 ++++++++-- signerverifier/ed25519.go | 17 +++++-- signerverifier/rsa.go | 50 ++++++++++++------- signerverifier/signerverifier.go | 7 +-- signerverifier/test-data/ecdsa-test-key | 2 +- signerverifier/test-data/ecdsa-test-key.pub | 2 +- signerverifier/test-data/ed25519-test-key | 2 +- signerverifier/test-data/ed25519-test-key.pub | 2 +- signerverifier/test-data/rsa-test-key | 2 +- signerverifier/test-data/rsa-test-key.pub | 2 +- signerverifier/utils.go | 22 ++++++-- 11 files changed, 91 insertions(+), 42 deletions(-) diff --git a/signerverifier/ecdsa.go b/signerverifier/ecdsa.go index 47097bb..578d6a5 100644 --- a/signerverifier/ecdsa.go +++ b/signerverifier/ecdsa.go @@ -7,11 +7,14 @@ import ( "crypto/rand" "crypto/sha256" "crypto/sha512" + "fmt" "os" ) const ECDSAKeyType = "ecdsa" +// ECDSASignerVerifier is a dsse.SignerVerifier compliant interface to sign and +// verify signatures using ECDSA keys. type ECDSASignerVerifier struct { keyID string curveSize int @@ -19,14 +22,20 @@ type ECDSASignerVerifier struct { public *ecdsa.PublicKey } +// NewECDSASignerVerifierFromSSLibKey creates an ECDSASignerVerifier from an +// SSLibKey. func NewECDSASignerVerifierFromSSLibKey(key *SSLibKey) (*ECDSASignerVerifier, error) { + if len(key.KeyVal.Public) == 0 { + return nil, ErrInvalidKey + } + _, publicParsedKey, err := decodeAndParsePEM([]byte(key.KeyVal.Public)) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to create ECDSA signerverifier: %w", err) } sv := &ECDSASignerVerifier{ - keyID: key.KeyID(), + keyID: key.KeyID, curveSize: publicParsedKey.(*ecdsa.PublicKey).Params().BitSize, public: publicParsedKey.(*ecdsa.PublicKey), private: nil, @@ -35,7 +44,7 @@ func NewECDSASignerVerifierFromSSLibKey(key *SSLibKey) (*ECDSASignerVerifier, er if len(key.KeyVal.Private) > 0 { _, privateParsedKey, err := decodeAndParsePEM([]byte(key.KeyVal.Private)) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to create ECDSA signerverifier: %w", err) } sv.private = privateParsedKey.(*ecdsa.PrivateKey) @@ -44,6 +53,7 @@ func NewECDSASignerVerifierFromSSLibKey(key *SSLibKey) (*ECDSASignerVerifier, er return sv, nil } +// Sign creates a signature for `data`. func (sv *ECDSASignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) { if sv.private == nil { return nil, ErrNotPrivateKey @@ -54,6 +64,7 @@ func (sv *ECDSASignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, e return ecdsa.SignASN1(rand.Reader, sv.private, hashedData) } +// Verify verifies the `sig` value passed in against `data`. func (sv *ECDSASignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error { hashedData := getECDSAHashedData(data, sv.curveSize) @@ -64,18 +75,24 @@ func (sv *ECDSASignerVerifier) Verify(ctx context.Context, data []byte, sig []by return nil } +// KeyID returns the identifier of the key used to create the +// ECDSASignerVerifier instance. func (sv *ECDSASignerVerifier) KeyID() (string, error) { return sv.keyID, nil } +// Public returns the public portion of the key used to create the +// ECDSASignerVerifier instance. func (sv *ECDSASignerVerifier) Public() crypto.PublicKey { return sv.public } +// LoadECDSAKeyFromFile returns an SSLibKey instance for an ECDSA key stored in +// a file in the custom securesystemslib format. func LoadECDSAKeyFromFile(path string) (*SSLibKey, error) { contents, err := os.ReadFile(path) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to load ECDSA key from file: %w", err) } return loadKeyFromSSLibBytes(contents) diff --git a/signerverifier/ed25519.go b/signerverifier/ed25519.go index 4d20143..c71d313 100644 --- a/signerverifier/ed25519.go +++ b/signerverifier/ed25519.go @@ -5,11 +5,14 @@ import ( "crypto" "crypto/ed25519" "encoding/hex" + "fmt" "os" ) const ED25519KeyType = "ed25519" +// ED25519SignerVerifier is a dsse.SignerVerifier compliant interface to sign +// and verify signatures using ED25519 keys. type ED25519SignerVerifier struct { keyID string private ed25519.PrivateKey @@ -19,16 +22,20 @@ type ED25519SignerVerifier struct { // NewED25519SignerVerifierFromSSLibKey creates an Ed25519SignerVerifier from an // SSLibKey. func NewED25519SignerVerifierFromSSLibKey(key *SSLibKey) (*ED25519SignerVerifier, error) { + if len(key.KeyVal.Public) == 0 { + return nil, ErrInvalidKey + } + public, err := hex.DecodeString(key.KeyVal.Public) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to create ED25519 signerverifier: %w", err) } var private []byte if len(key.KeyVal.Private) > 0 { private, err = hex.DecodeString(key.KeyVal.Private) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to create ED25519 signerverifier: %w", err) } // python-securesystemslib provides an interface to generate ed25519 @@ -43,7 +50,7 @@ func NewED25519SignerVerifierFromSSLibKey(key *SSLibKey) (*ED25519SignerVerifier } return &ED25519SignerVerifier{ - keyID: key.KeyID(), + keyID: key.KeyID, public: ed25519.PublicKey(public), private: ed25519.PrivateKey(private), }, nil @@ -79,10 +86,12 @@ func (sv *ED25519SignerVerifier) Public() crypto.PublicKey { return sv.public } +// LoadED25519KeyFromFile returns an SSLibKey instance for an ED25519 key stored +// in a file in the custom securesystemslib format. func LoadED25519KeyFromFile(path string) (*SSLibKey, error) { contents, err := os.ReadFile(path) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to load ED25519 key from file: %w", err) } return loadKeyFromSSLibBytes(contents) diff --git a/signerverifier/rsa.go b/signerverifier/rsa.go index 19cb393..954c8d7 100644 --- a/signerverifier/rsa.go +++ b/signerverifier/rsa.go @@ -7,54 +7,57 @@ import ( "crypto/rsa" "crypto/sha256" "crypto/x509" - "errors" + "fmt" "os" ) -// ErrNoPEMBlock gets triggered when there is no PEM block in the provided file -var ErrNoPEMBlock = errors.New("failed to decode the data as PEM block (are you sure this is a pem file?)") - -// ErrFailedPEMParsing gets returned when PKCS1, PKCS8 or PKIX key parsing fails -var ErrFailedPEMParsing = errors.New("failed parsing the PEM block: unsupported PEM type") - const ( RSAKeyType = "rsa" RSAKeyScheme = "rsassa-pss-sha256" RSAPrivateKeyPEM = "RSA PRIVATE KEY" ) +// RSAPSSSignerVerifier is a dsse.SignerVerifier compliant interface to sign and +// verify signatures using RSA keys following the RSA-PSS scheme. type RSAPSSSignerVerifier struct { keyID string private *rsa.PrivateKey public *rsa.PublicKey } +// NewRSAPSSSignerVerifierFromSSLibKey creates an RSAPSSSignerVerifier from an +// SSLibKey. func NewRSAPSSSignerVerifierFromSSLibKey(key *SSLibKey) (*RSAPSSSignerVerifier, error) { + if len(key.KeyVal.Public) == 0 { + return nil, ErrInvalidKey + } + _, publicParsedKey, err := decodeAndParsePEM([]byte(key.KeyVal.Public)) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to create RSA-PSS signerverifier: %w", err) } if len(key.KeyVal.Private) > 0 { _, privateParsedKey, err := decodeAndParsePEM([]byte(key.KeyVal.Private)) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to create RSA-PSS signerverifier: %w", err) } return &RSAPSSSignerVerifier{ - keyID: key.KeyID(), + keyID: key.KeyID, public: publicParsedKey.(*rsa.PublicKey), private: privateParsedKey.(*rsa.PrivateKey), }, nil } return &RSAPSSSignerVerifier{ - keyID: key.KeyID(), + keyID: key.KeyID, public: publicParsedKey.(*rsa.PublicKey), private: nil, }, nil } +// Sign creates a signature for `data`. func (sv *RSAPSSSignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) { if sv.private == nil { return nil, ErrNotPrivateKey @@ -65,6 +68,7 @@ func (sv *RSAPSSSignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, return rsa.SignPSS(rand.Reader, sv.private, crypto.SHA256, hashedData, &rsa.PSSOptions{SaltLength: sha256.Size, Hash: crypto.SHA256}) } +// Verify verifies the `sig` value passed in against `data`. func (sv *RSAPSSSignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error { hashedData := hashBeforeSigning(data, sha256.New()) @@ -75,23 +79,29 @@ func (sv *RSAPSSSignerVerifier) Verify(ctx context.Context, data []byte, sig []b return nil } +// KeyID returns the identifier of the key used to create the +// RSAPSSSignerVerifier instance. func (sv *RSAPSSSignerVerifier) KeyID() (string, error) { return sv.keyID, nil } +// Public returns the public portion of the key used to create the +// RSAPSSSignerVerifier instance. func (sv *RSAPSSSignerVerifier) Public() crypto.PublicKey { return sv.public } +// LoadRSAPSSKeyFromFile returns an SSLibKey instance for an RSA key stored in a +// file. func LoadRSAPSSKeyFromFile(path string) (*SSLibKey, error) { contents, err := os.ReadFile(path) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to load RSA PSS key from file: %w", err) } pemData, keyObj, err := decodeAndParsePEM(contents) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to load RSA PSS key from file: %w", err) } key := &SSLibKey{ @@ -105,24 +115,26 @@ func LoadRSAPSSKeyFromFile(path string) (*SSLibKey, error) { case *rsa.PublicKey: pubKeyBytes, err := x509.MarshalPKIXPublicKey(k) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to load RSA PSS key from file: %w", err) } key.KeyVal.Public = string(generatePEMBlock(pubKeyBytes, PublicKeyPEM)) case *rsa.PrivateKey: pubKeyBytes, err := x509.MarshalPKIXPublicKey(k.Public()) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to load RSA PSS key from file: %w", err) } key.KeyVal.Public = string(generatePEMBlock(pubKeyBytes, PublicKeyPEM)) key.KeyVal.Private = string(generatePEMBlock(pemData.Bytes, RSAPrivateKeyPEM)) } - keyID, err := calculateKeyID(key) - if err != nil { - return nil, err + if len(key.KeyID) == 0 { + keyID, err := calculateKeyID(key) + if err != nil { + return nil, fmt.Errorf("unable to load RSA PSS key from file: %w", err) + } + key.KeyID = keyID } - key.keyID = keyID return key, nil } diff --git a/signerverifier/signerverifier.go b/signerverifier/signerverifier.go index 6dd1d97..5f510f7 100644 --- a/signerverifier/signerverifier.go +++ b/signerverifier/signerverifier.go @@ -11,6 +11,7 @@ var ( ErrSignatureVerificationFailed = errors.New("failed to verify signature") ErrUnknownKeyType = errors.New("unknown key type") ErrInvalidThreshold = errors.New("threshold is either less than 1 or greater than number of provided public keys") + ErrInvalidKey = errors.New("key object has no value") ) const ( @@ -23,11 +24,7 @@ type SSLibKey struct { KeyType string `json:"keytype"` KeyVal KeyVal `json:"keyval"` Scheme string `json:"scheme"` - keyID string -} - -func (k *SSLibKey) KeyID() string { - return k.keyID + KeyID string `json:"keyid"` } type KeyVal struct { diff --git a/signerverifier/test-data/ecdsa-test-key b/signerverifier/test-data/ecdsa-test-key index 1feacfc..8737bc8 100644 --- a/signerverifier/test-data/ecdsa-test-key +++ b/signerverifier/test-data/ecdsa-test-key @@ -1 +1 @@ -{"keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid": "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----", "private": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAo6DxXlgqYy+TkvocIOyWlqA3KVtp6dlSY7lS3kkeEMoAoGCCqGSM49\nAwEHoUQDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1XM36oXymJ9wxpM68nCqkrZCV\nnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END EC PRIVATE KEY-----"}, "keyid_hash_algorithms": ["sha256", "sha512"]} \ No newline at end of file +{"keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid": "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----", "private": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAo6DxXlgqYy+TkvocIOyWlqA3KVtp6dlSY7lS3kkeEMoAoGCCqGSM49\nAwEHoUQDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1XM36oXymJ9wxpM68nCqkrZCV\nnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END EC PRIVATE KEY-----"}, "keyid_hash_algorithms": ["sha256", "sha512"]} diff --git a/signerverifier/test-data/ecdsa-test-key.pub b/signerverifier/test-data/ecdsa-test-key.pub index 4d0e048..cc695db 100755 --- a/signerverifier/test-data/ecdsa-test-key.pub +++ b/signerverifier/test-data/ecdsa-test-key.pub @@ -1 +1 @@ -{"keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----"}} \ No newline at end of file +{"keytype": "ecdsa", "scheme": "ecdsa-sha2-nistp256", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----"}} diff --git a/signerverifier/test-data/ed25519-test-key b/signerverifier/test-data/ed25519-test-key index 4d6a130..da14a8e 100644 --- a/signerverifier/test-data/ed25519-test-key +++ b/signerverifier/test-data/ed25519-test-key @@ -1 +1 @@ -{"keytype": "ed25519", "scheme": "ed25519", "keyid": "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f", "private": "66f6ebad4aeb949b91c84c9cfd6ee351fc4fd544744bab6e30fb400ba13c6e9a"}} \ No newline at end of file +{"keytype": "ed25519", "scheme": "ed25519", "keyid": "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f", "private": "66f6ebad4aeb949b91c84c9cfd6ee351fc4fd544744bab6e30fb400ba13c6e9a"}} diff --git a/signerverifier/test-data/ed25519-test-key.pub b/signerverifier/test-data/ed25519-test-key.pub index 48e944e..7ec2a0c 100644 --- a/signerverifier/test-data/ed25519-test-key.pub +++ b/signerverifier/test-data/ed25519-test-key.pub @@ -1 +1 @@ -{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f"}} \ No newline at end of file +{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f"}} diff --git a/signerverifier/test-data/rsa-test-key b/signerverifier/test-data/rsa-test-key index a3c7ba9..5571e86 100644 --- a/signerverifier/test-data/rsa-test-key +++ b/signerverifier/test-data/rsa-test-key @@ -36,4 +36,4 @@ AhhasnrL3Pzxf0WKzKmj/y2yEP0Vctm0muqxFnFwPwyOAd6HODJOSiFPD5VN4jvC +Yw96Qn29kHGXTKgL1J9cSL8z6Qzlc+UYCdSwmaZK5r36+NBTJgvKY9KrpkXCkSa c5YgIYtXMitmq9NmNvcSJWmuuiept3HFlwkU3pfmwzKNEeqi2jmuIOqI2zCOqX67 I+YQsJgrHE0TmYxxRkgeYUy7s5DoHE25rfvdy5Lx+xAOH8ZgD1SGOw== ------END RSA PRIVATE KEY----- \ No newline at end of file +-----END RSA PRIVATE KEY----- diff --git a/signerverifier/test-data/rsa-test-key.pub b/signerverifier/test-data/rsa-test-key.pub index ea6ac89..3482484 100644 --- a/signerverifier/test-data/rsa-test-key.pub +++ b/signerverifier/test-data/rsa-test-key.pub @@ -8,4 +8,4 @@ vZdaZUf3brD4ZZrxEtXw/tefhn1aHsSUajVW2wwjSpKhqj7Z0XS3bDS3T95/3xsN VQSgMzSxC43/2fINb2fyt8SbUHJ3Ct+mzRzd/1AQikWhBdstJLxInewzjYE/sb+c 2CmCxMPQG2BwmAWXaaumeJcXVPBlMgAcjMatM8bPByTbXpKDnQslOE7g/gswDIwn Em53T13mZzYUvbLJ0q3aljZVLIC3IZn3ZwA2yCWchBkVAgMBAAE= ------END PUBLIC KEY----- \ No newline at end of file +-----END PUBLIC KEY----- diff --git a/signerverifier/utils.go b/signerverifier/utils.go index 1555799..18e8e3c 100644 --- a/signerverifier/utils.go +++ b/signerverifier/utils.go @@ -6,11 +6,23 @@ import ( "encoding/hex" "encoding/json" "encoding/pem" + "errors" "hash" "github.com/secure-systems-lab/go-securesystemslib/cjson" ) +/* +Credits: Parts of this file were originally authored for in-toto-golang. +*/ + +var ( + // ErrNoPEMBlock gets triggered when there is no PEM block in the provided file + ErrNoPEMBlock = errors.New("failed to decode the data as PEM block (are you sure this is a pem file?)") + // ErrFailedPEMParsing gets returned when PKCS1, PKCS8 or PKIX key parsing fails + ErrFailedPEMParsing = errors.New("failed parsing the PEM block: unsupported PEM type") +) + // loadKeyFromSSLibBytes returns a pointer to a Key instance created from the // contents of the bytes. The key contents are expected to be in the custom // securesystemslib format. @@ -20,11 +32,13 @@ func loadKeyFromSSLibBytes(contents []byte) (*SSLibKey, error) { return nil, err } - keyID, err := calculateKeyID(key) - if err != nil { - return nil, err + if len(key.KeyID) == 0 { + keyID, err := calculateKeyID(key) + if err != nil { + return nil, err + } + key.KeyID = keyID } - key.keyID = keyID return key, nil } From abf9e07db81556bb50cbef6b69be70929d13d5e0 Mon Sep 17 00:00:00 2001 From: Aditya Sirish Date: Mon, 1 May 2023 12:18:06 -0400 Subject: [PATCH 8/8] Test cross compatibility * Add tests verifying Metablock signatures * Fix RSA key load with trailing newline Signed-off-by: Aditya Sirish --- signerverifier/ecdsa_test.go | 73 ++++++++++++++++ signerverifier/ed25519_test.go | 83 ++++++++++++++++--- signerverifier/rsa.go | 17 ++-- signerverifier/rsa_test.go | 80 +++++++++++++++++- .../test-data/test-ecdsa.98adf386.link | 17 ++++ .../test-data/test-ed25519.52e3b8e7.link | 17 ++++ .../test-data/test-rsa.4e8d20af.link | 17 ++++ signerverifier/utils.go | 10 +++ 8 files changed, 293 insertions(+), 21 deletions(-) create mode 100644 signerverifier/test-data/test-ecdsa.98adf386.link create mode 100644 signerverifier/test-data/test-ed25519.52e3b8e7.link create mode 100644 signerverifier/test-data/test-rsa.4e8d20af.link diff --git a/signerverifier/ecdsa_test.go b/signerverifier/ecdsa_test.go index 22a4226..71163dc 100644 --- a/signerverifier/ecdsa_test.go +++ b/signerverifier/ecdsa_test.go @@ -2,9 +2,12 @@ package signerverifier import ( "context" + "encoding/json" + "os" "path/filepath" "testing" + "github.com/secure-systems-lab/go-securesystemslib/cjson" "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/stretchr/testify/assert" ) @@ -29,6 +32,34 @@ func TestNewECDSASignerVerifierFromSSLibKey(t *testing.T) { assert.Nil(t, sv.private) } +func TestLoadECDSAKeyFromFile(t *testing.T) { + t.Run("ecdsa public key", func(t *testing.T) { + key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key.pub")) + assert.Nil(t, err) + + assert.Equal(t, "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", key.KeyID) + assert.Equal(t, "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----", key.KeyVal.Public) + assert.Equal(t, "ecdsa-sha2-nistp256", key.Scheme) + assert.Equal(t, ECDSAKeyType, key.KeyType) + }) + + t.Run("ecdsa private key", func(t *testing.T) { + key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key")) + assert.Nil(t, err) + + assert.Equal(t, "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", key.KeyID) + assert.Equal(t, "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1\nXM36oXymJ9wxpM68nCqkrZCVnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END PUBLIC KEY-----", key.KeyVal.Public) + assert.Equal(t, "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAo6DxXlgqYy+TkvocIOyWlqA3KVtp6dlSY7lS3kkeEMoAoGCCqGSM49\nAwEHoUQDQgAEu+HEqqpXLa48lXH9rkRygsfsCKq1XM36oXymJ9wxpM68nCqkrZCV\nnZ9lkEeCwD8qWYTNxD5yfWXwJjFh+K7qLQ==\n-----END EC PRIVATE KEY-----", key.KeyVal.Private) + assert.Equal(t, "ecdsa-sha2-nistp256", key.Scheme) + assert.Equal(t, ECDSAKeyType, key.KeyType) + }) + + t.Run("invalid path", func(t *testing.T) { + _, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "invalid")) + assert.ErrorContains(t, err, "unable to load ECDSA key from file") + }) +} + func TestECDSASignerVerifierSign(t *testing.T) { t.Run("using valid key", func(t *testing.T) { key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key")) @@ -116,3 +147,45 @@ func TestECDSASignerVerifierWithDSSEEnvelope(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", acceptedKeys[0].KeyID) } + +func TestECDSASignerVerifierWithMetablockFile(t *testing.T) { + key, err := LoadECDSAKeyFromFile(filepath.Join("test-data", "ecdsa-test-key.pub")) + if err != nil { + t.Fatal(err) + } + + sv, err := NewECDSASignerVerifierFromSSLibKey(key) + if err != nil { + t.Fatal(err) + } + + metadataBytes, err := os.ReadFile(filepath.Join("test-data", "test-ecdsa.98adf386.link")) + if err != nil { + t.Fatal(err) + } + + mb := struct { + Signatures []struct { + KeyID string `json:"keyid"` + Sig string `json:"sig"` + } `json:"signatures"` + Signed any `json:"signed"` + }{} + + if err := json.Unmarshal(metadataBytes, &mb); err != nil { + t.Fatal(err) + } + + assert.Equal(t, "304502201fbb03c0937504182a48c66f9218bdcb2e99a07ada273e92e5e543867f98c8d7022100dbfa7bbf74fd76d76c1d08676419cba85bbd81dfb000f3ac6a786693ddc508f5", mb.Signatures[0].Sig) + assert.Equal(t, sv.keyID, mb.Signatures[0].KeyID) + + encodedBytes, err := cjson.EncodeCanonical(mb.Signed) + if err != nil { + t.Fatal(err) + } + + decodedSig := hexDecode(t, mb.Signatures[0].Sig) + + err = sv.Verify(context.Background(), encodedBytes, decodedSig) + assert.Nil(t, err) +} diff --git a/signerverifier/ed25519_test.go b/signerverifier/ed25519_test.go index 1f122fb..19cf91e 100644 --- a/signerverifier/ed25519_test.go +++ b/signerverifier/ed25519_test.go @@ -3,10 +3,12 @@ package signerverifier import ( "context" "crypto/ed25519" - "encoding/hex" + "encoding/json" + "os" "path/filepath" "testing" + "github.com/secure-systems-lab/go-securesystemslib/cjson" "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/stretchr/testify/assert" ) @@ -30,6 +32,34 @@ func TestNewED25519SignerVerifierFromSSLibKey(t *testing.T) { assert.Nil(t, sv.private) } +func TestLoadED25519KeyFromFile(t *testing.T) { + t.Run("ED25519 public key", func(t *testing.T) { + key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) + assert.Nil(t, err) + + assert.Equal(t, "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", key.KeyID) + assert.Equal(t, "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f", key.KeyVal.Public) + assert.Equal(t, "ed25519", key.Scheme) + assert.Equal(t, ED25519KeyType, key.KeyType) + }) + + t.Run("ED25519 private key", func(t *testing.T) { + key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key")) + assert.Nil(t, err) + + assert.Equal(t, "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", key.KeyID) + assert.Equal(t, "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f", key.KeyVal.Public) + assert.Equal(t, "66f6ebad4aeb949b91c84c9cfd6ee351fc4fd544744bab6e30fb400ba13c6e9a", key.KeyVal.Private) + assert.Equal(t, "ed25519", key.Scheme) + assert.Equal(t, ED25519KeyType, key.KeyType) + }) + + t.Run("invalid path", func(t *testing.T) { + _, err := LoadED25519KeyFromFile(filepath.Join("test-data", "invalid")) + assert.ErrorContains(t, err, "unable to load ED25519 key from file") + }) +} + func TestED25519SignerVerifierSign(t *testing.T) { key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key")) if err != nil { @@ -85,15 +115,6 @@ func TestED25519SignerVerifierVerify(t *testing.T) { assert.ErrorIs(t, err, ErrSignatureVerificationFailed) } -func hexDecode(t *testing.T, data string) []byte { - t.Helper() - b, err := hex.DecodeString(data) - if err != nil { - t.Fatal(err) - } - return b -} - func TestED25519SignerVerifierWithDSSEEnvelope(t *testing.T) { key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key")) if err != nil { @@ -142,3 +163,45 @@ func TestED25519SignerVerifierWithDSSEEnvelope(t *testing.T) { assert.Nil(t, err) assert.Equal(t, "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", acceptedKeys[0].KeyID) } + +func TestED25519SignerVerifierWithMetablockFile(t *testing.T) { + key, err := LoadED25519KeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub")) + if err != nil { + t.Fatal(err) + } + + sv, err := NewED25519SignerVerifierFromSSLibKey(key) + if err != nil { + t.Fatal(err) + } + + metadataBytes, err := os.ReadFile(filepath.Join("test-data", "test-ed25519.52e3b8e7.link")) + if err != nil { + t.Fatal(err) + } + + mb := struct { + Signatures []struct { + KeyID string `json:"keyid"` + Sig string `json:"sig"` + } `json:"signatures"` + Signed any `json:"signed"` + }{} + + if err := json.Unmarshal(metadataBytes, &mb); err != nil { + t.Fatal(err) + } + + assert.Equal(t, "4c8b7605a9195d4ddba54493bbb5257a9836c1d16056a027fd77e97b95a4f3e36f8bc3c9c9960387d68187760b3072a30c44f992c5bf8f7497c303a3b0a32403", mb.Signatures[0].Sig) + assert.Equal(t, sv.keyID, mb.Signatures[0].KeyID) + + encodedBytes, err := cjson.EncodeCanonical(mb.Signed) + if err != nil { + t.Fatal(err) + } + + decodedSig := hexDecode(t, mb.Signatures[0].Sig) + + err = sv.Verify(context.Background(), encodedBytes, decodedSig) + assert.Nil(t, err) +} diff --git a/signerverifier/rsa.go b/signerverifier/rsa.go index 954c8d7..3612f28 100644 --- a/signerverifier/rsa.go +++ b/signerverifier/rsa.go @@ -9,6 +9,7 @@ import ( "crypto/x509" "fmt" "os" + "strings" ) const ( @@ -96,12 +97,12 @@ func (sv *RSAPSSSignerVerifier) Public() crypto.PublicKey { func LoadRSAPSSKeyFromFile(path string) (*SSLibKey, error) { contents, err := os.ReadFile(path) if err != nil { - return nil, fmt.Errorf("unable to load RSA PSS key from file: %w", err) + return nil, fmt.Errorf("unable to load RSA key from file: %w", err) } pemData, keyObj, err := decodeAndParsePEM(contents) if err != nil { - return nil, fmt.Errorf("unable to load RSA PSS key from file: %w", err) + return nil, fmt.Errorf("unable to load RSA key from file: %w", err) } key := &SSLibKey{ @@ -115,23 +116,23 @@ func LoadRSAPSSKeyFromFile(path string) (*SSLibKey, error) { case *rsa.PublicKey: pubKeyBytes, err := x509.MarshalPKIXPublicKey(k) if err != nil { - return nil, fmt.Errorf("unable to load RSA PSS key from file: %w", err) + return nil, fmt.Errorf("unable to load RSA key from file: %w", err) } - key.KeyVal.Public = string(generatePEMBlock(pubKeyBytes, PublicKeyPEM)) + key.KeyVal.Public = strings.TrimSpace(string(generatePEMBlock(pubKeyBytes, PublicKeyPEM))) case *rsa.PrivateKey: pubKeyBytes, err := x509.MarshalPKIXPublicKey(k.Public()) if err != nil { - return nil, fmt.Errorf("unable to load RSA PSS key from file: %w", err) + return nil, fmt.Errorf("unable to load RSA key from file: %w", err) } - key.KeyVal.Public = string(generatePEMBlock(pubKeyBytes, PublicKeyPEM)) - key.KeyVal.Private = string(generatePEMBlock(pemData.Bytes, RSAPrivateKeyPEM)) + key.KeyVal.Public = strings.TrimSpace(string(generatePEMBlock(pubKeyBytes, PublicKeyPEM))) + key.KeyVal.Private = strings.TrimSpace(string(generatePEMBlock(pemData.Bytes, RSAPrivateKeyPEM))) } if len(key.KeyID) == 0 { keyID, err := calculateKeyID(key) if err != nil { - return nil, fmt.Errorf("unable to load RSA PSS key from file: %w", err) + return nil, fmt.Errorf("unable to load RSA key from file: %w", err) } key.KeyID = keyID } diff --git a/signerverifier/rsa_test.go b/signerverifier/rsa_test.go index 789713b..b8711d8 100644 --- a/signerverifier/rsa_test.go +++ b/signerverifier/rsa_test.go @@ -3,9 +3,12 @@ package signerverifier import ( "context" "crypto/rsa" + "encoding/json" + "os" "path/filepath" "testing" + "github.com/secure-systems-lab/go-securesystemslib/cjson" "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/stretchr/testify/assert" ) @@ -25,11 +28,40 @@ func TestNewRSAPSSSignerVerifierFromSSLibKey(t *testing.T) { _, expectedPublicKey, err := decodeAndParsePEM([]byte(expectedPublicString)) assert.Nil(t, err) - assert.Equal(t, "966c5d84ba73ccded42eb473c939d77336e4def253ffaf6739f8e983ef73dad8", sv.keyID) // FIXME: mismatch? + assert.Equal(t, "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", sv.keyID) assert.Equal(t, expectedPublicKey.(*rsa.PublicKey), sv.public) assert.Nil(t, sv.private) } +func TestLoadRSAPSSKeyFromFile(t *testing.T) { + t.Run("RSA public key", func(t *testing.T) { + key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key.pub")) + assert.Nil(t, err) + + assert.Equal(t, "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", key.KeyID) + assert.Equal(t, "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA04egZRic+dZMVtiQc56D\nejU4FF1q3aOkUKnD+Q4lTbj1zp6ODKJTcktupmrad68jqtMiSGG8he6ELFs377q8\nbbgEUMWgAf+06Q8oFvUSfOXzZNFI7H5SMPOJY5aDWIMIEZ8DlcO7TfkA7D3iAEJX\nxxTOVS3UAIk5umO7Y7t7yXr8O/C4u78krGazCnoblcekMLJZV4O/5BloWNAe/B1c\nvZdaZUf3brD4ZZrxEtXw/tefhn1aHsSUajVW2wwjSpKhqj7Z0XS3bDS3T95/3xsN\n6+hlS6A7rJfiWpKIRHj0vh2SXLDmmhQl1In8TD/aiycTUyWcBRHVPlYFgYPt6SaT\nVQSgMzSxC43/2fINb2fyt8SbUHJ3Ct+mzRzd/1AQikWhBdstJLxInewzjYE/sb+c\n2CmCxMPQG2BwmAWXaaumeJcXVPBlMgAcjMatM8bPByTbXpKDnQslOE7g/gswDIwn\nEm53T13mZzYUvbLJ0q3aljZVLIC3IZn3ZwA2yCWchBkVAgMBAAE=\n-----END PUBLIC KEY-----", key.KeyVal.Public) + assert.Equal(t, RSAKeyScheme, key.Scheme) + assert.Equal(t, RSAKeyType, key.KeyType) + }) + + t.Run("RSA private key", func(t *testing.T) { + key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key")) + assert.Nil(t, err) + + assert.Equal(t, "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", key.KeyID) + assert.Equal(t, "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA04egZRic+dZMVtiQc56D\nejU4FF1q3aOkUKnD+Q4lTbj1zp6ODKJTcktupmrad68jqtMiSGG8he6ELFs377q8\nbbgEUMWgAf+06Q8oFvUSfOXzZNFI7H5SMPOJY5aDWIMIEZ8DlcO7TfkA7D3iAEJX\nxxTOVS3UAIk5umO7Y7t7yXr8O/C4u78krGazCnoblcekMLJZV4O/5BloWNAe/B1c\nvZdaZUf3brD4ZZrxEtXw/tefhn1aHsSUajVW2wwjSpKhqj7Z0XS3bDS3T95/3xsN\n6+hlS6A7rJfiWpKIRHj0vh2SXLDmmhQl1In8TD/aiycTUyWcBRHVPlYFgYPt6SaT\nVQSgMzSxC43/2fINb2fyt8SbUHJ3Ct+mzRzd/1AQikWhBdstJLxInewzjYE/sb+c\n2CmCxMPQG2BwmAWXaaumeJcXVPBlMgAcjMatM8bPByTbXpKDnQslOE7g/gswDIwn\nEm53T13mZzYUvbLJ0q3aljZVLIC3IZn3ZwA2yCWchBkVAgMBAAE=\n-----END PUBLIC KEY-----", key.KeyVal.Public) + expectedPrivateKey := "-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----" + assert.Equal(t, expectedPrivateKey, key.KeyVal.Private) + assert.Equal(t, RSAKeyScheme, key.Scheme) + assert.Equal(t, RSAKeyType, key.KeyType) + }) + + t.Run("invalid path", func(t *testing.T) { + _, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "invalid")) + assert.ErrorContains(t, err, "unable to load RSA key from file") + }) +} + func TestRSAPSSSignerVerifierSignAndVerify(t *testing.T) { t.Run("using valid key", func(t *testing.T) { key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key")) @@ -93,7 +125,7 @@ func TestRSAPSSSignerVerifierWithDSSEEnvelope(t *testing.T) { t.Error(err) } - assert.Equal(t, "966c5d84ba73ccded42eb473c939d77336e4def253ffaf6739f8e983ef73dad8", env.Signatures[0].KeyID) + assert.Equal(t, "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", env.Signatures[0].KeyID) envPayload, err := env.DecodeB64Payload() assert.Equal(t, payload, envPayload) assert.Nil(t, err) @@ -115,5 +147,47 @@ func TestRSAPSSSignerVerifierWithDSSEEnvelope(t *testing.T) { acceptedKeys, err := ev.Verify(context.Background(), env) assert.Nil(t, err) - assert.Equal(t, "966c5d84ba73ccded42eb473c939d77336e4def253ffaf6739f8e983ef73dad8", acceptedKeys[0].KeyID) + assert.Equal(t, "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", acceptedKeys[0].KeyID) +} + +func TestRSAPSSSignerVerifierWithMetablockFile(t *testing.T) { + key, err := LoadRSAPSSKeyFromFile(filepath.Join("test-data", "rsa-test-key.pub")) + if err != nil { + t.Fatal(err) + } + + sv, err := NewRSAPSSSignerVerifierFromSSLibKey(key) + if err != nil { + t.Fatal(err) + } + + metadataBytes, err := os.ReadFile(filepath.Join("test-data", "test-rsa.4e8d20af.link")) + if err != nil { + t.Fatal(err) + } + + mb := struct { + Signatures []struct { + KeyID string `json:"keyid"` + Sig string `json:"sig"` + } `json:"signatures"` + Signed any `json:"signed"` + }{} + + if err := json.Unmarshal(metadataBytes, &mb); err != nil { + t.Fatal(err) + } + + assert.Equal(t, "8958e5be66ee4352880a531bd097d1727adcc78e66b4faeb4a2cd6ad073dcb84f9a34e8156af39a7144cb5cd925325a18ccd4f0b2f981d6ff82655a7d63210d36655c50a0bf24e4839c10430a040dd6189d04fabec90eae4314c75ae2d585da17a56aaf6755e613a3a6a471ad2eddbb24504848e34f9ac163660f8ab80d7701bfa1189578a59597b3809ee62a70a7cc9545cfa65e23018fa442a45279b9fcf9d80bc92df711bfcfe16e3eae1bcf61b3286c1f0bdda17bc28bfab5b736bdcac4a38e31db1d0e0f56a2853b1b451650305f040a3425c3be47125700e92ef82c5a91a040b5e70ab7f6ebbe037ae1a6835044b5699748037e2e39a55a420c41cd9fa6e16868776367e3620e7d28eb9d8a3d710bdc98d488df1a9947d2ec8400f3c6209e8ca587cbffa30ceb3be98105e03182aab1bbb3c4e2560d99f0b09c012df2271f273ac70a6abb185abe11d559b118dca616417fa9205e74ab58e89ffd8b965da304ae9dc9cf6ffac4838b7c5375d6c2057a61cb286f06ad3b02a49c3af6178", mb.Signatures[0].Sig) + assert.Equal(t, sv.keyID, mb.Signatures[0].KeyID) + + encodedBytes, err := cjson.EncodeCanonical(mb.Signed) + if err != nil { + t.Fatal(err) + } + + decodedSig := hexDecode(t, mb.Signatures[0].Sig) + + err = sv.Verify(context.Background(), encodedBytes, decodedSig) + assert.Nil(t, err) } diff --git a/signerverifier/test-data/test-ecdsa.98adf386.link b/signerverifier/test-data/test-ecdsa.98adf386.link new file mode 100644 index 0000000..87661a4 --- /dev/null +++ b/signerverifier/test-data/test-ecdsa.98adf386.link @@ -0,0 +1,17 @@ +{ + "signatures": [ + { + "keyid": "98adf38602c48c5479e9a991ee3f8cbf541ee4f985e00f7a5fc4148d9a45b704", + "sig": "304502201fbb03c0937504182a48c66f9218bdcb2e99a07ada273e92e5e543867f98c8d7022100dbfa7bbf74fd76d76c1d08676419cba85bbd81dfb000f3ac6a786693ddc508f5" + } + ], + "signed": { + "_type": "link", + "byproducts": {}, + "command": [], + "environment": {}, + "materials": {}, + "name": "test-ecdsa", + "products": {} + } +} \ No newline at end of file diff --git a/signerverifier/test-data/test-ed25519.52e3b8e7.link b/signerverifier/test-data/test-ed25519.52e3b8e7.link new file mode 100644 index 0000000..f4f74e3 --- /dev/null +++ b/signerverifier/test-data/test-ed25519.52e3b8e7.link @@ -0,0 +1,17 @@ +{ + "signatures": [ + { + "keyid": "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", + "sig": "4c8b7605a9195d4ddba54493bbb5257a9836c1d16056a027fd77e97b95a4f3e36f8bc3c9c9960387d68187760b3072a30c44f992c5bf8f7497c303a3b0a32403" + } + ], + "signed": { + "_type": "link", + "byproducts": {}, + "command": [], + "environment": {}, + "materials": {}, + "name": "test-ed25519", + "products": {} + } +} \ No newline at end of file diff --git a/signerverifier/test-data/test-rsa.4e8d20af.link b/signerverifier/test-data/test-rsa.4e8d20af.link new file mode 100644 index 0000000..4ed6b64 --- /dev/null +++ b/signerverifier/test-data/test-rsa.4e8d20af.link @@ -0,0 +1,17 @@ +{ + "signatures": [ + { + "keyid": "4e8d20af09fcaed6c388a186427f94a5f7ff5591ec295f4aab2cff49ffe39e9b", + "sig": "8958e5be66ee4352880a531bd097d1727adcc78e66b4faeb4a2cd6ad073dcb84f9a34e8156af39a7144cb5cd925325a18ccd4f0b2f981d6ff82655a7d63210d36655c50a0bf24e4839c10430a040dd6189d04fabec90eae4314c75ae2d585da17a56aaf6755e613a3a6a471ad2eddbb24504848e34f9ac163660f8ab80d7701bfa1189578a59597b3809ee62a70a7cc9545cfa65e23018fa442a45279b9fcf9d80bc92df711bfcfe16e3eae1bcf61b3286c1f0bdda17bc28bfab5b736bdcac4a38e31db1d0e0f56a2853b1b451650305f040a3425c3be47125700e92ef82c5a91a040b5e70ab7f6ebbe037ae1a6835044b5699748037e2e39a55a420c41cd9fa6e16868776367e3620e7d28eb9d8a3d710bdc98d488df1a9947d2ec8400f3c6209e8ca587cbffa30ceb3be98105e03182aab1bbb3c4e2560d99f0b09c012df2271f273ac70a6abb185abe11d559b118dca616417fa9205e74ab58e89ffd8b965da304ae9dc9cf6ffac4838b7c5375d6c2057a61cb286f06ad3b02a49c3af6178" + } + ], + "signed": { + "_type": "link", + "byproducts": {}, + "command": [], + "environment": {}, + "materials": {}, + "name": "test-rsa", + "products": {} + } +} \ No newline at end of file diff --git a/signerverifier/utils.go b/signerverifier/utils.go index 18e8e3c..73aaa77 100644 --- a/signerverifier/utils.go +++ b/signerverifier/utils.go @@ -8,6 +8,7 @@ import ( "encoding/pem" "errors" "hash" + "testing" "github.com/secure-systems-lab/go-securesystemslib/cjson" ) @@ -138,3 +139,12 @@ func hashBeforeSigning(data []byte, h hash.Hash) []byte { h.Write(data) return h.Sum(nil) } + +func hexDecode(t *testing.T, data string) []byte { + t.Helper() + b, err := hex.DecodeString(data) + if err != nil { + t.Fatal(err) + } + return b +}