penguin/homekey-go

homekey-go

internal/nfcauth/auth_test.go

raw ยท 6912 bytes

package nfcauth

import (
	"bytes"
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/ecdh"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha1"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"math/big"
	"os"
	"testing"

	"homekey.local/provisioner/internal/provision"
)

func hx(s string) []byte { b, _ := hex.DecodeString(s); return b }
func TestIndependentVectors(t *testing.T) {
	b, e := os.ReadFile("testdata/standard.json")
	if e != nil {
		t.Fatal(e)
	}
	var f map[string]string
	if e = json.Unmarshal(b, &f); e != nil {
		t.Fatal(e)
	}
	for _, v := range []struct {
		name, key string
		n         int
	}{{"Volatile", "volatile", 48}, {"Persistent", "persistent", 32}} {
		got, e := derive(hx(f["shared"]), hx(f["nonce"]), hx(f["reader_pub"])[1:33], hx(f["device_pub"])[1:33], hx(f["versions"]), v.name, v.n)
		if e != nil || !bytes.Equal(got, hx(f[v.key])) {
			t.Fatalf("%s vector mismatch: %v", v.name, e)
		}
	}
	keys := hx(f["volatile"])
	got, e := decryptResponse(hx(f["response"]), keys[:16], keys[32:], 0)
	if e != nil || !bytes.Equal(got, hx(f["plaintext"])) {
		t.Fatalf("secure response vector: %v", e)
	}
	for i := range hx(f["response"]) {
		bad := hx(f["response"])
		bad[i] ^= 1
		if _, e := decryptResponse(bad, keys[:16], keys[32:], 0); e == nil {
			t.Fatalf("tampered byte %d accepted", i)
		}
	}
}
func TestCMACRFC4493(t *testing.T) {
	key := hx("2b7e151628aed2a6abf7158809cf4f3c")
	for _, v := range []struct{ data, mac string }{
		{"", "bb1d6929e95937287fa37d129b756746"},
		{"6bc1bee22e409f96e93d7e117393172a", "070a16b46b4d4144f79bdd9dd04a287c"},
		{"6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411", "dfa66747de9ae63030ca32611497c827"},
	} {
		got, e := cmac(key, hx(v.data))
		if e != nil || !bytes.Equal(got, hx(v.mac)) {
			t.Fatalf("CMAC vector failed: %v", e)
		}
	}
}

// A phone simulator exercises reader proof, response MAC and endpoint proof.
// The independent vectors above separately verify KDF and encryption framing.
type phone struct {
	t     *testing.T
	creds provision.Credentials
	key   *ecdsa.PrivateKey
	eph   *ecdh.PrivateKey
	auth0 map[byte][]byte
	step  int
	mode  string
}

func (p *phone) Exchange(ctx context.Context, cmd []byte) ([]byte, error) {
	if ctx.Err() != nil {
		return nil, ctx.Err()
	}
	p.step++
	ok := func(b []byte) []byte { return append(b, 0x90, 0) }
	switch p.step {
	case 1:
		if !bytes.Equal(cmd, []byte{0, 0xa4, 4, 0, 7, 0xa0, 0, 0, 8, 0x58, 1, 1, 0}) {
			p.t.Fatal("wrong SELECT")
		}
		if p.mode == "status" {
			return []byte{0x6a, 0x82}, nil
		}
		return ok(tlv(0x5c, []byte{2, 0, 1, 0})), nil
	case 2:
		if len(cmd) < 5 || !bytes.Equal(cmd[:4], []byte{0x80, 0x80, 0, 1}) || int(cmd[4]) != len(cmd)-5 {
			p.t.Fatal("wrong AUTH0 framing")
		}
		f, e := fields(cmd[5:])
		if e != nil {
			return nil, e
		}
		p.auth0 = f
		scalar := hx(p.creds.ReaderPrivateKey)
		h := sha256.Sum256(append([]byte("key-identifier"), scalar...))
		id := append(h[:8], hx(p.creds.ReaderIdentifier)...)
		if !bytes.Equal(f[0x4d], id) || len(f[0x4c]) != 16 {
			p.t.Fatal("wrong reader ID or nonce")
		}
		if p.mode == "bad-point" {
			return ok(tlv(0x86, make([]byte, 65))), nil
		}
		p.eph, e = ecdh.P256().GenerateKey(rand.Reader)
		if e != nil {
			return nil, e
		}
		return ok(tlv(0x86, p.eph.PublicKey().Bytes())), nil
	case 3:
		if !bytes.Equal(cmd[:4], []byte{0x80, 0x81, 0, 0}) {
			p.t.Fatal("wrong AUTH1 framing")
		}
		f, e := fields(cmd[5:])
		if e != nil {
			return nil, e
		}
		a := p.auth0
		dx := p.eph.PublicKey().Bytes()[1:33]
		rx := a[0x87][1:33]
		scalar := hx(p.creds.ReaderPrivateKey)
		x, y := elliptic.P256().ScalarBaseMult(scalar)
		digest := sha256.Sum256(transcript(a[0x4d], dx, rx, a[0x4c], []byte{0x41, 0x5d, 0x95, 0x69}))
		sig := f[0x9e]
		if len(sig) != 64 || !ecdsa.Verify(&ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}, digest[:], new(big.Int).SetBytes(sig[:32]), new(big.Int).SetBytes(sig[32:])) {
			p.t.Fatal("reader proof invalid")
		}
		devicePub := elliptic.Marshal(elliptic.P256(), p.key.X, p.key.Y)
		eid := sha1.Sum(devicePub)
		digest = sha256.Sum256(transcript(a[0x4d], dx, rx, a[0x4c], []byte{0x4e, 0x88, 0x7b, 0x4c}))
		r, s, e := ecdsa.Sign(rand.Reader, p.key, digest[:])
		if e != nil {
			return nil, e
		}
		proof := make([]byte, 64)
		r.FillBytes(proof[:32])
		s.FillBytes(proof[32:])
		if p.mode == "bad-signature" {
			proof[0] ^= 1
		}
		plain := append(tlv(0x4e, eid[:6]), tlv(0x9e, proof)...)
		readerEph, e := ecdh.P256().NewPublicKey(a[0x87])
		if e != nil {
			return nil, e
		}
		shared, e := p.eph.ECDH(readerEph)
		if e != nil {
			return nil, e
		}
		keys, e := derive(shared, a[0x4c], rx, dx, []byte{2, 0, 1, 0}, "Volatile", 48)
		if e != nil {
			return nil, e
		}
		block, _ := aes.NewCipher(keys[:16])
		iv := make([]byte, 16)
		pcb := make([]byte, 16)
		pcb[0] = 0x80
		block.Encrypt(iv, pcb)
		plain = append(plain, 0x80)
		for len(plain)%16 != 0 {
			plain = append(plain, 0)
		}
		encrypted := make([]byte, len(plain))
		cipher.NewCBCEncrypter(block, iv).CryptBlocks(encrypted, plain)
		mac, _ := cmac(keys[32:], append(make([]byte, 16), encrypted...))
		out := append(encrypted, mac[:8]...)
		if p.mode == "bad-mac" {
			out[len(out)-1] ^= 1
		}
		return ok(out), nil
	}
	return nil, fmt.Errorf("unexpected command")
}
func TestAuthenticate(t *testing.T) {
	for _, mode := range []string{"valid", "inactive", "unknown", "bad-point", "bad-signature", "bad-mac", "status"} {
		t.Run(mode, func(t *testing.T) {
			reader, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
			device, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
			scalar := make([]byte, 32)
			reader.D.FillBytes(scalar)
			raw := elliptic.Marshal(elliptic.P256(), device.X, device.Y)
			id := sha1.Sum(raw)
			eid := hex.EncodeToString(id[:6])
			ep := provision.Endpoint{PublicKey: hex.EncodeToString(raw), KeyType: 2, KeyState: 1}
			if mode == "inactive" {
				ep.KeyState = 0
			}
			endpoints := map[string]provision.Endpoint{eid: ep}
			if mode == "unknown" {
				endpoints = map[string]provision.Endpoint{}
			}
			c := provision.Credentials{ReaderPrivateKey: hex.EncodeToString(scalar), ReaderIdentifier: "0102030405060708", Issuers: map[string]provision.Issuer{"issuer": {Endpoints: endpoints}}}
			p := &phone{t: t, creds: c, key: device, mode: mode}
			result, e := Authenticate(context.Background(), p, c)
			if mode == "valid" {
				if e != nil || result == nil || result.EndpointID != eid || len(result.PersistentKey) != 32 {
					t.Fatalf("valid auth failed: %v", e)
				}
			} else if e == nil || result != nil {
				t.Fatalf("%s accepted", mode)
			}
		})
	}
}
func TestBERRejectsMalformed(t *testing.T) {
	for _, b := range [][]byte{{0x5c}, {0x5c, 4, 2, 0}, {0x5c, 0x80}, {0x5c, 0x81, 2, 2, 0}, {0x5c, 2, 2, 0, 0x5c, 2, 1, 0}} {
		if _, e := fields(b); e == nil {
			t.Fatalf("accepted malformed BER %x", b)
		}
	}
}