Skip to content

Commit 307123d

Browse files
committed
Add ED25519 SignerVerifier
Signed-off-by: Aditya Sirish <[email protected]>
1 parent 6476f36 commit 307123d

File tree

5 files changed

+247
-0
lines changed

5 files changed

+247
-0
lines changed

signerverifier/ed25519.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package signerverifier
2+
3+
import (
4+
"context"
5+
"crypto"
6+
"crypto/ed25519"
7+
"encoding/hex"
8+
)
9+
10+
const Ed25519KeyType = "ed25519"
11+
12+
type Ed25519SignerVerifier struct {
13+
keyID string
14+
private ed25519.PrivateKey
15+
public ed25519.PublicKey
16+
}
17+
18+
// NewEd25519SignerVerifierFromSSLibKey creates an Ed25519SignerVerifier from an
19+
// SSLibKey.
20+
func NewEd25519SignerVerifierFromSSLibKey(key *SSLibKey) (*Ed25519SignerVerifier, error) {
21+
public, err := hex.DecodeString(key.KeyVal.Public)
22+
if err != nil {
23+
return nil, err
24+
}
25+
26+
var private []byte
27+
if len(key.KeyVal.Private) > 0 {
28+
private, err = hex.DecodeString(key.KeyVal.Private)
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
// python-securesystemslib provides an interface to generate ed25519
34+
// keys but it differs slightly in how it serializes the key to disk.
35+
// Specifically, the keyval.private field includes _only_ the private
36+
// portion of the key while libraries such as crypto/ed25519 also expect
37+
// the public portion. So, if the private portion is half of what we
38+
// expect, we append the public portion as well.
39+
if len(private) == ed25519.PrivateKeySize/2 {
40+
private = append(private, public...)
41+
}
42+
}
43+
44+
return &Ed25519SignerVerifier{
45+
keyID: key.KeyID(),
46+
public: ed25519.PublicKey(public),
47+
private: ed25519.PrivateKey(private),
48+
}, nil
49+
}
50+
51+
// Sign creates a signature for `data`.
52+
func (sv *Ed25519SignerVerifier) Sign(ctx context.Context, data []byte) ([]byte, error) {
53+
if len(sv.private) == 0 {
54+
return nil, ErrNotPrivateKey
55+
}
56+
57+
signature := ed25519.Sign(sv.private, data)
58+
return signature, nil
59+
}
60+
61+
// Verify verifies the `sig` value passed in against `data`.
62+
func (sv Ed25519SignerVerifier) Verify(ctx context.Context, data []byte, sig []byte) error {
63+
if ok := ed25519.Verify(sv.public, data, sig); ok {
64+
return nil
65+
}
66+
return ErrSignatureVerificationFailed
67+
}
68+
69+
// KeyID returns the identifier of the key used to create the
70+
// Ed25519SignerVerifier instance.
71+
func (sv Ed25519SignerVerifier) KeyID() (string, error) {
72+
return sv.keyID, nil
73+
}
74+
75+
// Public returns the public portion of the key used to create the
76+
// Ed25519SignerVerifier instance.
77+
func (sv Ed25519SignerVerifier) Public() crypto.PublicKey {
78+
return sv.public
79+
}

signerverifier/ed25519_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package signerverifier
2+
3+
import (
4+
"context"
5+
"crypto/ed25519"
6+
"encoding/hex"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestNewEd25519SignerVerifierFromSSLibKey(t *testing.T) {
14+
key, err := LoadKeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub"))
15+
if err != nil {
16+
t.Error(err)
17+
}
18+
19+
sv, err := NewEd25519SignerVerifierFromSSLibKey(key)
20+
if err != nil {
21+
t.Error(err)
22+
}
23+
24+
expectedPublicString := "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f"
25+
expectedPublicKey := ed25519.PublicKey(hexDecode(t, expectedPublicString))
26+
27+
assert.Equal(t, "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", sv.keyID)
28+
assert.Equal(t, expectedPublicKey, sv.public)
29+
assert.Nil(t, sv.private)
30+
}
31+
32+
func TestEd25519SignerVerifierSign(t *testing.T) {
33+
key, err := LoadKeyFromFile(filepath.Join("test-data", "ed25519-test-key"))
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
38+
sv, err := NewEd25519SignerVerifierFromSSLibKey(key)
39+
if err != nil {
40+
t.Error(err)
41+
}
42+
43+
message := []byte("test message")
44+
45+
signature, err := sv.Sign(context.Background(), message)
46+
if err != nil {
47+
t.Error(err)
48+
}
49+
50+
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}
51+
assert.Equal(t, expectedSignature, signature)
52+
53+
key, err = LoadKeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub"))
54+
if err != nil {
55+
t.Fatal(err)
56+
}
57+
58+
sv, err = NewEd25519SignerVerifierFromSSLibKey(key)
59+
if err != nil {
60+
t.Error(err)
61+
}
62+
63+
_, err = sv.Sign(context.Background(), message)
64+
assert.ErrorIs(t, err, ErrNotPrivateKey)
65+
}
66+
67+
func TestEd25519SignerVerifierVerify(t *testing.T) {
68+
key, err := LoadKeyFromFile(filepath.Join("test-data", "ed25519-test-key.pub"))
69+
if err != nil {
70+
t.Fatal(err)
71+
}
72+
73+
sv, err := NewEd25519SignerVerifierFromSSLibKey(key)
74+
if err != nil {
75+
t.Error(err)
76+
}
77+
78+
message := []byte("test message")
79+
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}
80+
assert.Nil(t, sv.Verify(context.Background(), message, signature))
81+
82+
message = []byte("corrupted message")
83+
err = sv.Verify(context.Background(), message, signature)
84+
assert.ErrorIs(t, err, ErrSignatureVerificationFailed)
85+
}
86+
87+
func hexDecode(t *testing.T, data string) []byte {
88+
t.Helper()
89+
b, err := hex.DecodeString(data)
90+
if err != nil {
91+
t.Fatal(err)
92+
}
93+
return b
94+
}

signerverifier/signerverifier.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package signerverifier
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
"encoding/json"
7+
"errors"
8+
"os"
9+
10+
"github.com/secure-systems-lab/go-securesystemslib/cjson"
11+
)
12+
13+
var (
14+
ErrNotPrivateKey = errors.New("loaded key is not a private key")
15+
ErrSignatureVerificationFailed = errors.New("failed to verify signature")
16+
ErrUnknownKeyType = errors.New("unknown key type")
17+
ErrInvalidThreshold = errors.New("threshold is either less than 1 or greater than number of provided public keys")
18+
)
19+
20+
type SSLibKey struct {
21+
KeyIDHashAlgorithms []string `json:"keyid_hash_algorithms"`
22+
KeyType string `json:"keytype"`
23+
KeyVal KeyVal `json:"keyval"`
24+
Scheme string `json:"scheme"`
25+
keyID string
26+
}
27+
28+
func (k *SSLibKey) KeyID() string {
29+
return k.keyID
30+
}
31+
32+
type KeyVal struct {
33+
Private string `json:"private,omitempty"`
34+
Public string `json:"public"`
35+
Certificate string `json:"certificate,omitempty"`
36+
}
37+
38+
// LoadKeyFromBytes returns a pointer to a Key instance created from the
39+
// contents of the bytes. The key contents are expected to be in the custom
40+
// securesystemslib format.
41+
func LoadKeyFromBytes(contents []byte) (*SSLibKey, error) {
42+
var key *SSLibKey
43+
if err := json.Unmarshal(contents, &key); err != nil {
44+
return nil, err
45+
}
46+
47+
keyID, err := calculateKeyID(key)
48+
if err != nil {
49+
return nil, err
50+
}
51+
key.keyID = keyID
52+
53+
return key, nil
54+
}
55+
56+
func LoadKeyFromFile(path string) (*SSLibKey, error) {
57+
contents, err := os.ReadFile(path)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
return LoadKeyFromBytes(contents)
63+
}
64+
65+
func calculateKeyID(k *SSLibKey) (string, error) {
66+
canonical, err := cjson.EncodeCanonical(k)
67+
if err != nil {
68+
return "", err
69+
}
70+
digest := sha256.Sum256(canonical)
71+
return hex.EncodeToString(digest[:]), nil
72+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"keytype": "ed25519", "scheme": "ed25519", "keyid": "52e3b8e73279d6ebdd62a5016e2725ff284f569665eb92ccb145d83817a02997", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f", "private": "66f6ebad4aeb949b91c84c9cfd6ee351fc4fd544744bab6e30fb400ba13c6e9a"}}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"keytype": "ed25519", "scheme": "ed25519", "keyid_hash_algorithms": ["sha256", "sha512"], "keyval": {"public": "3f586ce67329419fb0081bd995914e866a7205da463d593b3b490eab2b27fd3f"}}

0 commit comments

Comments
 (0)