Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trail of bits 018 #9674

Merged
merged 11 commits into from
Aug 17, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions command/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ func (c *AgentCommand) Run(args []string) int {
Client: client,
WrapTTL: sc.WrapTTL,
DHType: sc.DHType,
DeriveKey: sc.DeriveKey,
DHPath: sc.DHPath,
AAD: sc.AAD,
}
Expand Down
6 changes: 5 additions & 1 deletion command/agent/cert_with_name_end_to_end_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,11 @@ func testCertWithNameEndToEnd(t *testing.T, ahWrapping bool) {
continue
}

aesKey, err := dhutil.GenerateSharedKey(pri, resp.Curve25519PublicKey)
shared, err := dhutil.GenerateSharedSecret(pri, resp.Curve25519PublicKey)
if err != nil {
t.Fatal(err)
}
aesKey, err := dhutil.DeriveSharedKey(shared, pub, resp.Curve25519PublicKey)
if err != nil {
t.Fatal(err)
}
Expand Down
6 changes: 5 additions & 1 deletion command/agent/cert_with_no_name_end_to_end_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,11 @@ func testCertWithNoNAmeEndToEnd(t *testing.T, ahWrapping bool) {
continue
}

aesKey, err := dhutil.GenerateSharedKey(pri, resp.Curve25519PublicKey)
shared, err := dhutil.GenerateSharedSecret(pri, resp.Curve25519PublicKey)
if err != nil {
t.Fatal(err)
}
aesKey, err := dhutil.DeriveSharedKey(shared, pub, resp.Curve25519PublicKey)
if err != nil {
t.Fatal(err)
}
Expand Down
4 changes: 4 additions & 0 deletions command/agent/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type Sink struct {
WrapTTLRaw interface{} `hcl:"wrap_ttl"`
WrapTTL time.Duration `hcl:"-"`
DHType string `hcl:"dh_type"`
DeriveKey bool `hcl:"derive_key"`
DHPath string `hcl:"dh_path"`
AAD string `hcl:"aad"`
AADEnvVar string `hcl:"aad_env_var"`
Expand Down Expand Up @@ -395,6 +396,9 @@ func parseSinks(result *Config, list *ast.ObjectList) error {
if s.AAD != "" {
return multierror.Prefix(errors.New("specifying AAD data without 'dh_type' does not make sense"), fmt.Sprintf("sink.%s", s.Type))
}
if s.DeriveKey {
return multierror.Prefix(errors.New("specifying 'derive_key' data without 'dh_type' does not make sense"), fmt.Sprintf("sink.%s", s.Type))
}
case s.DHPath != "" && s.DHType != "":
default:
return multierror.Prefix(errors.New("'dh_type' and 'dh_path' must be specified together"), fmt.Sprintf("sink.%s", s.Type))
Expand Down
1 change: 1 addition & 0 deletions command/agent/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ func TestLoadConfigFile(t *testing.T) {
DHType: "curve25519",
DHPath: "/tmp/file-foo-dhpath2",
AAD: "aad",
DeriveKey: true,
Config: map[string]interface{}{
"path": "/tmp/file-bar",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ auto_auth {
aad_env_var = "TEST_AAD_ENV"
dh_type = "curve25519"
dh_path = "/tmp/file-foo-dhpath2"
derive_key = true
config = {
path = "/tmp/file-bar"
}
Expand Down
1 change: 1 addition & 0 deletions command/agent/config/test-fixtures/config.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ auto_auth {
aad_env_var = "TEST_AAD_ENV"
dh_type = "curve25519"
dh_path = "/tmp/file-foo-dhpath2"
derive_key = true
config = {
path = "/tmp/file-bar"
}
Expand Down
6 changes: 5 additions & 1 deletion command/agent/jwt_end_to_end_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,11 @@ func testJWTEndToEnd(t *testing.T, ahWrapping bool) {
continue
}

aesKey, err := dhutil.GenerateSharedKey(pri, resp.Curve25519PublicKey)
shared, err := dhutil.GenerateSharedSecret(pri, resp.Curve25519PublicKey)
if err != nil {
t.Fatal(err)
}
aesKey, err := dhutil.DeriveSharedKey(shared, pub, resp.Curve25519PublicKey)
if err != nil {
t.Fatal(err)
}
Expand Down
12 changes: 11 additions & 1 deletion command/agent/sink/sink.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type SinkConfig struct {
WrapTTL time.Duration
DHType string
DHPath string
DeriveKey bool
AAD string
cachedRemotePubKey []byte
cachedPubKey []byte
Expand Down Expand Up @@ -205,7 +206,16 @@ func (s *SinkConfig) encryptToken(token string) (string, error) {
resp.Curve25519PublicKey = s.cachedPubKey
}

aesKey, err = dhutil.GenerateSharedKey(s.cachedPriKey, s.cachedRemotePubKey)
secret, err := dhutil.GenerateSharedSecret(s.cachedPriKey, s.cachedRemotePubKey)
if err != nil {
return "", errwrap.Wrapf("error calculating shared key: {{err}}", err)
}
if s.DeriveKey {
aesKey, err = dhutil.DeriveSharedKey(secret, s.cachedPubKey, s.cachedRemotePubKey)
} else {
aesKey = secret
}

if err != nil {
return "", errwrap.Wrapf("error deriving shared key: {{err}}", err)
}
Expand Down
41 changes: 35 additions & 6 deletions helper/dhutil/dhutil.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package dhutil

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"golang.org/x/crypto/hkdf"
"io"

"golang.org/x/crypto/curve25519"
Expand Down Expand Up @@ -36,21 +39,47 @@ func GeneratePublicPrivateKey() ([]byte, []byte, error) {

// generateSharedKey uses the private key and the other party's public key to
ncabatoff marked this conversation as resolved.
Show resolved Hide resolved
// generate the shared secret.
func GenerateSharedKey(ourPrivate, theirPublic []byte) ([]byte, error) {
func GenerateSharedSecret(ourPrivate, theirPublic []byte) ([]byte, error) {
if len(ourPrivate) != 32 {
return nil, fmt.Errorf("invalid private key length: %d", len(ourPrivate))
}
if len(theirPublic) != 32 {
return nil, fmt.Errorf("invalid public key length: %d", len(theirPublic))
}

var scalar, pub, secret [32]byte
copy(scalar[:], ourPrivate)
copy(pub[:], theirPublic)
return curve25519.X25519(ourPrivate, theirPublic)
}

// DeriveSharedKey uses HKDF to derive a key from a shared secret and public keys
func DeriveSharedKey(secret, ourPublic, theirPublic []byte) ([]byte, error) {
// Derive the final key from the HKDF of the secret and public keys.

//These must be consistently ordered we're on "our" side of key negotiation or the other.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain a bit more (or link to some reading material) why the result of the comparison determines the order of arguments?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internally, HKDF hashes the secret and two public keys. If Alice and Bob are doing DH key exchange, Alice calculates:

HKDF(secret, A, B) since ourPublic is A.

Bob calculates HKDF(secret, B, A), since Bob's ours is B. That produces a different value. Now we only care that both public keys participate in the derivation, so simply sorting them so they are in a consistent numerical order (either one would do) arrives at an agreed value.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind adding that as a comment?

var pub1 []byte
var pub2 []byte
switch bytes.Compare(ourPublic, theirPublic) {
ncabatoff marked this conversation as resolved.
Show resolved Hide resolved
case 0:
return nil, errors.New("same public key supplied for both participants")
case -1:
pub1 = ourPublic
pub2 = theirPublic
case 1:
pub1 = theirPublic
pub2 = ourPublic
}

curve25519.ScalarMult(&secret, &scalar, &pub)
kio := hkdf.New(sha256.New, secret, pub1, pub2)

return secret[:], nil
var key [32]byte
n, err := io.ReadFull(kio, key[:])
if n != 32 {
return nil, errors.New("short read from hkdf")
}
if err != nil {
ncabatoff marked this conversation as resolved.
Show resolved Hide resolved
// Don't return the key along with the error to prevent misuse
return nil, err
}
return key[:], nil
}

// Use AES256-GCM to encrypt some plaintext with a provided key. The returned values are
Expand Down
5 changes: 5 additions & 0 deletions website/pages/docs/agent/autoauth/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ These configuration values are common to all Sinks:
agent should read the client's initial parameters (e.g. curve25519 public
key).

- `derive_key` `(bool: false)` - If specified, the final encryption key is
calculated by using HKDF-SHA256 to derive a key from the calculated shared
secret and the two public keys for enhanced security. This is recommended
if backward compatibility isn't a concern.

- `aad` `(string: optional)` - If specified, additional authenticated data to
use with the AES-GCM encryption of the token. Can be any string, including
serialized data.
Expand Down