Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
91 changes: 80 additions & 11 deletions pkg/eap/eap.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ const (
CodeSuccess
CodeFailure

SubTypeAkaChallenge = 1
SubTypeAkaChallenge = 1
SubTypeAkaSynchronizationFailure = 4

AT_RAND AttributeType = 1
AT_AUTN AttributeType = 2
AT_RES AttributeType = 3
AT_AUTS AttributeType = 4
AT_CUSTOM_IV AttributeType = 126
)

Expand Down Expand Up @@ -54,9 +56,10 @@ type EapAkaChallenge struct {
amf uint16
op []byte

podIV []byte
pdmIV []byte
Sqn uint64
podIV []byte
pdmIV []byte
Sqn uint64 // the controller's, read from AUTN
lastSqn uint64 // the last one the pod accepted

identifier byte
}
Expand Down Expand Up @@ -107,6 +110,10 @@ func Unmarshal(data []byte) (*EapAka, error) {

}
data = data[2:10] // 8 bytes
case AT_AUTS:
if len != 16 {
return nil, fmt.Errorf("invalid len received for attribute: %d -- %d", aType, len)
}
case AT_CUSTOM_IV:
if len != 8 {
return nil, fmt.Errorf("invalid len received for attribute: %d -- %d", len, aType)
Expand Down Expand Up @@ -162,6 +169,10 @@ func (e *EapAka) Marshal() ([]byte, error) {
buf.WriteByte(0)
buf.WriteByte(64) // RES len in bits
dataLen = 8
case AT_AUTS:
len = 4 // 4 * 4 = 16 bytes, no reserved bytes
buf.WriteByte(len)
dataLen = 14
case AT_CUSTOM_IV:
len = 2
buf.WriteByte(len)
Expand All @@ -180,17 +191,75 @@ func (e *EapAka) Marshal() ([]byte, error) {
return ret, nil
}

func NewEapAkaChallenge(k []byte, sqn uint64) *EapAkaChallenge {
func NewEapAkaChallenge(k []byte, lastSqn uint64) *EapAkaChallenge {
op, _ := hex.DecodeString("cdc202d5123e20f62b6d676ac72cb318")
// amf, _ := hex.DecodeString("b9b9")
log.Debugf("Starting EAP-AKA session with SQN(after incrementing SQN): %d", sqn+1)
log.Debugf("Starting EAP-AKA session; last accepted SQN: %d", lastSqn)
return &EapAkaChallenge{
k: k,
op: op,
Sqn: sqn + 1,
amf: 47545, // b9b9
podIV: []byte{0xa, 0xa, 0xa, 0xa}, // constant for now, easier to debug. TODO
k: k,
op: op,
lastSqn: lastSqn,
amf: 47545, // b9b9
podIV: []byte{0xa, 0xa, 0xa, 0xa}, // constant for now, easier to debug. TODO
}
}

// CheckSqn reads the controller's SQN from AUTN (SQN xor AK || AMF || MAC-A) and
// verifies MAC-A. It returns false when the SQN is not past the last one accepted,
// which a pod answers with a synchronization failure.
func (e *EapAkaChallenge) CheckSqn() (bool, error) {
if len(e.autn) != 16 || len(e.rand) != 16 {
return false, fmt.Errorf("missing RAND or AUTN")
}
_, _, _, ak, err := milenage.New(e.k, e.op, e.rand, 0, 0).F2345()
if err != nil {
return false, err
}
var sqn uint64
for i := 0; i < 6; i++ {
sqn = sqn<<8 | uint64(e.autn[i]^ak[i])
}
amf := uint16(e.autn[6])<<8 | uint16(e.autn[7])
mac, err := milenage.New(e.k, e.op, e.rand, sqn, amf).F1()
if err != nil {
return false, err
}
if !bytes.Equal(mac, e.autn[8:16]) {
return false, fmt.Errorf("AUTN MAC-A mismatch for SQN %d", sqn)
}
e.Sqn = sqn
return sqn > e.lastSqn, nil
}

// GenerateSynchronizationFailure answers a stale SQN with AUTS = SQN_ms xor AK* || MAC-S,
// SQN_ms being the last SQN the pod accepted.
func (e *EapAkaChallenge) GenerateSynchronizationFailure() (*message.Message, error) {
mil := milenage.New(e.k, e.op, e.rand, e.lastSqn, 0)
aks, err := mil.F5Star()
if err != nil {
return nil, err
}
macS, err := mil.F1Star(mil.SQN, []byte{0, 0})
if err != nil {
return nil, err
}
auts := make([]byte, 0, 14)
for i := 0; i < 6; i++ {
auts = append(auts, mil.SQN[i]^aks[i])
}
auts = append(auts, macS...)

eap := &EapAka{Code: CodeResponse,
Attributes: map[AttributeType]*Attribute{AT_AUTS: {Data: auts}},
SubType: SubTypeAkaSynchronizationFailure,
Identifier: e.identifier,
}
ret := message.NewMessage(message.MessageTypeSessionEstablishment, e.podID, e.pdmID)
ret.Payload, err = eap.Marshal()
if err != nil {
return nil, err
}
return ret, nil
}

func (e *EapAkaChallenge) ParseChallenge(msg *message.Message) error {
Expand Down
12 changes: 12 additions & 0 deletions pkg/eap/eap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ func TestEapAka_MarshalUnmarshal(t *testing.T) {
},
},
},
{
name: "synchronization failure",
eap: &EapAka{
Code: CodeResponse,
SubType: SubTypeAkaSynchronizationFailure,
Attributes: map[AttributeType]*Attribute{
AT_AUTS: {
Data: make([]byte, 14),
},
},
},
},
}

for _, tt := range tests {
Expand Down
41 changes: 29 additions & 12 deletions pkg/pod/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,27 +183,44 @@ func (p *Pod) StartActivation() {
log.Fatalf("pkg pod; could not get LTK %s", err)
}
log.Infof("pkg pod; LTK %x", p.state.LTK)
p.state.EapAkaSeq = 1
p.state.EapAkaSeq = 0 // the controller's first session uses SQN 1
p.state.Save()

p.EapAka()
}

func (p *Pod) EapAka() {

session := eap.NewEapAkaChallenge(p.state.LTK, p.state.EapAkaSeq)

msg, ok := p.readSetup()
if !ok {
return
}
err := session.ParseChallenge(msg)
if err != nil {
p.startOver("error parsing the EAP-AKA challenge: %s", err)
return
var session *eap.EapAkaChallenge
var msg *message.Message
var ok bool
for {
session = eap.NewEapAkaChallenge(p.state.LTK, p.state.EapAkaSeq)
if msg, ok = p.readSetup(); !ok {
return
}
if err := session.ParseChallenge(msg); err != nil {
p.startOver("error parsing the EAP-AKA challenge: %s", err)
return
}
fresh, err := session.CheckSqn()
if err != nil {
p.startOver("error checking the EAP-AKA challenge: %s", err)
return
}
if fresh {
break
}
// Another controller has had a session since this one's last: tell it our SQN.
// It retries on the same connection with the next one.
log.Infof("pkg pod; EAP-AKA SQN %d is not past %d; sending a synchronization failure", session.Sqn, p.state.EapAkaSeq)
if msg, err = session.GenerateSynchronizationFailure(); err != nil {
log.Fatalf("pkg pod; error generating the eap-aka synchronization failure: %s", err)
}
p.ble.WriteMessage(msg)
}

msg, err = session.GenerateChallengeResponse()
msg, err := session.GenerateChallengeResponse()
if err != nil {
log.Fatalf("pkg pod; error generating the eap-aka challenge response")
}
Expand Down
142 changes: 142 additions & 0 deletions pkg/pod/pod_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package pod

import (
"bytes"
"encoding/hex"
"path/filepath"
"sync"
"testing"
"time"

"github.com/avereha/pod/pkg/bluetooth"
"github.com/avereha/pod/pkg/eap"
"github.com/avereha/pod/pkg/message"
"github.com/wmnsk/milenage"
)

// radio carries one side's notifications to the other side, and records
Expand Down Expand Up @@ -83,3 +87,141 @@ func TestAGarbledSessionMessageHangsUp(t *testing.T) {
t.Fatal("the pod did not hang up")
}
}

var milenageOP, _ = hex.DecodeString("cdc202d5123e20f62b6d676ac72cb318")

// challenge is the controller's EAP-AKA challenge: AUTN = SQN xor AK || AMF || MAC-A.
func challenge(t *testing.T, ltk, rand []byte, sqn uint64) *message.Message {
m := milenage.New(ltk, milenageOP, rand, sqn, 0xb9b9)
_, _, _, ak, err := m.F2345()
if err != nil {
t.Fatal(err)
}
mac, err := m.F1()
if err != nil {
t.Fatal(err)
}
autn := make([]byte, 0, 16)
for i := 0; i < 6; i++ {
autn = append(autn, m.SQN[i]^ak[i])
}
autn = append(append(autn, 0xb9, 0xb9), mac...)
msg := message.NewMessage(message.MessageTypeSessionEstablishment, []byte{1, 2, 3, 4}, []byte{5, 6, 7, 8})
msg.Payload, err = (&eap.EapAka{Code: eap.CodeRequest, SubType: eap.SubTypeAkaChallenge,
Attributes: map[eap.AttributeType]*eap.Attribute{
eap.AT_RAND: {Data: rand},
eap.AT_AUTN: {Data: autn},
eap.AT_CUSTOM_IV: {Data: []byte{1, 2, 3, 4}},
}}).Marshal()
if err != nil {
t.Fatal(err)
}
return msg
}

func readEap(t *testing.T, phone *bluetooth.Ble) *eap.EapAka {
msg, didTimeout, _ := phone.ReadMessageWithTimeout(2 * time.Second)
if didTimeout || msg == nil {
t.Fatal("no answer from the pod")
}
e, err := eap.Unmarshal(msg.Payload)
if err != nil {
t.Fatal(err)
}
return e
}

// A controller whose SQN is behind gets the pod's in AUTS, as OmnipodKit reads it, and its
// retry with the next SQN on the same connection establishes the session.
func TestAStaleSqnGetsASynchronizationFailure(t *testing.T) {
toPhone, toPod := &radio{hungUp: make(chan bool, 1)}, &radio{}
podBle, phone := newBle(t, toPhone), newBle(t, toPod)
toPod.peer = podBle
toPhone.peer = phone

ltk := bytes.Repeat([]byte{0x5a}, 16)
state := NewFreshState(filepath.Join(t.TempDir(), "state.toml"))
state.LTK = ltk
state.EapAkaSeq = 40
p := New(podBle, state)

podBle.Connected()
go p.StartAcceptingCommands()
podBle.CmdWritten(bluetooth.Packet{6, 1, 4})
phone.Connected()
phone.StartMessageLoop()

rand := bytes.Repeat([]byte{0x11}, 16)
phone.WriteMessage(challenge(t, ltk, rand, 37))
answer := readEap(t, phone)
if answer.SubType != eap.SubTypeAkaSynchronizationFailure || answer.Attributes[eap.AT_AUTS] == nil {
t.Fatalf("expected a synchronization failure, got %+v", answer)
}
auts := answer.Attributes[eap.AT_AUTS].Data
m := milenage.New(ltk, milenageOP, rand, 0, 0)
aks, err := m.F5Star()
if err != nil {
t.Fatal(err)
}
sqnMs := make([]byte, 6)
for i := range sqnMs {
sqnMs[i] = auts[i] ^ aks[i]
}
if !bytes.Equal(sqnMs, []byte{0, 0, 0, 0, 0, 40}) {
t.Fatalf("AUTS carries SQN %x, want 40", sqnMs)
}
macS, err := m.F1Star(sqnMs, []byte{0, 0})
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(macS, auts[6:]) {
t.Fatalf("MAC-S %x, want %x", auts[6:], macS)
}

phone.WriteMessage(challenge(t, ltk, bytes.Repeat([]byte{0x22}, 16), 41))
if answer = readEap(t, phone); answer.SubType != eap.SubTypeAkaChallenge || answer.Attributes[eap.AT_RES] == nil {
t.Fatalf("expected a challenge response, got %+v", answer)
}
success := message.NewMessage(message.MessageTypeSessionEstablishment, []byte{1, 2, 3, 4}, []byte{5, 6, 7, 8})
success.Payload, _ = (&eap.EapAka{Code: eap.CodeSuccess}).Marshal()
phone.WriteMessage(success)

deadline := time.Now().Add(2 * time.Second)
for {
p.mtx.Lock()
sqn := p.state.EapAkaSeq
p.mtx.Unlock()
if sqn == 41 {
break
}
if time.Now().After(deadline) {
t.Fatalf("pod SQN %d after the session, want 41", sqn)
}
time.Sleep(10 * time.Millisecond)
}
}

// An AUTN whose MAC-A doesn't verify ends the connection.
func TestABadAutnHangsUp(t *testing.T) {
toPhone, toPod := &radio{hungUp: make(chan bool, 1)}, &radio{}
podBle, phone := newBle(t, toPhone), newBle(t, toPod)
toPod.peer = podBle
toPhone.peer = phone

state := NewFreshState(filepath.Join(t.TempDir(), "state.toml"))
state.LTK = bytes.Repeat([]byte{0x5a}, 16)
p := New(podBle, state)

podBle.Connected()
go p.StartAcceptingCommands()
podBle.CmdWritten(bluetooth.Packet{6, 1, 4})
phone.Connected()
phone.StartMessageLoop()

phone.WriteMessage(challenge(t, bytes.Repeat([]byte{0x77}, 16), bytes.Repeat([]byte{0x11}, 16), 1))
select {
case <-toPhone.hungUp:
case <-time.After(2 * time.Second):
t.Fatal("the pod did not hang up")
}
}
Loading