Skip to content

Commit 61872a3

Browse files
asraatoby-jn
andauthored
feat: Support ecdsa and RSA keys (#270 with backwards compatibility) (#357)
* * fix!: ECDSA verifiers now expect PEM-encoded public keys per TUF specification * feat: ECDSA signers are now implemented * feat: RSA verifiers and signers are implemented BREAKING CHANGE: ECDSA verifiers expect PEM-encoded public keys. If you rely on previous behavior of hex-encoded public keys for verifiers, then you must import pkg/deprecated/set_ecdsa that will allow a fallback for hex-encoded ECDSA keys. Co-authored-by: Asra Ali <[email protected]> Co-authored-by: Toby Bristow <[email protected]> Signed-off-by: Asra Ali <[email protected]> * add comment Signed-off-by: Asra Ali <[email protected]> Signed-off-by: Asra Ali <[email protected]> Co-authored-by: Toby Bristow <[email protected]>
1 parent 06ed599 commit 61872a3

File tree

20 files changed

+932
-148
lines changed

20 files changed

+932
-148
lines changed

cmd/tuf/gen_key.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import (
66

77
"github.com/flynn/go-docopt"
88
"github.com/theupdateframework/go-tuf"
9+
"github.com/theupdateframework/go-tuf/data"
910
)
1011

1112
func init() {
1213
register("gen-key", cmdGenKey, `
13-
usage: tuf gen-key [--expires=<days>] <role>
14+
usage: tuf gen-key [--expires=<days>] [--scheme=<scheme>] <role>
1415
1516
Generate a new signing key for the given role.
1617
@@ -23,28 +24,40 @@ form of TUF_{{ROLE}}_PASSPHRASE
2324
2425
Options:
2526
--expires=<days> Set the root metadata file to expire <days> days from now.
27+
--scheme=<scheme> Set the key scheme to use [default: ed25519].
2628
`)
2729
}
2830

2931
func cmdGenKey(args *docopt.Args, repo *tuf.Repo) error {
3032
role := args.String["<role>"]
3133
var keyids []string
34+
35+
keyScheme := data.KeySchemeEd25519
36+
switch t := args.String["--scheme"]; t {
37+
case string(data.KeySchemeEd25519),
38+
string(data.KeySchemeECDSA_SHA2_P256),
39+
string(data.KeySchemeRSASSA_PSS_SHA256):
40+
keyScheme = data.KeyScheme(t)
41+
default:
42+
fmt.Println("Using default key scheme", keyScheme)
43+
}
44+
3245
var err error
46+
var expires time.Time
3347
if arg := args.String["--expires"]; arg != "" {
34-
var expires time.Time
3548
expires, err = parseExpires(arg)
3649
if err != nil {
3750
return err
3851
}
39-
keyids, err = repo.GenKeyWithExpires(role, expires)
4052
} else {
41-
keyids, err = repo.GenKey(role)
53+
expires = data.DefaultExpires(role)
4254
}
55+
keyids, err = repo.GenKeyWithSchemeAndExpires(role, expires, keyScheme)
4356
if err != nil {
4457
return err
4558
}
4659
for _, id := range keyids {
47-
fmt.Println("Generated", role, "key with ID", id)
60+
fmt.Println("Generated", role, keyScheme, "key with ID", id)
4861
}
4962
return nil
5063
}

data/types.go

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,29 @@ import (
1515
"github.com/secure-systems-lab/go-securesystemslib/cjson"
1616
)
1717

18+
type KeyType string
19+
20+
type KeyScheme string
21+
22+
type HashAlgorithm string
23+
1824
const (
19-
KeyIDLength = sha256.Size * 2
20-
KeyTypeEd25519 = "ed25519"
21-
KeyTypeECDSA_SHA2_P256 = "ecdsa-sha2-nistp256"
22-
KeySchemeEd25519 = "ed25519"
23-
KeySchemeECDSA_SHA2_P256 = "ecdsa-sha2-nistp256"
24-
KeyTypeRSASSA_PSS_SHA256 = "rsa"
25-
KeySchemeRSASSA_PSS_SHA256 = "rsassa-pss-sha256"
25+
KeyIDLength = sha256.Size * 2
26+
27+
KeyTypeEd25519 KeyType = "ed25519"
28+
KeyTypeECDSA_SHA2_P256 KeyType = "ecdsa-sha2-nistp256"
29+
KeyTypeRSASSA_PSS_SHA256 KeyType = "rsa"
30+
31+
KeySchemeEd25519 KeyScheme = "ed25519"
32+
KeySchemeECDSA_SHA2_P256 KeyScheme = "ecdsa-sha2-nistp256"
33+
KeySchemeRSASSA_PSS_SHA256 KeyScheme = "rsassa-pss-sha256"
34+
35+
HashAlgorithmSHA256 HashAlgorithm = "sha256"
36+
HashAlgorithmSHA512 HashAlgorithm = "sha512"
2637
)
2738

2839
var (
29-
HashAlgorithms = []string{"sha256", "sha512"}
40+
HashAlgorithms = []HashAlgorithm{HashAlgorithmSHA256, HashAlgorithmSHA512}
3041
ErrPathsAndPathHashesSet = errors.New("tuf: failed validation of delegated target: paths and path_hash_prefixes are both set")
3142
)
3243

@@ -41,19 +52,19 @@ type Signature struct {
4152
}
4253

4354
type PublicKey struct {
44-
Type string `json:"keytype"`
45-
Scheme string `json:"scheme"`
46-
Algorithms []string `json:"keyid_hash_algorithms,omitempty"`
55+
Type KeyType `json:"keytype"`
56+
Scheme KeyScheme `json:"scheme"`
57+
Algorithms []HashAlgorithm `json:"keyid_hash_algorithms,omitempty"`
4758
Value json.RawMessage `json:"keyval"`
4859

4960
ids []string
5061
idOnce sync.Once
5162
}
5263

5364
type PrivateKey struct {
54-
Type string `json:"keytype"`
55-
Scheme string `json:"scheme,omitempty"`
56-
Algorithms []string `json:"keyid_hash_algorithms,omitempty"`
65+
Type KeyType `json:"keytype"`
66+
Scheme KeyScheme `json:"scheme,omitempty"`
67+
Algorithms []HashAlgorithm `json:"keyid_hash_algorithms,omitempty"`
5768
Value json.RawMessage `json:"keyval"`
5869
}
5970

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/google/gofuzz v1.2.0
99
github.com/secure-systems-lab/go-securesystemslib v0.4.0
1010
github.com/stretchr/testify v1.8.0
11+
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d
1112
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
1213
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
1314
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
@@ -20,7 +21,6 @@ require (
2021
github.com/kr/text v0.1.0 // indirect
2122
github.com/onsi/ginkgo v1.16.5 // indirect
2223
github.com/pmezard/go-difflib v1.0.0 // indirect
23-
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
2424
golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect
2525
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
2626
gopkg.in/yaml.v3 v3.0.1 // indirect

internal/signer/sort_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func (s *mockSigner) PublicData() *data.PublicKey {
2626
return &data.PublicKey{
2727
Type: "mock",
2828
Scheme: "mock",
29-
Algorithms: []string{"mock"},
29+
Algorithms: []data.HashAlgorithm{"mock"},
3030
Value: s.value,
3131
}
3232
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package deprecated
2+
3+
import (
4+
"crypto"
5+
"crypto/elliptic"
6+
"crypto/rand"
7+
"crypto/sha256"
8+
"encoding/json"
9+
"testing"
10+
11+
"github.com/secure-systems-lab/go-securesystemslib/cjson"
12+
repo "github.com/theupdateframework/go-tuf"
13+
"github.com/theupdateframework/go-tuf/data"
14+
_ "github.com/theupdateframework/go-tuf/pkg/deprecated/set_ecdsa"
15+
"github.com/theupdateframework/go-tuf/pkg/keys"
16+
. "gopkg.in/check.v1"
17+
)
18+
19+
func Test(t *testing.T) { TestingT(t) }
20+
21+
type RepoSuite struct{}
22+
23+
var _ = Suite(&RepoSuite{})
24+
25+
func genKey(c *C, r *repo.Repo, role string) []string {
26+
keyids, err := r.GenKey(role)
27+
c.Assert(err, IsNil)
28+
c.Assert(len(keyids) > 0, Equals, true)
29+
return keyids
30+
}
31+
32+
// Deprecated ecdsa key support: Support verification against roots that were
33+
// signed with hex-encoded ecdsa keys.
34+
func (rs *RepoSuite) TestDeprecatedHexEncodedKeysSucceed(c *C) {
35+
files := map[string][]byte{"foo.txt": []byte("foo")}
36+
local := repo.MemoryStore(make(map[string]json.RawMessage), files)
37+
r, err := repo.NewRepo(local)
38+
c.Assert(err, IsNil)
39+
40+
r.Init(false)
41+
// Add a root key with hex-encoded ecdsa format
42+
signer, err := keys.GenerateEcdsaKey()
43+
c.Assert(err, IsNil)
44+
type deprecatedP256Verifier struct {
45+
PublicKey data.HexBytes `json:"public"`
46+
}
47+
pub := signer.PublicKey
48+
keyValBytes, err := json.Marshal(&deprecatedP256Verifier{PublicKey: elliptic.Marshal(pub.Curve, pub.X, pub.Y)})
49+
c.Assert(err, IsNil)
50+
publicData := &data.PublicKey{
51+
Type: data.KeyTypeECDSA_SHA2_P256,
52+
Scheme: data.KeySchemeECDSA_SHA2_P256,
53+
Algorithms: data.HashAlgorithms,
54+
Value: keyValBytes,
55+
}
56+
err = r.AddVerificationKey("root", publicData)
57+
c.Assert(err, IsNil)
58+
// Add other keys as normal
59+
genKey(c, r, "targets")
60+
genKey(c, r, "snapshot")
61+
genKey(c, r, "timestamp")
62+
c.Assert(r.AddTarget("foo.txt", nil), IsNil)
63+
64+
// Sign the root role manually
65+
rootMeta, err := r.SignedMeta("root.json")
66+
c.Assert(err, IsNil)
67+
rootCanonical, err := cjson.EncodeCanonical(rootMeta.Signed)
68+
c.Assert(err, IsNil)
69+
hash := sha256.Sum256(rootCanonical)
70+
rootSig, err := signer.PrivateKey.Sign(rand.Reader, hash[:], crypto.SHA256)
71+
c.Assert(err, IsNil)
72+
for _, id := range publicData.IDs() {
73+
c.Assert(r.AddOrUpdateSignature("root.json", data.Signature{
74+
KeyID: id,
75+
Signature: rootSig}), IsNil)
76+
}
77+
78+
// Committing should succeed because the deprecated key pkg is added.
79+
c.Assert(r.Snapshot(), IsNil)
80+
c.Assert(r.Timestamp(), IsNil)
81+
c.Assert(r.Commit(), IsNil)
82+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package set_ecdsa
2+
3+
import (
4+
"errors"
5+
6+
"github.com/theupdateframework/go-tuf/data"
7+
"github.com/theupdateframework/go-tuf/pkg/keys"
8+
)
9+
10+
/*
11+
Importing this package will allow support for both hex-encoded ECDSA
12+
verifiers and PEM-encoded ECDSA verifiers.
13+
Note that this package imports "github.com/theupdateframework/go-tuf/pkg/keys"
14+
and overrides the ECDSA verifier loaded at init time in that package.
15+
*/
16+
17+
func init() {
18+
_, ok := keys.VerifierMap.Load(data.KeyTypeECDSA_SHA2_P256)
19+
if !ok {
20+
panic(errors.New("expected to override previously loaded PEM-only ECDSA verifier"))
21+
}
22+
keys.VerifierMap.Store(data.KeyTypeECDSA_SHA2_P256, keys.NewDeprecatedEcdsaVerifier)
23+
}

pkg/keys/deprecated_ecdsa.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package keys
2+
3+
import (
4+
"bytes"
5+
"crypto/ecdsa"
6+
"crypto/elliptic"
7+
"crypto/sha256"
8+
"encoding/json"
9+
"errors"
10+
"fmt"
11+
"io"
12+
"os"
13+
14+
"github.com/theupdateframework/go-tuf/data"
15+
)
16+
17+
func NewDeprecatedEcdsaVerifier() Verifier {
18+
return &ecdsaVerifierWithDeprecatedSupport{}
19+
}
20+
21+
type ecdsaVerifierWithDeprecatedSupport struct {
22+
key *data.PublicKey
23+
// This will switch based on whether this is a PEM-encoded key
24+
// or a deprecated hex-encoded key.
25+
Verifier
26+
}
27+
28+
func (p *ecdsaVerifierWithDeprecatedSupport) UnmarshalPublicKey(key *data.PublicKey) error {
29+
p.key = key
30+
pemVerifier := &EcdsaVerifier{}
31+
if err := pemVerifier.UnmarshalPublicKey(key); err != nil {
32+
// Try the deprecated hex-encoded verifier
33+
hexVerifier := &deprecatedP256Verifier{}
34+
if err := hexVerifier.UnmarshalPublicKey(key); err != nil {
35+
return err
36+
}
37+
p.Verifier = hexVerifier
38+
return nil
39+
}
40+
p.Verifier = pemVerifier
41+
return nil
42+
}
43+
44+
/*
45+
Deprecated ecdsaVerifier that used hex-encoded public keys.
46+
This MAY be used to verify existing metadata that used this
47+
old format. This will be deprecated soon, ensure that repositories
48+
are re-signed and clients receieve a fully compliant root.
49+
*/
50+
51+
type deprecatedP256Verifier struct {
52+
PublicKey data.HexBytes `json:"public"`
53+
key *data.PublicKey
54+
}
55+
56+
func (p *deprecatedP256Verifier) Public() string {
57+
return p.PublicKey.String()
58+
}
59+
60+
func (p *deprecatedP256Verifier) Verify(msg, sigBytes []byte) error {
61+
x, y := elliptic.Unmarshal(elliptic.P256(), p.PublicKey)
62+
k := &ecdsa.PublicKey{
63+
Curve: elliptic.P256(),
64+
X: x,
65+
Y: y,
66+
}
67+
68+
hash := sha256.Sum256(msg)
69+
70+
if !ecdsa.VerifyASN1(k, hash[:], sigBytes) {
71+
return errors.New("tuf: deprecated ecdsa signature verification failed")
72+
}
73+
return nil
74+
}
75+
76+
func (p *deprecatedP256Verifier) MarshalPublicKey() *data.PublicKey {
77+
return p.key
78+
}
79+
80+
func (p *deprecatedP256Verifier) UnmarshalPublicKey(key *data.PublicKey) error {
81+
// Prepare decoder limited to 512Kb
82+
dec := json.NewDecoder(io.LimitReader(bytes.NewReader(key.Value), MaxJSONKeySize))
83+
84+
// Unmarshal key value
85+
if err := dec.Decode(p); err != nil {
86+
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
87+
return fmt.Errorf("tuf: the public key is truncated or too large: %w", err)
88+
}
89+
return err
90+
}
91+
92+
curve := elliptic.P256()
93+
94+
// Parse as uncompressed marshalled point.
95+
x, _ := elliptic.Unmarshal(curve, p.PublicKey)
96+
if x == nil {
97+
return errors.New("tuf: invalid ecdsa public key point")
98+
}
99+
100+
p.key = key
101+
fmt.Fprintln(os.Stderr, "tuf: warning using deprecated ecdsa hex-encoded keys")
102+
return nil
103+
}

0 commit comments

Comments
 (0)