Skip to content

Commit

Permalink
Add support for encrypting vehicle responses
Browse files Browse the repository at this point in the history
Clients indicate that they support encrypted vehhicle responses by
setting a flag bit in the request. The flag is authenticated but ignored
by vehicles that do not support encryption.

Clients should always set the flag when possible. In the future,
vehicles may refuse to respond to certain requests if the client does
not support this feature.
  • Loading branch information
Seth Terashima authored and sethterashima committed Nov 18, 2024
1 parent 55b7965 commit 856a76e
Show file tree
Hide file tree
Showing 21 changed files with 750 additions and 302 deletions.
2 changes: 0 additions & 2 deletions internal/authentication/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"github.com/golang-jwt/jwt/v5"
)

const testVIN = "0123456789abcdefX"

func TestVerify(t *testing.T) {
pkey := []byte{
0x04, 0x77, 0x5e, 0x2e, 0xf5, 0x70, 0xd2, 0x92, 0xdf, 0x42, 0x4c, 0x09,
Expand Down
37 changes: 37 additions & 0 deletions internal/authentication/peer.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,40 @@ func (p *Peer) hmacTag(message *universal.RoutableMessage, hmacData *signatures.
}
return meta.Checksum(message.GetProtobufMessageAsBytes()), nil
}

func RequestID(message *universal.RoutableMessage) []byte {
sigData := message.GetSignatureData()
if sigData.GetSigType() == nil {
return nil
}

switch s := sigData.GetSigType().(type) {
case *signatures.SignatureData_AES_GCM_PersonalizedData:
return append(
[]byte{byte(signatures.SignatureType_SIGNATURE_TYPE_AES_GCM_PERSONALIZED)},
s.AES_GCM_PersonalizedData.GetTag()...)
case *signatures.SignatureData_HMAC_PersonalizedData:
tag := s.HMAC_PersonalizedData.GetTag()
if message.GetToDestination().GetDomain() == universal.Domain_DOMAIN_VEHICLE_SECURITY {
tag = tag[:16]
}
return append(
[]byte{byte(signatures.SignatureType_SIGNATURE_TYPE_HMAC_PERSONALIZED)}, tag...)
default:
return nil
}
}

func (p *Peer) responseMetadata(message *universal.RoutableMessage, id []byte, counter uint32) ([]byte, error) {
meta := newMetadata()
meta.Add(signatures.Tag_TAG_SIGNATURE_TYPE, []byte{byte(signatures.SignatureType_SIGNATURE_TYPE_AES_GCM_RESPONSE)})
meta.Add(signatures.Tag_TAG_DOMAIN, []byte{byte(message.GetFromDestination().GetDomain())})
if err := meta.Add(signatures.Tag_TAG_PERSONALIZATION, p.verifierName); err != nil {
return nil, err
}
meta.AddUint32(signatures.Tag_TAG_COUNTER, counter)
meta.AddUint32(signatures.Tag_TAG_FLAGS, message.Flags)
meta.Add(signatures.Tag_TAG_REQUEST_HASH, id)
meta.AddUint32(signatures.Tag_TAG_FAULT, uint32(message.GetSignedMessageStatus().GetSignedMessageFault()))
return meta.Checksum(nil), nil
}
46 changes: 46 additions & 0 deletions internal/authentication/peer_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package authentication

import (
"bytes"
"testing"

"github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/signatures"
universal "github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/universalmessage"
)

Expand Down Expand Up @@ -84,3 +86,47 @@ func checkError(t *testing.T, err error, expectedCode universal.MessageFault_E)
t.Errorf("Got unexpected error type: %s", err)
}
}

func TestRequestID(t *testing.T) {
tag := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}
message := universal.RoutableMessage{
ToDestination: &universal.Destination{
SubDestination: &universal.Destination_Domain{
Domain: universal.Domain_DOMAIN_VEHICLE_SECURITY,
},
},
SubSigData: &universal.RoutableMessage_SignatureData{
SignatureData: &signatures.SignatureData{
SigType: &signatures.SignatureData_HMAC_PersonalizedData{
HMAC_PersonalizedData: &signatures.HMAC_Personalized_Signature_Data{
Tag: append([]byte{}, tag...),
},
},
},
},
}
id := RequestID(&message)
if len(id) != 17 {
t.Errorf("Expected 17-byte id, but got %d bytes", len(id))
}
if id[0] != byte(signatures.SignatureType_SIGNATURE_TYPE_HMAC_PERSONALIZED) {
t.Errorf("Invalid first byte of request ID: %02x", id[0])
}
if !bytes.Equal(id[1:], tag[:16]) {
t.Errorf("Expected: %02x", tag[:16])
t.Errorf("Observed: %02x", id[1:])
}

message.ToDestination.SubDestination = &universal.Destination_Domain{
Domain: universal.Domain_DOMAIN_INFOTAINMENT,
}

id = RequestID(&message)
if id[0] != byte(signatures.SignatureType_SIGNATURE_TYPE_HMAC_PERSONALIZED) {
t.Errorf("Invalid first byte of request ID: %02x", id[0])
}
if !bytes.Equal(id[1:], tag) {
t.Errorf("Expected: %02x", tag)
t.Errorf("Observed: %02x", id[1:])
}
}
29 changes: 29 additions & 0 deletions internal/authentication/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,32 @@ func (s *Signer) AuthorizeHMAC(message *universal.RoutableMessage, expiresIn tim
}
return nil
}

// Decrypt a Verifier message in place.
//
// Returns the anti-replay counter included in the message, which the client must verify increases
// monotonically for a given id or is inside of a sliding window.
func (s *Signer) Decrypt(message *universal.RoutableMessage, id []byte) (uint32, error) {
gcmInfo := message.GetSignatureData().GetAES_GCM_ResponseData()
if gcmInfo == nil {
return 0, newError(errCodeBadParameter, "missing AES-GCM data")
}
authenticatedData, err := s.responseMetadata(message, id, gcmInfo.Counter)
if err != nil {
return 0, nil
}
plaintext, err := s.session.Decrypt(
gcmInfo.Nonce,
message.GetProtobufMessageAsBytes(),
authenticatedData,
gcmInfo.Tag,
)
if err != nil {
return 0, err
}
message.Payload = &universal.RoutableMessage_ProtobufMessageAsBytes{
ProtobufMessageAsBytes: plaintext,
}
message.SubSigData = nil
return gcmInfo.Counter, nil
}
88 changes: 39 additions & 49 deletions internal/authentication/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Verifier struct {
Peer
lock sync.Mutex
window uint64
handle uint32
}

// NewVerifier returns a Verifier.
Expand Down Expand Up @@ -72,6 +73,12 @@ func (v *Verifier) rotateEpochIfNeeded(force bool) error {
return nil
}

func (v *Verifier) AssignHandle(handle uint32) {
v.lock.Lock()
v.handle = handle
v.lock.Unlock()
}

func (v *Verifier) sessionInfo() (*signatures.SessionInfo, error) {
if err := v.adjustClock(); err != nil {
return nil, err
Expand All @@ -81,6 +88,7 @@ func (v *Verifier) sessionInfo() (*signatures.SessionInfo, error) {
PublicKey: v.session.LocalPublicBytes(),
Epoch: v.epoch[:],
ClockTime: v.timestamp(),
Handle: v.handle,
}
return info, nil
}
Expand Down Expand Up @@ -217,55 +225,6 @@ func (v *Verifier) Verify(message *universal.RoutableMessage) (plaintext []byte,
return
}

// updateSlidingWindow takes the current counter value (i.e., the highest
// counter value of any authentic message received so far), the current sliding
// window, and the newCounter value from an incoming message. The function
// returns the updated counter and window values and sets ok to true if it
// could confirm that newCounter has never been previously used. If ok is
// false, then updatedCounter = counter and updatedWindow = window.
func updateSlidingWindow(counter uint32, window uint64, newCounter uint32) (updatedCounter uint32, updatedWindow uint64, ok bool) {
// If we exit early due to an error, we want to leave the counter/window
// state unchanged. Therefore we initialize return values to the current
// state.
updatedCounter = counter
updatedWindow = window
ok = false

if counter == newCounter {
// This counter value has been used before.
return
}

if newCounter < counter {
// This message arrived out of order.
age := counter - newCounter
if age > windowSize {
// Our history doesn't go back this far, so we can't determine if
// we've seen this newCounter value before.
return
}
if window>>(age-1)&1 == 1 {
// The newCounter value has been used before.
return
}
// Everything looks good.
ok = true
updatedWindow |= (1 << (age - 1))
return
}

// If we've reached this point, newCounter > counter, so newCounter is valid.
ok = true
updatedCounter = newCounter
// Compute how far we need to shift our sliding window.
shiftCount := newCounter - counter
updatedWindow <<= shiftCount
// We need to set the bit in our window that corresponds to counter (if
// newCounter = counter + 1, then this is the first [LSB] of the window).
updatedWindow |= uint64(1) << (shiftCount - 1)
return
}

func (v *Verifier) verifyGCM(message *universal.RoutableMessage, gcmData *signatures.AES_GCM_Personalized_Signature_Data) (plaintext []byte, err error) {
if err = v.verifySessionInfo(message, gcmData); err != nil {
return nil, err
Expand Down Expand Up @@ -337,3 +296,34 @@ func (v *Verifier) verifySessionInfo(message *universal.RoutableMessage, info se
}
return nil
}

// Encrypt a message response in place.
//
// The message id must uniquely identify the Signer's request that prompted the message. The
// counter must increase monotonically for a given id.
func (v *Verifier) Encrypt(message *universal.RoutableMessage, id []byte, counter uint32) error {
plaintext := message.GetProtobufMessageAsBytes()
authenticatedData, err := v.responseMetadata(message, id, counter)
if err != nil {
return err
}
nonce, ciphertext, tag, err := v.session.Encrypt(plaintext, authenticatedData)
if err != nil {
return err
}
message.SubSigData = &universal.RoutableMessage_SignatureData{
SignatureData: &signatures.SignatureData{
SigType: &signatures.SignatureData_AES_GCM_ResponseData{
AES_GCM_ResponseData: &signatures.AES_GCM_Response_Signature_Data{
Counter: counter,
Nonce: nonce,
Tag: tag,
},
},
},
}
message.Payload = &universal.RoutableMessage_ProtobufMessageAsBytes{
ProtobufMessageAsBytes: ciphertext,
}
return nil
}
122 changes: 41 additions & 81 deletions internal/authentication/verifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,86 +550,46 @@ func TestNoSignatureData(t *testing.T) {
runVerifyTest(t, verifier, message, errCodeBadParameter, false)
}

func TestSlidingWindow(t *testing.T) {
type windowTest struct {
counter uint32
window uint64
newCounter uint32
expectedUpdatedCounter uint32
expectedUpdatedWindow uint64
expectedOk bool
}
tests := []windowTest{
// Update should succeed because newCounter is greater than all previous counters.
windowTest{
counter: 100,
window: uint64((1 << 0) | (1 << 5)),
newCounter: 101,
expectedUpdatedCounter: 101,
expectedUpdatedWindow: uint64(1 | (1 << 1) | (1 << 6)),
expectedOk: true,
},
// Update should succeed because newCounter is greater than all previous counters.
// In this test, some messages were skipped and so the expectedUpdatedWindow shifts further.
windowTest{
counter: 100,
window: uint64((1 << 0) | (1 << 5)),
newCounter: 103,
expectedUpdatedCounter: 103,
expectedUpdatedWindow: uint64((1 << 2) | (1 << 3) | (1 << 8)),
expectedOk: true,
},
// Update should succeed because newCounter is greater than all previous counters.
// In this test, the previous counter doesn't fit in sliding window.
windowTest{
counter: 100,
window: uint64((1 << 0) | (1 << 5)),
newCounter: 500,
expectedUpdatedCounter: 500,
expectedUpdatedWindow: 0,
expectedOk: true,
},
// Update should succeed because newCounter falls in window but isn't set.
windowTest{
counter: 100,
window: uint64((1 << 0) | (1 << 5)),
newCounter: 98,
expectedUpdatedCounter: 100,
expectedUpdatedWindow: uint64((1 << 0) | (1 << 1) | (1 << 5)),
expectedOk: true,
},
// Update should fail because newCounter falls in window and is already set.
windowTest{
counter: 100,
window: uint64((1 << 0) | (1 << 5)),
newCounter: 99,
expectedUpdatedCounter: 100,
expectedUpdatedWindow: uint64((1 << 0) | (1 << 5)),
expectedOk: false,
},
// Update should fail because newCounter falls outside of window and freshness cannot be validated.
windowTest{
counter: 100,
window: uint64((1 << 0) | (1 << 5)),
newCounter: 3,
expectedUpdatedCounter: 100,
expectedUpdatedWindow: uint64((1 << 0) | (1 << 5)),
expectedOk: false,
},
// Update should fail because newCounter == counter.
windowTest{
counter: 100,
window: uint64((1 << 0) | (1 << 5)),
newCounter: 100,
expectedUpdatedCounter: 100,
expectedUpdatedWindow: uint64((1 << 0) | (1 << 5)),
expectedOk: false,
},
}
for _, test := range tests {
counter, window, ok := updateSlidingWindow(test.counter, test.window, test.newCounter)
if counter != test.expectedUpdatedCounter || window != test.expectedUpdatedWindow || ok != test.expectedOk {
t.Errorf("Failed window test %+v, got counter=%d, window=%d, ok=%v", test, counter, window, ok)
}
func TestProvideHandle(t *testing.T) {
verifier, _ := getGCMVerifierAndSigner(t)
handle := uint32(0xDEADBEEF)
verifier.AssignHandle(handle)
info, err := verifier.SessionInfo()
if err != nil {
t.Fatal(err)
}
if info.GetHandle() != handle {
t.Fatalf("Invalid handle value: %s", info)
}
}

func TestVerifierEncryption(t *testing.T) {
verifier, signer := getGCMVerifierAndSigner(t)
message := getTestMessage()
message.FromDestination = &universal.Destination{
SubDestination: &universal.Destination_Domain{Domain: testVerifierDomain},
}
plaintext := append([]byte{}, message.GetProtobufMessageAsBytes()...)
id := []byte{1, 2, 3}
counter := uint32(4)
t.Logf("Original message: %+v", message)
if err := verifier.Encrypt(message, id, counter); err != nil {
t.Fatalf("Error encrypting message: %s", err)
}
t.Logf("Encrypted message: %+v", message)
if bytes.Equal(plaintext, message.GetProtobufMessageAsBytes()) {
t.Fatalf("Encryption didn't change payload")
}

id[0] ^= 1
if _, err := signer.Decrypt(message, id); err == nil {
t.Fatalf("Decryption should have failed")
}
id[0] ^= 1
if rxCounter, err := signer.Decrypt(message, id); err != nil || rxCounter != counter {
t.Fatalf("Decryption failed: %s", err)
}
if !bytes.Equal(plaintext, message.GetProtobufMessageAsBytes()) {
t.Fatalf("Decryption failed to recover original plaintext")
}
}
Loading

0 comments on commit 856a76e

Please sign in to comment.