Skip to content

Commit

Permalink
feat: Support ecdsa and RSA keys (#270 with backwards compatibility) (#…
Browse files Browse the repository at this point in the history
…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 <asraa@google.com>
Co-authored-by: Toby Bristow <toby.bristow@qush.com>
Signed-off-by: Asra Ali <asraa@google.com>

* add comment

Signed-off-by: Asra Ali <asraa@google.com>

Signed-off-by: Asra Ali <asraa@google.com>
Co-authored-by: Toby Bristow <toby.bristow@qush.com>
  • Loading branch information
asraa and toby-jn authored Sep 7, 2022
1 parent 06ed599 commit 61872a3
Show file tree
Hide file tree
Showing 20 changed files with 932 additions and 148 deletions.
23 changes: 18 additions & 5 deletions cmd/tuf/gen_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import (

"github.com/flynn/go-docopt"
"github.com/theupdateframework/go-tuf"
"github.com/theupdateframework/go-tuf/data"
)

func init() {
register("gen-key", cmdGenKey, `
usage: tuf gen-key [--expires=<days>] <role>
usage: tuf gen-key [--expires=<days>] [--scheme=<scheme>] <role>
Generate a new signing key for the given role.
Expand All @@ -23,28 +24,40 @@ form of TUF_{{ROLE}}_PASSPHRASE
Options:
--expires=<days> Set the root metadata file to expire <days> days from now.
--scheme=<scheme> Set the key scheme to use [default: ed25519].
`)
}

func cmdGenKey(args *docopt.Args, repo *tuf.Repo) error {
role := args.String["<role>"]
var keyids []string

keyScheme := data.KeySchemeEd25519
switch t := args.String["--scheme"]; t {
case string(data.KeySchemeEd25519),
string(data.KeySchemeECDSA_SHA2_P256),
string(data.KeySchemeRSASSA_PSS_SHA256):
keyScheme = data.KeyScheme(t)
default:
fmt.Println("Using default key scheme", keyScheme)
}

var err error
var expires time.Time
if arg := args.String["--expires"]; arg != "" {
var expires time.Time
expires, err = parseExpires(arg)
if err != nil {
return err
}
keyids, err = repo.GenKeyWithExpires(role, expires)
} else {
keyids, err = repo.GenKey(role)
expires = data.DefaultExpires(role)
}
keyids, err = repo.GenKeyWithSchemeAndExpires(role, expires, keyScheme)
if err != nil {
return err
}
for _, id := range keyids {
fmt.Println("Generated", role, "key with ID", id)
fmt.Println("Generated", role, keyScheme, "key with ID", id)
}
return nil
}
39 changes: 25 additions & 14 deletions data/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,29 @@ import (
"github.com/secure-systems-lab/go-securesystemslib/cjson"
)

type KeyType string

type KeyScheme string

type HashAlgorithm string

const (
KeyIDLength = sha256.Size * 2
KeyTypeEd25519 = "ed25519"
KeyTypeECDSA_SHA2_P256 = "ecdsa-sha2-nistp256"
KeySchemeEd25519 = "ed25519"
KeySchemeECDSA_SHA2_P256 = "ecdsa-sha2-nistp256"
KeyTypeRSASSA_PSS_SHA256 = "rsa"
KeySchemeRSASSA_PSS_SHA256 = "rsassa-pss-sha256"
KeyIDLength = sha256.Size * 2

KeyTypeEd25519 KeyType = "ed25519"
KeyTypeECDSA_SHA2_P256 KeyType = "ecdsa-sha2-nistp256"
KeyTypeRSASSA_PSS_SHA256 KeyType = "rsa"

KeySchemeEd25519 KeyScheme = "ed25519"
KeySchemeECDSA_SHA2_P256 KeyScheme = "ecdsa-sha2-nistp256"
KeySchemeRSASSA_PSS_SHA256 KeyScheme = "rsassa-pss-sha256"

HashAlgorithmSHA256 HashAlgorithm = "sha256"
HashAlgorithmSHA512 HashAlgorithm = "sha512"
)

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

Expand All @@ -41,19 +52,19 @@ type Signature struct {
}

type PublicKey struct {
Type string `json:"keytype"`
Scheme string `json:"scheme"`
Algorithms []string `json:"keyid_hash_algorithms,omitempty"`
Type KeyType `json:"keytype"`
Scheme KeyScheme `json:"scheme"`
Algorithms []HashAlgorithm `json:"keyid_hash_algorithms,omitempty"`
Value json.RawMessage `json:"keyval"`

ids []string
idOnce sync.Once
}

type PrivateKey struct {
Type string `json:"keytype"`
Scheme string `json:"scheme,omitempty"`
Algorithms []string `json:"keyid_hash_algorithms,omitempty"`
Type KeyType `json:"keytype"`
Scheme KeyScheme `json:"scheme,omitempty"`
Algorithms []HashAlgorithm `json:"keyid_hash_algorithms,omitempty"`
Value json.RawMessage `json:"keyval"`
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/google/gofuzz v1.2.0
github.com/secure-systems-lab/go-securesystemslib v0.4.0
github.com/stretchr/testify v1.8.0
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
Expand All @@ -20,7 +21,6 @@ require (
github.com/kr/text v0.1.0 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
2 changes: 1 addition & 1 deletion internal/signer/sort_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (s *mockSigner) PublicData() *data.PublicKey {
return &data.PublicKey{
Type: "mock",
Scheme: "mock",
Algorithms: []string{"mock"},
Algorithms: []data.HashAlgorithm{"mock"},
Value: s.value,
}
}
Expand Down
82 changes: 82 additions & 0 deletions pkg/deprecated/deprecated_repo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package deprecated

import (
"crypto"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"testing"

"github.com/secure-systems-lab/go-securesystemslib/cjson"
repo "github.com/theupdateframework/go-tuf"
"github.com/theupdateframework/go-tuf/data"
_ "github.com/theupdateframework/go-tuf/pkg/deprecated/set_ecdsa"
"github.com/theupdateframework/go-tuf/pkg/keys"
. "gopkg.in/check.v1"
)

func Test(t *testing.T) { TestingT(t) }

type RepoSuite struct{}

var _ = Suite(&RepoSuite{})

func genKey(c *C, r *repo.Repo, role string) []string {
keyids, err := r.GenKey(role)
c.Assert(err, IsNil)
c.Assert(len(keyids) > 0, Equals, true)
return keyids
}

// Deprecated ecdsa key support: Support verification against roots that were
// signed with hex-encoded ecdsa keys.
func (rs *RepoSuite) TestDeprecatedHexEncodedKeysSucceed(c *C) {
files := map[string][]byte{"foo.txt": []byte("foo")}
local := repo.MemoryStore(make(map[string]json.RawMessage), files)
r, err := repo.NewRepo(local)
c.Assert(err, IsNil)

r.Init(false)
// Add a root key with hex-encoded ecdsa format
signer, err := keys.GenerateEcdsaKey()
c.Assert(err, IsNil)
type deprecatedP256Verifier struct {
PublicKey data.HexBytes `json:"public"`
}
pub := signer.PublicKey
keyValBytes, err := json.Marshal(&deprecatedP256Verifier{PublicKey: elliptic.Marshal(pub.Curve, pub.X, pub.Y)})
c.Assert(err, IsNil)
publicData := &data.PublicKey{
Type: data.KeyTypeECDSA_SHA2_P256,
Scheme: data.KeySchemeECDSA_SHA2_P256,
Algorithms: data.HashAlgorithms,
Value: keyValBytes,
}
err = r.AddVerificationKey("root", publicData)
c.Assert(err, IsNil)
// Add other keys as normal
genKey(c, r, "targets")
genKey(c, r, "snapshot")
genKey(c, r, "timestamp")
c.Assert(r.AddTarget("foo.txt", nil), IsNil)

// Sign the root role manually
rootMeta, err := r.SignedMeta("root.json")
c.Assert(err, IsNil)
rootCanonical, err := cjson.EncodeCanonical(rootMeta.Signed)
c.Assert(err, IsNil)
hash := sha256.Sum256(rootCanonical)
rootSig, err := signer.PrivateKey.Sign(rand.Reader, hash[:], crypto.SHA256)
c.Assert(err, IsNil)
for _, id := range publicData.IDs() {
c.Assert(r.AddOrUpdateSignature("root.json", data.Signature{
KeyID: id,
Signature: rootSig}), IsNil)
}

// Committing should succeed because the deprecated key pkg is added.
c.Assert(r.Snapshot(), IsNil)
c.Assert(r.Timestamp(), IsNil)
c.Assert(r.Commit(), IsNil)
}
23 changes: 23 additions & 0 deletions pkg/deprecated/set_ecdsa/set_ecdsa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package set_ecdsa

import (
"errors"

"github.com/theupdateframework/go-tuf/data"
"github.com/theupdateframework/go-tuf/pkg/keys"
)

/*
Importing this package will allow support for both hex-encoded ECDSA
verifiers and PEM-encoded ECDSA verifiers.
Note that this package imports "github.com/theupdateframework/go-tuf/pkg/keys"
and overrides the ECDSA verifier loaded at init time in that package.
*/

func init() {
_, ok := keys.VerifierMap.Load(data.KeyTypeECDSA_SHA2_P256)
if !ok {
panic(errors.New("expected to override previously loaded PEM-only ECDSA verifier"))
}
keys.VerifierMap.Store(data.KeyTypeECDSA_SHA2_P256, keys.NewDeprecatedEcdsaVerifier)
}
103 changes: 103 additions & 0 deletions pkg/keys/deprecated_ecdsa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package keys

import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"os"

"github.com/theupdateframework/go-tuf/data"
)

func NewDeprecatedEcdsaVerifier() Verifier {
return &ecdsaVerifierWithDeprecatedSupport{}
}

type ecdsaVerifierWithDeprecatedSupport struct {
key *data.PublicKey
// This will switch based on whether this is a PEM-encoded key
// or a deprecated hex-encoded key.
Verifier
}

func (p *ecdsaVerifierWithDeprecatedSupport) UnmarshalPublicKey(key *data.PublicKey) error {
p.key = key
pemVerifier := &EcdsaVerifier{}
if err := pemVerifier.UnmarshalPublicKey(key); err != nil {
// Try the deprecated hex-encoded verifier
hexVerifier := &deprecatedP256Verifier{}
if err := hexVerifier.UnmarshalPublicKey(key); err != nil {
return err
}
p.Verifier = hexVerifier
return nil
}
p.Verifier = pemVerifier
return nil
}

/*
Deprecated ecdsaVerifier that used hex-encoded public keys.
This MAY be used to verify existing metadata that used this
old format. This will be deprecated soon, ensure that repositories
are re-signed and clients receieve a fully compliant root.
*/

type deprecatedP256Verifier struct {
PublicKey data.HexBytes `json:"public"`
key *data.PublicKey
}

func (p *deprecatedP256Verifier) Public() string {
return p.PublicKey.String()
}

func (p *deprecatedP256Verifier) Verify(msg, sigBytes []byte) error {
x, y := elliptic.Unmarshal(elliptic.P256(), p.PublicKey)
k := &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: x,
Y: y,
}

hash := sha256.Sum256(msg)

if !ecdsa.VerifyASN1(k, hash[:], sigBytes) {
return errors.New("tuf: deprecated ecdsa signature verification failed")
}
return nil
}

func (p *deprecatedP256Verifier) MarshalPublicKey() *data.PublicKey {
return p.key
}

func (p *deprecatedP256Verifier) UnmarshalPublicKey(key *data.PublicKey) error {
// Prepare decoder limited to 512Kb
dec := json.NewDecoder(io.LimitReader(bytes.NewReader(key.Value), MaxJSONKeySize))

// Unmarshal key value
if err := dec.Decode(p); err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
return fmt.Errorf("tuf: the public key is truncated or too large: %w", err)
}
return err
}

curve := elliptic.P256()

// Parse as uncompressed marshalled point.
x, _ := elliptic.Unmarshal(curve, p.PublicKey)
if x == nil {
return errors.New("tuf: invalid ecdsa public key point")
}

p.key = key
fmt.Fprintln(os.Stderr, "tuf: warning using deprecated ecdsa hex-encoded keys")
return nil
}
Loading

0 comments on commit 61872a3

Please sign in to comment.