diff --git a/go.sum b/go.sum index f7afbec..c7a2ead 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ= github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= diff --git a/nitrite.go b/nitrite.go index c5a9983..2f78bed 100644 --- a/nitrite.go +++ b/nitrite.go @@ -8,9 +8,10 @@ import ( "crypto/x509" "errors" "fmt" - "github.com/fxamacker/cbor/v2" "math/big" "time" + + "github.com/fxamacker/cbor/v2" ) // Document represents the AWS Nitro Enclave Attestation Document. @@ -398,3 +399,29 @@ func checkECDSASignature(publicKey *ecdsa.PublicKey, sigStruct, signature []byte return ecdsa.Verify(publicKey, hashSigStruct, r, s) } + +// Timestamp extracts attestation timestamp from `data` without verifying +// the attestation. +func Timestamp(data []byte) (time.Time, error) { + cose := cosePayload{} + err := cbor.Unmarshal(data, &cose) + if nil != err { + return time.Time{}, ErrBadCOSESign1Structure + } + + doc := Document{} + err = cbor.Unmarshal(cose.Payload, &doc) + if nil != err { + return time.Time{}, ErrBadAttestationDocument + } + + if doc.Timestamp == 0 { + return time.Time{}, ErrMandatoryFieldsMissing + } + + // https://docs.aws.amazon.com/pdfs/enclaves/latest/user/enclaves-user.pdf + // (p. 64) describes Timestamp as "UTC time when document was created, + // in milliseconds" + msec := int64(doc.Timestamp) + return time.Unix(msec/1e3, (msec%1e3)*1e6), nil +} diff --git a/nitrite_test.go b/nitrite_test.go new file mode 100644 index 0000000..121af87 --- /dev/null +++ b/nitrite_test.go @@ -0,0 +1,108 @@ +package nitrite_test + +import ( + "errors" + "testing" + "time" + + "github.com/fxamacker/cbor/v2" + "github.com/hf/nitrite" +) + +func requireNoError(t *testing.T, got error) { + if got != nil { + t.Fatalf("unexpected error: %v", got) + } +} + +func requireEqual(t *testing.T, got, want interface{}) { + if got != want { + t.Fatalf("not equal: got %v, want %v", got, want) + } +} + +func requireErrorIs(t *testing.T, got, want error) { + if !errors.Is(got, want) { + t.Fatalf("unexpected error type: got %T, want %T", got, want) + } +} + +type testingCOSEPayload struct { + _ struct{} `cbor:",toarray"` + + Protected []byte + Unprotected cbor.RawMessage + Payload []byte + Signature []byte +} + +func TestAttestationCreatedAt(t *testing.T) { + timeToMillis := func(t time.Time) uint64 { + return uint64(t.UnixNano() / 1e6) + } + + t.Run("happy path", func(t *testing.T) { + // given + wantTime := time.Now() + doc := nitrite.Document{ + Timestamp: timeToMillis(wantTime), + } + docBytes, err := cbor.Marshal(doc) + requireNoError(t, err) + cosePayload := testingCOSEPayload{ + Payload: docBytes, + } + cosePayloadBytes, err := cbor.Marshal(cosePayload) + requireNoError(t, err) + + // when + gotTime, err := nitrite.Timestamp(cosePayloadBytes) + + // then + requireNoError(t, err) + requireEqual(t, timeToMillis(gotTime), timeToMillis(wantTime)) + }) + + t.Run("cannot unmarshal COSE payload", func(t *testing.T) { + // when + _, err := nitrite.Timestamp([]byte("invalid")) + + // then + requireErrorIs(t, err, nitrite.ErrBadCOSESign1Structure) + }) + + t.Run("cannot unmarshal Document", func(t *testing.T) { + // given + cosePayload := testingCOSEPayload{ + Payload: []byte("invalid"), + } + cosePayloadBytes, err := cbor.Marshal(cosePayload) + requireNoError(t, err) + + // when + _, err = nitrite.Timestamp(cosePayloadBytes) + + // then + requireErrorIs(t, err, nitrite.ErrBadAttestationDocument) + }) + + t.Run("attestation document has no timestamp", func(t *testing.T) { + // given + doc := nitrite.Document{ + Timestamp: 0, + } + docBytes, err := cbor.Marshal(doc) + requireNoError(t, err) + cosePayload := testingCOSEPayload{ + Payload: docBytes, + } + cosePayloadBytes, err := cbor.Marshal(cosePayload) + requireNoError(t, err) + + // when + _, err = nitrite.Timestamp(cosePayloadBytes) + + // then + requireErrorIs(t, err, nitrite.ErrMandatoryFieldsMissing) + }) +}