Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement key synchronization. #32

Merged
merged 99 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
990abad
Implement mechanism for leader designation.
NullHypothesis Aug 4, 2023
718214e
Implement mechanism to register worker enclaves.
NullHypothesis Aug 4, 2023
b19e1f7
Add handlers for key (re-)synchronization.
NullHypothesis Aug 8, 2023
e724427
Improve key sync protocol.
Aug 11, 2023
904468f
Move log message to the correct place.
Aug 11, 2023
f8f6e89
Remove debug code.
Aug 11, 2023
efeeb8b
Improve debug messages.
Aug 11, 2023
0b35752
Improve registration process.
Aug 11, 2023
75c059a
Refactor and test key synchronization.
Aug 15, 2023
2aa6d45
Fix proxy.go
Aug 15, 2023
bc8eb82
Add crude heartbeat mechanism.
Aug 15, 2023
f656393
Revise heartbeat mechanism.
Aug 15, 2023
099e2c9
Polish heartbeat mechanism.
Aug 15, 2023
9f444ca
Make worker initiate re-synchronization.
Aug 16, 2023
c72be54
Refactoring.
Aug 16, 2023
cb6b360
Add scripts for testing sync outside of enclaves.
Aug 16, 2023
060ca35
Update test scripts.
Aug 16, 2023
e9a4fb9
Remove unused constant.
Aug 16, 2023
4c79050
Terminate if enclave keys cannot be installed.
Aug 16, 2023
d306826
Remove annoying error messages.
Aug 16, 2023
be940b9
Address linter error.
Aug 16, 2023
4cd624a
Add log message.
Aug 16, 2023
6303f03
Fix bug.
Aug 16, 2023
269ec4d
Actually fix bug this time.
Aug 16, 2023
3eb556a
Improve test coverage.
Aug 16, 2023
ca9c01c
Improve log message.
Aug 16, 2023
d403d4b
Use goroutines for execution.
Aug 16, 2023
dbd88ea
Rename struct.
Aug 16, 2023
cc0636e
Fix capitalization.
Aug 16, 2023
0591be7
Tidy up dependencies.
Aug 16, 2023
697f033
Merge heartbeat and registration handler.
Aug 16, 2023
56a9745
Use self-signed certificates for tests to pass.
Aug 16, 2023
4a83286
Delete unused code block.
Aug 16, 2023
ee82653
Don't block on forAll.
Aug 16, 2023
165d947
Fix data race.
Aug 17, 2023
359336b
Run the "designate leader" code only once.
Aug 18, 2023
e27377f
Make attestation handler use attester interface.
Aug 18, 2023
33bcf39
More refactoring and shuffling code around.
Aug 18, 2023
d52948e
Remove debug message.
Aug 18, 2023
0557bda
Fix linter warning.
Aug 18, 2023
632f83a
Add work-in-progress documentation.
Aug 18, 2023
25f5427
Elaborate on key synchronization.
Aug 21, 2023
bc27aa4
Elaborate on heartbeat mechanism.
Aug 21, 2023
a6d22c8
Add installation of HTTPS certificate.
Aug 21, 2023
ba7b3e3
Shuffle variables around.
Aug 22, 2023
b3cf4c2
Remove TODO item.
Aug 22, 2023
d67f006
Use AWS metadata service to get worker hostname.
Aug 22, 2023
b0a7b76
Fix conditional address extraction.
Aug 22, 2023
fb34e56
Obtain hostname manually from IMDSv2.
Aug 23, 2023
689ab28
Add a way to determine hostname outside of enclaves.
Aug 23, 2023
3f80620
Simplify getWorker.
Aug 23, 2023
58fb3b5
Log worker's hostname.
Aug 23, 2023
4ce1d08
Fix incorrect function invocation.
Aug 23, 2023
9fa0f61
Remove debug message.
Aug 23, 2023
8b769eb
Make log message more descriptive.
Aug 23, 2023
c888027
Fix unit tests.
Aug 23, 2023
e36b808
Elaborate on image IDs.
Aug 28, 2023
35d6911
Use nitro attester by default.
Aug 28, 2023
0f42e6b
Elaborate on security considerations.
Aug 29, 2023
a1c1559
Test leader designation via self-probing.
Aug 29, 2023
f3f9167
Cleaning up previous commit.
Aug 29, 2023
e609422
Fix deadlock and add log messages.
Aug 29, 2023
36ff042
Re-attempt talking to leader designation endpoint.
Aug 29, 2023
2e0fba3
Raise read limit as 5K wasn't enough.
Aug 30, 2023
62c3dd1
Back off for a second before re-trying.
Aug 30, 2023
72ed9c4
Print differing PCR values.
Aug 30, 2023
308f43e
Start heartbeat loop after key synchronization.
Aug 30, 2023
374812b
Ignore PCR4 when comparing PCR values.
Aug 30, 2023
8990920
Dereference argument to fix bug.
Aug 30, 2023
d9605b0
Pad public key field if unused.
Aug 30, 2023
817cc81
Fix if condition.
Aug 30, 2023
aaf3c72
Transmit encrypted key material separately.
Aug 30, 2023
6e7cc50
Remove unused endpoint.
Aug 31, 2023
f3099fb
Implement Darnell's suggestion for state handlers.
Aug 31, 2023
18afc7a
Update tooling.
Aug 31, 2023
d13fb9f
Add tests for revised state handlers.
Aug 31, 2023
ae0fd4c
Rename helper functions for clarity.
Aug 31, 2023
436ed32
Add unit test.
Aug 31, 2023
277491e
Delete unused context.
Aug 31, 2023
193b2bd
Replace context with stop channel.
Aug 31, 2023
3bad177
Add draft of HTTP API docs.
Aug 31, 2023
a206598
Replace status code 410 with 403.
Sep 1, 2023
2a8f019
Add unit test.
Sep 11, 2023
e5245c9
Clean up and delete unnecessary code.
Sep 11, 2023
f66886c
Add test and clean up code.
Sep 11, 2023
2552574
Improve clarity and remove unnecessary function calls.
Sep 11, 2023
8638ab6
Use sync.Once and change return code after first run.
Sep 11, 2023
fba9cb7
Remove unused function.
Sep 11, 2023
fe82053
Remove comment.
Sep 11, 2023
2091010
Fix comment.
Sep 11, 2023
cd09cee
Use sync.Mutex instead.
Sep 11, 2023
44487e6
Only expose endpoint if necessary.
Sep 11, 2023
138580f
Expose Prometheus metrics for heartbeats.
Sep 11, 2023
1d6f59f
Minor improvements to clarity.
Sep 11, 2023
0fe0922
Remove unnecessary newline.
Sep 11, 2023
f1594eb
Update docs to match protocol.
Sep 11, 2023
eb37930
Limit the # of bytes we're willing to read.
Sep 11, 2023
94a9d73
Register heartbeat metrics with Prometheus.
Sep 25, 2023
a5fd697
Also run 'go vet' and govulncheck.
Sep 25, 2023
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
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ all: lint test $(binary)
.PHONY: lint
lint: $(godeps)
golangci-lint run
go vet ./...
govulncheck ./...

.PHONY: test
test: $(godeps)
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,6 @@ system, take a look at our [research paper](https://arxiv.org/abs/2206.04123).

* [How to use nitriding](doc/usage.md)
* [System architecture](doc/architecture.md)
* [HTTP API](doc/http-api.md)
* [Horizontal scaling](doc/key-synchronization.md)
* [Example application](example/)
57 changes: 14 additions & 43 deletions attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,19 @@ import (
"fmt"

"github.com/hf/nitrite"
"github.com/hf/nsm"
"github.com/hf/nsm/request"
)

const (
nonceLen = 20 // The size of a nonce in bytes.
nonceNumDigits = nonceLen * 2 // The number of hex digits in a nonce.
maxAttDocLen = 5000 // A (reasonable?) upper limit for attestation doc lengths.
hashPrefix = "sha256:"
hashSeparator = ";"
hashPrefix = "sha256:"
hashSeparator = ";"
)

var (
errBadForm = "failed to parse POST form data"
errNoNonce = "could not find nonce in URL query parameters"
errBadNonceFormat = fmt.Sprintf("unexpected nonce format; must be %d-digit hex string", nonceNumDigits)
errFailedAttestation = "failed to obtain attestation document from hypervisor"
errProfilingSet = "attestation disabled because profiling is enabled"
errBadForm = errors.New("failed to parse POST form data")
errNoNonce = errors.New("could not find nonce in URL query parameters")
errBadNonceFormat = fmt.Errorf("unexpected nonce format; must be %d-digit hex string", nonceLen*2)
errFailedAttestation = errors.New("failed to obtain attestation document from hypervisor")
errProfilingSet = errors.New("attestation disabled because profiling is enabled")

// getPCRValues is a variable pointing to a function that returns PCR
// values. Using a variable allows us to easily mock the function in our
Expand Down Expand Up @@ -55,7 +50,7 @@ func (a *AttestationHashes) Serialize() []byte {
// _getPCRValues returns the enclave's platform configuration register (PCR)
// values.
func _getPCRValues() (map[uint][]byte, error) {
rawAttDoc, err := attest(nil, nil, nil)
rawAttDoc, err := newNitroAttester().createAttstn(nil)
if err != nil {
return nil, err
}
Expand All @@ -76,6 +71,12 @@ func arePCRsIdentical(ourPCRs, theirPCRs map[uint][]byte) bool {
}

for pcr, ourValue := range ourPCRs {
// PCR4 contains a hash over the parent's instance ID. Our enclaves run
// on different parent instances; PCR4 will therefore always differ:
// https://docs.aws.amazon.com/enclaves/latest/user/set-up-attestation.html
if pcr == 4 {
continue
}
theirValue, exists := theirPCRs[pcr]
if !exists {
return false
Expand All @@ -86,33 +87,3 @@ func arePCRsIdentical(ourPCRs, theirPCRs map[uint][]byte) bool {
}
return true
}

// attest takes as input a nonce, user-provided data and a public key, and then
// asks the Nitro hypervisor to return a signed attestation document that
// contains all three values.
func attest(nonce, userData, publicKey []byte) ([]byte, error) {
s, err := nsm.OpenDefaultSession()
if err != nil {
return nil, err
}
defer func() {
if err = s.Close(); err != nil {
elog.Printf("Attestation: Failed to close default NSM session: %s", err)
}
}()

res, err := s.Send(&request.Attestation{
Nonce: nonce,
UserData: userData,
PublicKey: publicKey,
})
if err != nil {
return nil, err
}

if res.Attestation == nil || res.Attestation.Document == nil {
return nil, errors.New("NSM device did not return an attestation")
}

return res.Attestation.Document, nil
}
8 changes: 7 additions & 1 deletion attestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ func TestArePCRsIdentical(t *testing.T) {
t.Fatal("Failed to recognize identical PCRs as such.")
}

// PCR4 should be ignored.
pcr1[4], pcr2[4] = []byte("foo"), []byte("bar")
if !arePCRsIdentical(pcr1, pcr2) {
t.Fatal("Failed to recognize identical PCRs as such.")
}

// Add a new PCR value, so our two maps are no longer identical.
pcr1[2] = []byte("barfoo")
if arePCRsIdentical(pcr1, pcr2) {
Expand Down Expand Up @@ -49,7 +55,7 @@ func TestAttestationHashes(t *testing.T) {
rec := httptest.NewRecorder()
buf := bytes.NewBufferString(base64.StdEncoding.EncodeToString(appKeyHash[:]))
req := httptest.NewRequest(http.MethodPost, pathHash, buf)
e.privSrv.Handler.ServeHTTP(rec, req)
e.intSrv.Handler.ServeHTTP(rec, req)

s := e.hashes.Serialize()
expectedLen := sha256.Size*2 + len(hashPrefix)*2 + len(hashSeparator)
Expand Down
197 changes: 197 additions & 0 deletions attester.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package main

import (
"bytes"
"encoding/json"
"errors"

"github.com/hf/nitrite"
"github.com/hf/nsm"
"github.com/hf/nsm/request"
)

var (
errPCRMismatch = errors.New("PCR values differ")
errNonceMismatch = errors.New("nonce is unexpected")
errNoAttstnFromNSM = errors.New("NSM device did not return an attestation")
padding = []byte("dummy")
)

// attester defines functions for the creation and verification of attestation
// documents. Making this an interface helps with testing: It allows us to
// implement a dummy attester that works without the AWS Nitro hypervisor.
type attester interface {
createAttstn(auxInfo) ([]byte, error)
verifyAttstn([]byte, nonce) (auxInfo, error)
}

type auxInfo interface{}

// workerAuxInfo holds the auxilitary information of an attestation document
// requested by clients.
type clientAuxInfo struct {
clientNonce nonce
attestationHashes []byte
}

// workerAuxInfo holds the auxiliary information of the worker's attestation
// document.
type workerAuxInfo struct {
WorkersNonce nonce `json:"workers_nonce"`
LeadersNonce nonce `json:"leaders_nonce"`
PublicKey []byte `json:"public_key"`
}

// leaderAuxInfo holds the auxiliary information of the leader's attestation
// document.
type leaderAuxInfo struct {
WorkersNonce nonce `json:"workers_nonce"`
HashOfEncrypted []byte `json:"hash_of_encrypted"`
}

// dummyAttester helps with local testing. The interface simply turns
// auxiliary information into JSON, and does not do any cryptography.
type dummyAttester struct{}

// newDummyAttester returns a new dummyAttester.
func newDummyAttester() *dummyAttester {
return new(dummyAttester)
}

func (*dummyAttester) createAttstn(aux auxInfo) ([]byte, error) {
return json.Marshal(aux)
}

func (*dummyAttester) verifyAttstn(doc []byte, n nonce) (auxInfo, error) {
var (
w workerAuxInfo
l leaderAuxInfo
)

// First, assume we're dealing with a worker's auxiliary information.
if err := json.Unmarshal(doc, &w); err != nil {
return nil, err
}
if w.PublicKey != nil {
if n.b64() != w.LeadersNonce.b64() {
return nil, errNonceMismatch
}
return &w, nil
}

// Next, let's assume it's a leader.
if err := json.Unmarshal(doc, &l); err != nil {
return nil, err
}
if l.HashOfEncrypted != nil {
if n.b64() != l.WorkersNonce.b64() {
return nil, errNonceMismatch
}
return &l, nil
}

return nil, errors.New("invalid auxiliary information")
}

// nitroAttester implements the attester interface by drawing on the AWS Nitro
// Enclave hypervisor.
type nitroAttester struct{}

// newNitroAttester returns a new nitroAttester.
func newNitroAttester() *nitroAttester {
return new(nitroAttester)
}

// createAttstn asks the AWS Nitro Enclave hypervisor for an attestation
// document that contains the given auxiliary information.
func (*nitroAttester) createAttstn(aux auxInfo) ([]byte, error) {
var nonce, userData, publicKey []byte

// Prepare our auxiliary information. If the public key field is unused, we
// pad it with dummy bytes because the nitrite package (which we use to
// verify attestation documents) expects all three fields to be set.
switch v := aux.(type) {
case *workerAuxInfo:
nonce = v.LeadersNonce[:]
userData = v.WorkersNonce[:]
publicKey = v.PublicKey
case *leaderAuxInfo:
nonce = v.WorkersNonce[:]
userData = v.HashOfEncrypted
publicKey = padding
case *clientAuxInfo:
nonce = v.clientNonce[:]
userData = v.attestationHashes
publicKey = padding
}

s, err := nsm.OpenDefaultSession()
if err != nil {
return nil, err
}
defer s.Close()

res, err := s.Send(&request.Attestation{
Nonce: nonce,
UserData: userData,
PublicKey: publicKey,
})
if err != nil {
return nil, err
}
if res.Attestation == nil || res.Attestation.Document == nil {
return nil, errNoAttstnFromNSM
}

return res.Attestation.Document, nil
}

// verifyAttstn verifies the given attestation document and, if successful,
// returns the document's auxiliary information.
func (*nitroAttester) verifyAttstn(doc []byte, ourNonce nonce) (auxInfo, error) {
// First, verify the remote enclave's attestation document.
opts := nitrite.VerifyOptions{CurrentTime: currentTime()}
their, err := nitrite.Verify(doc, opts)
if err != nil {
return nil, err
}

// Verify that the remote enclave's PCR values (e.g., the image ID) are
// identical to ours.
ourPCRs, err := getPCRValues()
if err != nil {
return nil, err
}
if !arePCRsIdentical(ourPCRs, their.Document.PCRs) {
return nil, errPCRMismatch
}

// Verify that the remote enclave's attestation document contains the nonce
// that we asked it to embed.
theirNonce, err := sliceToNonce(their.Document.Nonce)
if err != nil {
return nil, err
}
if ourNonce != theirNonce {
return nil, errNonceMismatch
}

// If the "public key" field contains padding, we know that we're
// dealing with a leader's auxiliary information.
if bytes.Equal(their.Document.PublicKey, padding) {
return &leaderAuxInfo{
WorkersNonce: theirNonce,
HashOfEncrypted: their.Document.UserData,
}, nil
}

workersNonce, err := sliceToNonce(their.Document.UserData)
if err != nil {
return nil, err
}
return &workerAuxInfo{
WorkersNonce: workersNonce,
LeadersNonce: theirNonce,
PublicKey: their.Document.PublicKey,
}, nil
}
46 changes: 46 additions & 0 deletions attester_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"bytes"
"errors"
"testing"

"github.com/hf/nitrite"
)

func TestDummyAttestation(t *testing.T) {
var (
d = newDummyAttester()
workersNonce = nonce{1, 2, 3}
hashOfEncrypted = []byte("this is a hash")
)

attstn, err := d.createAttstn(&leaderAuxInfo{
WorkersNonce: workersNonce,
HashOfEncrypted: hashOfEncrypted,
})
failOnErr(t, err)

aux, err := d.verifyAttstn(attstn, workersNonce)
failOnErr(t, err)

leaderAux := aux.(*leaderAuxInfo)
if leaderAux.WorkersNonce != workersNonce {
t.Fatal("Extracted unexpected workers nonce.")
}
if !bytes.Equal(leaderAux.HashOfEncrypted, hashOfEncrypted) {
t.Fatalf("Extracted unexpected hash over encrypted keys.")
}
}

func TestVerifyNitroAttstn(t *testing.T) {
var n = newNitroAttester()
_, err := n.verifyAttstn([]byte("foobar"), nonce{})
assertEqual(t, errors.Is(err, nitrite.ErrBadCOSESign1Structure), true)
}

func TestCreateNitroAttstn(t *testing.T) {
var n = newNitroAttester()
_, err := n.createAttstn(nil)
assertEqual(t, err != nil, true)
}
4 changes: 0 additions & 4 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ import (
"time"
)

const (
defaultItemExpiry = time.Minute
)

// cache implements a simple cache whose items expire.
type cache struct {
sync.RWMutex
Expand Down
Loading