From bbc1a847496c309839d33bc0c9286a08e06a0df4 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Wed, 15 Jan 2020 22:30:50 -0800 Subject: [PATCH] Add a basic example of attestation over BLE Add a trivial remote attestation client and server based on Bluetooth low energy. This is a PoC rather than production-ready code and has multiple rough edges, but demonstrates that remote attestation can be performed over a local network rather than requiring an internet connection and remote server. --- README.md | 25 ++ attest/bluetooth-client/bluetooth-client.go | 297 +++++++++++++++ attest/bluetooth-server/bluetooth-server.go | 377 ++++++++++++++++++++ go.mod | 3 +- go.sum | 25 ++ 5 files changed, 726 insertions(+), 1 deletion(-) create mode 100644 attest/bluetooth-client/bluetooth-client.go create mode 100644 attest/bluetooth-server/bluetooth-server.go diff --git a/README.md b/README.md index b460f7d..9a45b8d 100644 --- a/README.md +++ b/README.md @@ -125,3 +125,28 @@ if err != nil { At this point, the server records the AK and EK association and allows the client to use its AK as a credential (e.g. by issuing it a client certificate). + +## Bluetooth + +Currently only supported under Linux. Build attest/bluetooth-server +and attest/bluetooth-client. Create a 16 byte shared secret: + +``` +dd if=/dev/random of=secret bs=1 count=16 +``` + +and place it on client and server. On the machine you wish to attest, run + +``` +bluetooth-server name secret +``` + +where name is some name that defines the system and secret is the path +to the shared secret. On the machine that should verify the attestation, run + +``` +bluetooth-client name secret +``` + +where name is the same name used on the attesting system and secret is +the path to the shared secret. diff --git a/attest/bluetooth-client/bluetooth-client.go b/attest/bluetooth-client/bluetooth-client.go new file mode 100644 index 0000000..d198562 --- /dev/null +++ b/attest/bluetooth-client/bluetooth-client.go @@ -0,0 +1,297 @@ +package main + +import ( + "context" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/subtle" + "encoding/binary" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "strings" + + "github.com/go-ble/ble" + "github.com/go-ble/ble/examples/lib/dev" + "github.com/google/go-attestation/attest" +) + +var AuthCharUUID = ble.MustParse("ebee1790-50b3-4943-8396-16c0b7231cad") +var EkCharUUID = ble.MustParse("ebee1791-50b3-4943-8396-16c0b7231cad") +var AkCharUUID = ble.MustParse("ebee1792-50b3-4943-8396-16c0b7231cad") +var ActivateCharUUID = ble.MustParse("ebee1793-50b3-4943-8396-16c0b7231cad") +var AttestationCharUUID = ble.MustParse("ebee1794-50b3-4943-8396-16c0b7231cad") + +func readCharacteristic(cln ble.Client, profile ble.Profile, uuid ble.UUID) ([]byte, error) { + readlen := 0 + var buf []byte + char := profile.Find(ble.NewCharacteristic(uuid)) + if char == nil { + return nil, fmt.Errorf("unable to find uuid %s", uuid) + } + + for { + data, err := cln.ReadCharacteristic(char.(*ble.Characteristic)) + if err != nil { + return nil, err + } + if len(data) == 0 { + return nil, fmt.Errorf("0 byte read") + } + if readlen == 0 { + if len(data) < 4 { + return nil, fmt.Errorf("Read too little data: %v", data) + } + readlen = int(binary.LittleEndian.Uint32(data[0:4])) + buf = append(buf, data[4:]...) + } else { + buf = append(buf, data...) + } + if len(buf) == readlen { + return buf, nil + } + } +} + +func writeCharacteristic(cln ble.Client, profile ble.Profile, uuid ble.UUID, buf []byte) error { + written := 0 + char := profile.Find(ble.NewCharacteristic(uuid)) + if char == nil { + return fmt.Errorf("unable to find uuid %s", uuid) + } + + for { + var writebuf [20]byte + writelen := 20 + if written == 0 { + binary.LittleEndian.PutUint32(writebuf[:], uint32(len(buf))) + end := len(buf) + if end > 16 { + end = 16 + } + copy(writebuf[4:20], buf[0:end]) + written = 16 + } else if written < len(buf) { + end := written + 20 + if end > len(buf) { + end = len(buf) + writelen = end - written + } + copy(writebuf[0:20], buf[written:end]) + written = end + } else { + break + } + err := cln.WriteCharacteristic(char.(*ble.Characteristic), writebuf[:writelen], true) + if err != nil { + return err + } + } + return nil +} + +func filter(arg string) ble.AdvFilter { + target := strings.ToLower(arg) + return func(device ble.Advertisement) bool { + if strings.ToLower(device.LocalName()) == target || + strings.ToLower(device.Addr().String()) == target { + return true + } + return false + } +} + +func main() { + var ap attest.AttestationParameters + var ek rsa.PublicKey + var att attest.PlatformParameters + + if len(os.Args) != 3 { + log.Fatalf("Usage: %s target keydata") + } + + data, err := ioutil.ReadFile(os.Args[2]) + if len(data) != 16 { + log.Fatalf("Authentication data is %d bytes long, should be 16", len(data)) + } + + block, err := aes.NewCipher(data) + if err != nil { + log.Fatalf("Failed to create cipher: %v", err) + } + + ctx, _ := context.WithCancel(context.Background()) + + d, err := dev.NewDevice("default") + if err != nil { + log.Fatalf("can't new device : %v", err) + } + ble.SetDefaultDevice(d) + + cln, err := ble.Connect(ctx, filter(os.Args[1])) + if err != nil { + log.Fatalf("Unable to connect: %v", err) + } + + profile, err := cln.DiscoverProfile(true) + if err != nil { + log.Fatalf("Unable to obtain device profile: %v", err) + } + + // Read the challenge from the server. We will encrypt it and pass it + // back to the server in order to prove that we have access to the + // authentication secret. + auth, err := readCharacteristic(cln, *profile, AuthCharUUID) + if err != nil { + log.Fatalf("Unable to read auth challenge: %v", err) + } + + if len(auth) != 32 { + log.Fatalf("Challenge is %d bytes, should be 32", len(auth)) + } + + // Encrypt the challenge + aescbc := cipher.NewCBCEncrypter(block, auth[0:16]) + ciphertext := make([]byte, 16) + aescbc.CryptBlocks(ciphertext, auth[16:32]) + + // Pass the encrypted challenge back to the server + err = writeCharacteristic(cln, *profile, AuthCharUUID, ciphertext) + if err != nil { + log.Fatalf("Failed to write auth challenge: %v", err) + } + + // Read the server's Endorsement Key + encodedEk, err := readCharacteristic(cln, *profile, EkCharUUID) + if err != nil { + log.Fatalf("Unable to read ek: %v", err) + } + + err = json.Unmarshal(encodedEk, &ek) + if err != nil { + log.Fatalf("Unable to unmarshal ek: %v", err) + } + + // At this point we should verify that the EK certificate chains + // back to a known TPM manufacturer, but that's not needed for this + // PoC. We should also save the EK and use it in future in order to + // verify that we're talking to the same TPM. + + // Ask the remote TPM to generate an AK and send it to us. Instead, + // we could persist the AK locally and send it back to the TPM. TODO. + ak, err := readCharacteristic(cln, *profile, AkCharUUID) + if err != nil { + log.Fatalf("Unable to read ak: %v", err) + } + + err = json.Unmarshal(ak, &ap) + if err != nil { + log.Fatalf("Unable to unmarshal ak: %v", err) + } + + // We now have an AK and an EK. We need to verify that the AK + // matches the EK in order to prove that we're talking to the same + // TPM. This is done with credential activation (see + // docs/credential-activation.md) + activation := attest.ActivationParameters{ + TPMVersion: attest.TPMVersion12, + EK: &ek, + AK: ap, + } + + secret, challenge, err := activation.Generate() + if err != nil { + log.Fatalf("Unable to generate activation challenge: %v", err) + } + + encodedChallenge, err := json.Marshal(challenge) + if err != nil { + log.Fatalf("Unable to marshal challenge: %v", err) + } + + err = writeCharacteristic(cln, *profile, ActivateCharUUID, encodedChallenge) + if err != nil { + log.Fatalf("Unable to write credential: %v", err) + } + + // The remote will TPM now attempt to decrypt the secret with the EK + // (proving it owns it) and check that the AK matches the appropriate + // characteristics. If so, it'll return the decrypted secret to us. + + decrypted, err := readCharacteristic(cln, *profile, ActivateCharUUID) + if err != nil { + log.Fatalf("Unable to read decrypted secret: %v", err) + } + + if subtle.ConstantTimeCompare(secret, decrypted) == 0 { + log.Fatalf("Remote failed to generate correct secret - %v != %v", secret, decrypted) + } + + // At this point, we know that the AK corresponds to the EK. We now + // want a quote (a signed copy of the PCR values) and a copy of the + // event log. + nonce := make([]byte, 32) + _, err = rand.Read(nonce) + if err != nil { + log.Fatalf("Unable to generate a nonce: %v", err) + } + + err = writeCharacteristic(cln, *profile, AttestationCharUUID, nonce) + if err != nil { + log.Fatalf("Failed to write nonce: %v", err) + } + + rawquote, err := readCharacteristic(cln, *profile, AttestationCharUUID) + if err != nil { + log.Fatalf("Failed to read quote: %v", err) + } + + err = json.Unmarshal(rawquote, &att) + if err != nil { + log.Fatalf("Unable to marshal platform attestation: %v", err) + } + + // Pull out the AK pub and verify that the quotes are signed with it. + // Since we've already verified that the AK was generated from the EK, + // we know that the quote is coming from the TPM. + pub, err := attest.ParseAKPublic(attest.TPMVersion12, ap.Public) + if err != nil { + log.Fatalf("Failed to parse AK public: %v", err) + } + + for i, q := range att.Quotes { + if err := pub.Verify(q, att.PCRs, nonce); err != nil { + log.Fatalf("quote[%d] verification failed: %v", i, err) + } + } + + // Make sure the event log is well formed. + el, err := attest.ParseEventLog(att.EventLog) + if err != nil { + log.Fatalf("Failed to parse event log: %v", err) + } + + // Verify that the event log matches the PCR values we got. We do this + // by replaying the values in the event log in the same way that the + // TPM responded to them originally. If the values in the log match + // the values that were sent to the TPM, that means that the values + // in the event log match what actually happened during boot. + events, err := el.Verify(att.PCRs) + if err != nil { + log.Fatalf("Log failed to replay: %v", err) + } + + // Finally, examine the event log to determine whether the system + // had UEFI secure boot enabled. There is a specific event recorded + // to PCR 7 that tells us this. + sbState, err := attest.ParseSecurebootState(events) + if err != nil { + log.Fatalf("Failed to parse secure boot state: %v", err) + } + + log.Printf("Validation succeeded - secure boot state is %v", sbState.Enabled) +} diff --git a/attest/bluetooth-server/bluetooth-server.go b/attest/bluetooth-server/bluetooth-server.go new file mode 100644 index 0000000..8370a8e --- /dev/null +++ b/attest/bluetooth-server/bluetooth-server.go @@ -0,0 +1,377 @@ +package main + +import ( + "context" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/subtle" + "encoding/binary" + "encoding/json" + "io/ioutil" + "log" + "os" + + "github.com/go-ble/ble" + "github.com/go-ble/ble/examples/lib/dev" + "github.com/go-ble/ble/linux/hci/evt" + "github.com/google/go-attestation/attest" +) + +var TpmSvcUUID = ble.MustParse("99a84b8f-8d7c-47b0-b24e-800e00fa5d57") + +var AuthCharUUID = ble.MustParse("ebee1790-50b3-4943-8396-16c0b7231cad") +var EkCharUUID = ble.MustParse("ebee1791-50b3-4943-8396-16c0b7231cad") +var AkCharUUID = ble.MustParse("ebee1792-50b3-4943-8396-16c0b7231cad") +var ActivateCharUUID = ble.MustParse("ebee1793-50b3-4943-8396-16c0b7231cad") +var AttestationCharUUID = ble.MustParse("ebee1794-50b3-4943-8396-16c0b7231cad") + +var decrypted []byte +var encodedek []byte +var encodedak []byte +var encodedec []byte +var encodedattestation []byte +var nonce []byte +var tpm *attest.TPM +var ak *attest.AK +var marshaledak []byte +var authkey [16]byte +var authenticated bool + +func writeBuf(written *int, buf []byte, rsp ble.ResponseWriter, force bool) bool { + var writebuf [20]byte + + if authenticated == false && force == false { + return false + } + + writelen := 20 + if *written == 0 { + binary.LittleEndian.PutUint32(writebuf[:], uint32(len(buf))) + end := len(buf) + if end > 16 { + end = 16 + } + copy(writebuf[4:end+4], buf[0:end]) + *written = end + writelen = end + 4 + } else if *written < len(buf) { + end := *written + 20 + if end > len(buf) { + end = len(buf) + writelen = end - *written + } + copy(writebuf[0:20], buf[*written:end]) + *written = end + } else { + writelen = 0 + } + rsp.Write(writebuf[:writelen]) + if *written == len(buf) { + return true + } + return false +} + +func readBuf(readlen *int, buf *[]byte, req ble.Request) bool { + if authenticated == false { + return false + } + + if *readlen == 0 { + *readlen = int(binary.LittleEndian.Uint32(req.Data()[0:4])) + *buf = append(*buf, req.Data()[4:]...) + } else { + *buf = append(*buf, req.Data()...) + } + if len(*buf) == *readlen { + return true + } + return false +} + +func AuthChar() *ble.Characteristic { + var valid bool + var data []byte + written := 0 + + block, err := aes.NewCipher(authkey[:]) + if err != nil { + return nil + } + + authnonce := make([]byte, 16) + challenge := make([]byte, 16) + + _, err = rand.Read(nonce) + if err != nil { + return nil + } + _, err = rand.Read(challenge) + if err != nil { + return nil + } + + data = append(data, authnonce...) + data = append(data, challenge...) + + valid = true + + c := ble.NewCharacteristic(AuthCharUUID) + c.HandleRead(ble.ReadHandlerFunc(func(req ble.Request, rsp ble.ResponseWriter) { + if valid == false { + _, err := rand.Read(authnonce) + if err != nil { + return + } + _, err = rand.Read(challenge) + if err != nil { + return + } + + data = data[:0] + data = append(data, authnonce...) + data = append(data, challenge...) + + valid = true + } + + complete := writeBuf(&written, data, rsp, true) + if complete == true { + written = 0 + } + + })) + c.HandleWrite(ble.WriteHandlerFunc(func(req ble.Request, rsp ble.ResponseWriter) { + written = 0 + if valid == false { + return + } + + valid = false + if len(req.Data()) != 20 { + return + } + aescbc := cipher.NewCBCDecrypter(block, authnonce) + + ciphertext := req.Data()[4:20] + + aescbc.CryptBlocks(ciphertext, ciphertext) + + if subtle.ConstantTimeCompare(challenge, ciphertext) != 0 { + authenticated = true + } else { + log.Printf("Auth expected %v, got %v", challenge, ciphertext) + } + })) + + return c +} + +func EkChar() *ble.Characteristic { + written := 0 + + // Read the Endorsement Key from the TPM and pass it back to the + // client + c := ble.NewCharacteristic(EkCharUUID) + c.HandleRead(ble.ReadHandlerFunc(func(req ble.Request, rsp ble.ResponseWriter) { + complete := writeBuf(&written, encodedek, rsp, false) + if complete == true { + written = 0 + } + })) + + return c +} + +func AkChar() *ble.Characteristic { + var err error + written := 0 + readlen := 0 + + c := ble.NewCharacteristic(AkCharUUID) + + // Load an Attestation Key that was given to us by the client + c.HandleWrite(ble.WriteHandlerFunc(func(req ble.Request, rsp ble.ResponseWriter) { + written = 0 + complete := readBuf(&readlen, &marshaledak, req) + if complete == true { + ak, err = tpm.LoadAK(marshaledak) + if err != nil { + log.Fatalf("Failed to load AK: %v", err) + } + readlen = 0 + } + })) + + // Generate a new Attestation Key and provide it to the client on + // request + c.HandleRead(ble.ReadHandlerFunc(func(req ble.Request, rsp ble.ResponseWriter) { + if len(encodedak) == 0 { + if ak == nil { + ak, err = tpm.NewAK(nil) + if err != nil { + log.Fatalf("Failed to create AK: %v", err) + } + } + + encodedak, err = json.Marshal(ak.AttestationParameters()) + if err != nil { + log.Fatalf("Failed to marshal AK: %v", err) + } + } + complete := writeBuf(&written, encodedak, rsp, false) + if complete == true { + written = 0 + } + })) + + return c +} + +func ActivateChar() *ble.Characteristic { + written := 0 + readlen := 0 + + c := ble.NewCharacteristic(ActivateCharUUID) + + // The client has given us a Credential Activation + // challenge. Decrypt it using the Endorsement Key in order to prove + // that we are the legitimate TPM and that the Activation Key + // matches the Endorsement Key. + c.HandleWrite(ble.WriteHandlerFunc(func(req ble.Request, rsp ble.ResponseWriter) { + written = 0 + complete := readBuf(&readlen, &encodedec, req) + if complete == true { + var ec attest.EncryptedCredential + err := json.Unmarshal(encodedec, &ec) + if err != nil { + log.Fatalf("Failed to unmarshal encrypted credential: %v", err) + } + decrypted, err = ak.ActivateCredential(tpm, ec) + if err != nil { + log.Fatalf("Failed to activate credentials: %v", err) + } + readlen = 0 + } + })) + + // Give the decrypted secret from the Credential Activation + // challenge back to the client. The client will then compare it to + // the secret it generated - if they match then we must have access + // to the Endorsement Key (because otherwise we couldn't have + // decrypted it) + c.HandleRead(ble.ReadHandlerFunc(func(req ble.Request, rsp ble.ResponseWriter) { + complete := writeBuf(&written, decrypted, rsp, false) + if complete == true { + written = 0 + } + })) + + return c +} + +func AttestationChar() *ble.Characteristic { + written := 0 + readlen := 0 + + c := ble.NewCharacteristic(AttestationCharUUID) + + // The client sends us a random nonce. The TPM will take this nonce + // and add it to the list of PCR values, and then sign all of this + // with the Attestation Key. The client will check that the + // signature is valid and that the nonce matches the nonce it sent - + // this avoids a replay attack where an attacker simply sends back + // an old quote. The client is then able to look at the PCR values + // and check whether they're valid. + c.HandleWrite(ble.WriteHandlerFunc(func(req ble.Request, rsp ble.ResponseWriter) { + written = 0 + complete := readBuf(&readlen, &nonce, req) + if complete == true { + att, err := tpm.AttestPlatform(ak, nonce, nil) + if err != nil { + log.Fatalf("Failed to perform attestation: %v", err) + } + + encodedattestation, err = json.Marshal(att) + if err != nil { + log.Fatalf("Unable to marshal attestation data: %v", err) + } + readlen = 0 + } + })) + + // Send the signed PCR quote back to the client + c.HandleRead(ble.ReadHandlerFunc(func(req ble.Request, rsp ble.ResponseWriter) { + complete := writeBuf(&written, encodedattestation, rsp, false) + if complete == true { + written = 0 + } + })) + + return c +} + +func connected(evt.LEConnectionComplete) { + decrypted = decrypted[:0] + encodedak = encodedak[:0] + encodedec = encodedec[:0] + encodedattestation = encodedattestation[:0] + nonce = nonce[:0] +} + +func disconnected(evt.DisconnectionComplete) { + authenticated = false +} + +func main() { + var err error + + if len(os.Args) != 3 { + log.Fatalf("Usage: %s name keydata") + } + + data, err := ioutil.ReadFile(os.Args[2]) + if err != nil { + log.Fatalf("Failed to read authentication key data: %v", err) + } + if len(data) != 16 { + log.Fatalf("Authentication data is %d bytes long, should be 16", len(data)) + } + copy(authkey[:], data) + + tpm, err = attest.OpenTPM(nil) + if err != nil { + log.Fatalf("Failed to open TPM: %v", err) + } + + eks, err := tpm.EKs() + if err != nil { + log.Fatalf("Failed to read EKs: %v", err) + } + + encodedek, err = json.Marshal(eks[0].Public) + if err != nil { + log.Fatalf("Unable to marshal EK: %v", err) + } + + d, err := dev.NewDevice("default", ble.OptConnectHandler(connected), ble.OptDisconnectHandler(disconnected)) + if err != nil { + log.Fatalf("can't new device : %v", err) + } + ble.SetDefaultDevice(d) + tpmSvc := ble.NewService(TpmSvcUUID) + + tpmSvc.AddCharacteristic(AuthChar()) + tpmSvc.AddCharacteristic(EkChar()) + tpmSvc.AddCharacteristic(AkChar()) + tpmSvc.AddCharacteristic(ActivateChar()) + tpmSvc.AddCharacteristic(AttestationChar()) + + if err := ble.AddService(tpmSvc); err != nil { + log.Fatalf("can't add service: %s", err) + } + ctx := ble.WithSigHandler(context.WithCancel(context.Background())) + err = ble.AdvertiseNameAndServices(ctx, os.Args[1], tpmSvc.UUID) + log.Fatalf("Exiting with %v", err) +} diff --git a/go.mod b/go.mod index dbddb8a..6c2415d 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,12 @@ module github.com/google/go-attestation go 1.12 require ( + github.com/go-ble/ble v0.0.0-20200407180624-067514cd6e24 github.com/google/certificate-transparency-go v1.0.22-0.20190605205155-41fc2ef3a2a8 github.com/google/go-cmp v0.3.1 github.com/google/go-tpm v0.2.1-0.20191015210219-431489f43254 github.com/google/go-tpm-tools v0.1.1 github.com/google/go-tspi v0.2.1-0.20190423175329-115dea689aad golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 // indirect - golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 + golang.org/x/sys v0.0.0-20191126131656-8a8471f7e56d ) diff --git a/go.sum b/go.sum index b39e5b2..9a570d0 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,12 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-ble/ble v0.0.0-20200407180624-067514cd6e24 h1:6St0uI/mfzuJX/y596wl2dJmA1VfdBSqopaUfS29z24= +github.com/go-ble/ble v0.0.0-20200407180624-067514cd6e24/go.mod h1:nwmyxHsP2cqjashMTTAl3A5t6V3vzev1rLgMb/pZ7jc= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE= @@ -27,19 +31,36 @@ github.com/google/go-tspi v0.2.1-0.20190423175329-115dea689aad/go.mod h1:xfMGI3G github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab h1:n8cgpHzJ5+EDyDri2s/GC7a9+qK3/YEGnBsd0uS/8PY= +github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab/go.mod h1:y1pL58r5z2VvAjeG1VLGc8zOQgSOzbKN7kMHPvFXJ+8= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/raff/goble v0.0.0-20190909174656-72afc67d6a99/go.mod h1:CxaUhijgLFX0AROtH5mluSY71VqpjQBw9JXE2UKZmc4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= @@ -50,9 +71,13 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 h1:/zi0zzlPHWXYXrO1LjNRByFu8sdGgCkj2JLDdBIB84k= golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191126131656-8a8471f7e56d h1:kCXqdOO2GMlu0vCsEMBXwj/b0E9wyFpNPBpuv/go/F8= +golang.org/x/sys v0.0.0-20191126131656-8a8471f7e56d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=