Skip to content

Commit

Permalink
register: take control of the registration process
Browse files Browse the repository at this point in the history
Till now, the attestation and communication with the elemental operator
were all demanded to the github.com/rancher-sandbox/go-tpm package.
Split TPM attestation from the communication with the elemental operator
demanding TPM authentication to the external library while taking full
control of the communication with the operator.

This doesn't introduce functional changes (so it will keep retrocompatibility
with the current elemental operator) and is a preparatory step to address
#5

Signed-off-by: Francesco Giudici <francesco.giudici@suse.com>
  • Loading branch information
fgiudici committed Aug 31, 2022
1 parent 861753c commit 931c6cf
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 59 deletions.
4 changes: 2 additions & 2 deletions cmd/register/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (

"github.com/mudler/yip/pkg/schema"
"github.com/rancher/elemental-operator/pkg/config"
"github.com/rancher/elemental-operator/pkg/tpm"
"github.com/rancher/elemental-operator/pkg/register"
"github.com/rancher/elemental-operator/pkg/version"
agent "github.com/rancher/system-agent/pkg/config"
"github.com/sanity-io/litter"
Expand Down Expand Up @@ -156,7 +156,7 @@ func run(config config.Config) {
}

for {
data, err = tpm.Register(registration.URL, caCert, !registration.NoSMBIOS, registration.EmulateTPM, registration.EmulatedTPMSeed, registration.Labels)
data, err = register.Register(registration.URL, caCert, !registration.NoSMBIOS, registration.EmulateTPM, registration.EmulatedTPMSeed, registration.Labels)
if err != nil {
logrus.Error("failed to register machine inventory: ", err)
time.Sleep(time.Second * 5)
Expand Down
150 changes: 150 additions & 0 deletions pkg/register/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
Copyright © 2022 SUSE LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package register

import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"

"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/rancher/elemental-operator/pkg/dmidecode"
"github.com/rancher/elemental-operator/pkg/tpm"
"github.com/sirupsen/logrus"
)

func Register(url string, caCert []byte, smbios bool, emulatedTPM bool, emulatedSeed int64, labels map[string]string) ([]byte, error) {

dialer := websocket.DefaultDialer

if len(caCert) > 0 {
pool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
pool.AppendCertsFromPEM(caCert)
dialer.TLSClientConfig = &tls.Config{RootCAs: pool}
}

tpmAuth := &tpm.AuthClient{}
if emulatedTPM {
logrus.Info("Starting TPM emulation")
tpmAuth.EmulateTPM(emulatedSeed)
}
authToken, tpmHash, err := tpmAuth.GetAuthToken()
if err != nil {
return nil, fmt.Errorf("cannot generate authentication token from TPM: %w", err)
}

wsURL := strings.Replace(url, "http", "ws", 1)
logrus.Infof("Using TPMHash %s to dial %s", tpmHash, wsURL)

header := http.Header{}
header.Add("Authorization", authToken)
if smbios {
err = addSMBIOSHeaders(&header)
if err != nil {
return nil, fmt.Errorf("add SMBIOS headers: %w", err)
}
}
if len(labels) > 0 {
err = addLabelsHeaders(&header, labels)
if err != nil {
return nil, fmt.Errorf("add labels in header: %w", err)
}
}

conn, resp, err := dialer.Dial(wsURL, header)
if err != nil {
if resp != nil {
if resp.StatusCode == http.StatusUnauthorized {
data, err := ioutil.ReadAll(resp.Body)
if err == nil {
return nil, errors.New(string(data))
}
} else {
return nil, fmt.Errorf("%w (Status: %s)", err, resp.Status)
}
}
return nil, err
}
defer conn.Close()

err = tpmAuth.Init(conn)
if err != nil {
return nil, fmt.Errorf("failed TPM based authentication: %w", err)
}

_, msg, err := conn.NextReader()
if err != nil {
return nil, fmt.Errorf("reading elemental config: %w", err)
}

return ioutil.ReadAll(msg)
}

func addSMBIOSHeaders(header *http.Header) error {
data, err := dmidecode.Decode()
if err != nil {
return errors.Wrap(err, "failed to read dmidecode data")
}

var buf bytes.Buffer
b64Enc := base64.NewEncoder(base64.StdEncoding, &buf)

if err = json.NewEncoder(b64Enc).Encode(data); err != nil {
return errors.Wrap(err, "failed to encode dmidecode data")
}

_ = b64Enc.Close()

chunk := make([]byte, 875) //the chunk size
part := 1
for {
n, err := buf.Read(chunk)
if err != nil {
if err != io.EOF {
return errors.Wrap(err, "failed to read file in chunks")
}
break
}
header.Set(fmt.Sprintf("X-Cattle-Smbios-%d", part), string(chunk[0:n]))
part++
}
return nil
}

func addLabelsHeaders(header *http.Header, labels map[string]string) error {
var buf bytes.Buffer
b64Enc := base64.NewEncoder(base64.StdEncoding, &buf)

if err := json.NewEncoder(b64Enc).Encode(labels); err != nil {
return errors.Wrap(err, "failed to encode labels")
}

_ = b64Enc.Close()
header.Set("X-Cattle-Labels", buf.String())
return nil
}
106 changes: 49 additions & 57 deletions pkg/tpm/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,76 +17,68 @@ limitations under the License.
package tpm

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/pkg/errors"
"github.com/rancher-sandbox/go-tpm"
"github.com/rancher/elemental-operator/pkg/dmidecode"
"github.com/gorilla/websocket"
gotpm "github.com/rancher-sandbox/go-tpm"
)

func Register(url string, caCert []byte, smbios bool, emulatedTPM bool, emulatedSeed int64, labels map[string]string) ([]byte, error) {
var opts []tpm.Option
type AttestationChannel struct {
conn *websocket.Conn
}

if len(caCert) > 0 {
opts = append(opts, tpm.WithCAs(caCert))
opts = append(opts, tpm.AppendCustomCAToSystemCA)
func (att *AttestationChannel) Read(p []byte) (int, error) {
_, r, err := att.conn.NextReader()
if err != nil {
return 0, err
}
return r.Read(p)
}

header := http.Header{}

if smbios {
data, err := dmidecode.Decode()
if err != nil {
return nil, errors.Wrap(err, "failed to read dmidecode data")
}
func (att *AttestationChannel) Write(p []byte) (int, error) {
w, err := att.conn.NextWriter(websocket.BinaryMessage)
if err != nil {
return 0, err
}
defer w.Close()

var buf bytes.Buffer
b64Enc := base64.NewEncoder(base64.StdEncoding, &buf)
return w.Write(p)
}

if err = json.NewEncoder(b64Enc).Encode(data); err != nil {
return nil, errors.Wrap(err, "failed to encode dmidecode data")
}
type AuthClient struct {
emulateTPM bool
seed int64
ak []byte
}

_ = b64Enc.Close()
func (auth *AuthClient) EmulateTPM(seed int64) {
auth.emulateTPM = true
auth.seed = seed
}

chunk := make([]byte, 875) //the chunk size
part := 1
for {
n, err := buf.Read(chunk)
if err != nil {
if err != io.EOF {
return nil, errors.Wrap(err, "failed to read file in chunks")
}
break
}
header.Set(fmt.Sprintf("X-Cattle-Smbios-%d", part), string(chunk[0:n]))
part++
}
func (auth *AuthClient) GetAuthToken() (string, string, error) {
var opts []gotpm.Option
if auth.emulateTPM {
opts = append(opts, gotpm.Emulated)
opts = append(opts, gotpm.WithSeed(auth.seed))
}

if len(labels) > 0 {
var buf bytes.Buffer
b64Enc := base64.NewEncoder(base64.StdEncoding, &buf)

if err := json.NewEncoder(b64Enc).Encode(labels); err != nil {
return nil, errors.Wrap(err, "failed to encode labels")
}

_ = b64Enc.Close()
header.Set("X-Cattle-Labels", buf.String())
token, akBytes, err := gotpm.GetAuthToken(opts...)
if err != nil {
return "", "", err
}
auth.ak = akBytes

if emulatedTPM {
opts = append(opts, tpm.Emulated)
opts = append(opts, tpm.WithSeed(emulatedSeed))
hash, err := gotpm.GetPubHash(opts...)
if err != nil {
return "", "", err
}
return token, hash, nil
}

opts = append(opts, tpm.WithHeader(header))
func (auth *AuthClient) Init(conn *websocket.Conn) error {
var opts []gotpm.Option
if auth.emulateTPM {
opts = append(opts, gotpm.Emulated)
opts = append(opts, gotpm.WithSeed(auth.seed))
}

return tpm.Get(url, opts...)
return gotpm.Authenticate(auth.ak, &AttestationChannel{conn}, opts...)
}

0 comments on commit 931c6cf

Please sign in to comment.