diff --git a/.github/workflows/ci-go-cover.yml b/.github/workflows/ci-go-cover.yml index c534a7b..66f69e8 100644 --- a/.github/workflows/ci-go-cover.yml +++ b/.github/workflows/ci-go-cover.yml @@ -14,20 +14,23 @@ # 1. Change workflow name from "cover 100%" to "cover ≥92.5%". Script will automatically use 92.5%. # 2. Update README.md to use the new path to badge.svg because the path includes the workflow name. -name: cover ≥75% -on: [push] +name: cover ≥80.0% +on: [push, pull_request] jobs: - - # Verify minimum coverage is reached using `go test -short -cover` on latest-ubuntu with default version of Go. - # The grep expression can't be too strict, it needed to be relaxed to work with different versions of Go. cover: name: Coverage runs-on: ubuntu-latest + env: + GO111MODULE: on steps: + - uses: actions/setup-go@v3 + with: + go-version: "1.18" - name: Checkout code uses: actions/checkout@v2 - name: Go Coverage run: | go version - go test -short -cover | grep "^.*coverage:.*of statements$" | python -c "import os,re,sys; cover_rpt = sys.stdin.read(); print(cover_rpt) if len(cover_rpt) != 0 and len(cover_rpt.splitlines()) == 1 else sys.exit(1); min_cover = float(re.findall(r'\d*\.\d+|\d+', os.environ['GITHUB_WORKFLOW'])[0]); cover = float(re.findall(r'\d*\.\d+|\d+', cover_rpt)[0]); sys.exit(1) if (cover > 100) or (cover < min_cover) else sys.exit(0)" + make test-cover | grep -o "coverage:.*of statements$" | python scripts/cov.py + shell: bash diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 7790b6a..e6666b7 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -1,20 +1,21 @@ # Go Linters - GitHub Actions name: linters -on: [push] +on: [push, pull_request] jobs: - - # Check linters on latest-ubuntu with default version of Go. lint: name: Lint runs-on: ubuntu-latest + env: + GO111MODULE: on steps: + - uses: actions/setup-go@v3 + with: + go-version: "1.18" - name: Checkout code uses: actions/checkout@v2 - - name: Install golangci-lint + - name: Install golangci-lint run: | go version - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.23.8 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.2 - name: Run required linters in .golangci.yml plus hard-coded ones here - run: $(go env GOPATH)/bin/golangci-lint run --timeout=3m - - name: Run optional linters (not required to pass) - run: $(go env GOPATH)/bin/golangci-lint run --timeout=3m --issues-exit-code=0 -E dupl -E gocritic -E gosimple -E lll -E prealloc + run: make -w GOLINT=$(go env GOPATH)/bin/golangci-lint lint diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..d92b808 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,87 @@ +# Do not delete linter settings. Linters like gocritic can be enabled on the command line. + +linters-settings: + dupl: + threshold: 100 + funlen: + lines: 100 + statements: 50 + goconst: + min-len: 2 + min-occurrences: 3 + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - style + disabled-checks: + - dupImport # https://github.com/go-critic/go-critic/issues/845 + - ifElseChain + - octalLiteral + - paramTypeCombine + - whyNoLint + - wrapperFunc + gofmt: + simplify: false + goimports: + golint: + min-confidence: 0 + govet: + check-shadowing: true + lll: + line-length: 140 + maligned: + suggest-new: true + misspell: + locale: US + +linters: + disable-all: true + enable: + - deadcode + - errcheck + - goconst + - gocyclo + - gofmt + - goimports + - golint + - gosec + - govet + - ineffassign + - maligned + - misspell + - staticcheck + - structcheck + - typecheck + - unconvert + - unused + - varcheck + + +issues: + # max-issues-per-linter default is 50. Set to 0 to disable limit. + max-issues-per-linter: 0 + # max-same-issues default is 3. Set to 0 to disable limit. + max-same-issues: 0 + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + - path: _test\.go + linters: + - goconst + - dupl + - gomnd + - lll + - path: doc\.go + linters: + - goimports + - gomnd + - lll + - path: pretty_test_vectors.go + linters: + - lll + +# golangci.com configuration +# https://github.com/golangci/golangci/wiki/Configuration +service: + golangci-lint-version: 1.23.x # use the fixed version to not introduce new linters unexpectedly diff --git a/LICENSE b/LICENSE index 261eeb9..98f00dc 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2023 Contributors to the Veraison Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Makefile b/Makefile index 2d40435..4383a87 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,8 @@ export GO111MODULE := on export SHELL := /bin/bash -GOPKG := github.com/veraison/dice/tcg +GOPKG := github.com/veraison/dice/open +GOPKG += github.com/veraison/dice/tcg GOLINT ?= golangci-lint diff --git a/README.md b/README.md index d7f549c..29d4df0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Features -Implementation of the attestation extension defined in [TCG DICE Attestation Architecture](https://trustedcomputinggroup.org/wp-content/uploads/TCG_DICE_Attestation_Architecture_r22_02dec2020.pdf). +- Implementation of the attestation extension defined in [TCG DICE Attestation Architecture](https://trustedcomputinggroup.org/wp-content/uploads/TCG_DICE_Attestation_Architecture_r22_02dec2020.pdf). +- Implementation of TCG DICE TCB Info evidence extension. +- Implementation of [Open + DICE](https://pigweed.googlesource.com/open-dice/+/refs/heads/master/docs/specification.md) certificate (CBOR and X.509) chain validation and claim extraction. # Make targets diff --git a/go.mod b/go.mod index fa03461..9f069a3 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,17 @@ module github.com/veraison/dice -go 1.15 +go 1.18 -require github.com/stretchr/testify v1.6.1 +require ( + github.com/fxamacker/cbor/v2 v2.4.0 + github.com/stretchr/testify v1.8.3 + github.com/veraison/go-cose v1.1.1-0.20230613195103-433d4c233485 + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 56d62e7..fbac6fd 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,18 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= +github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/veraison/go-cose v1.1.1-0.20230613195103-433d4c233485 h1:DrmH7fx8abH1uhkNyUv0pjH8DVkfkD9BcRLbrHr9Buo= +github.com/veraison/go-cose v1.1.1-0.20230613195103-433d4c233485/go.mod h1:XIgugULT1VrP3RsJabABJqSvdMpy40KaAAhWbspGZUU= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/open/open.go b/open/open.go new file mode 100644 index 0000000..6fd36e6 --- /dev/null +++ b/open/open.go @@ -0,0 +1,548 @@ +package open + +import ( + "bytes" + "crypto/x509" + "encoding/asn1" + "encoding/binary" + "errors" + "fmt" + + cbor "github.com/fxamacker/cbor/v2" + cose "github.com/veraison/go-cose" + "golang.org/x/exp/slices" +) + +// X509CdiExtOid encodes the Open-DICE custom x509 extension OID +var X509CdiExtOid = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 1, 24} + +// Mode represents the value of the Mode field inside the Open DICE +// custom extension. See: +// https://pigweed.googlesource.com/open-dice/+/refs/heads/master/docs/specification.md#Mode-Value-Details +type Mode uint8 + +const ( + // OdmNotConfigured indicates that at least one security mechanism has + // not been configured. This mode also acts as a catch-all for + // configurations which do not fit the other modes. Invalid mode values + // -- values not defined here -- should be treated like this mode. + OdmNotConfigured Mode = iota + // OdmNormal indicates the device is operating normally under secure + // configuration. This may mean, for example: Verified boot is enabled, + // verified boot authorities used for development or debug have been + // disabled, debug ports or other debug facilities have been disabled, + // and the device booted software from the normal primary source, for + // example, eMMC, not USB, network, or removable storage. + OdmNormal + // OdmDebug indicates at least one criteria for Normal mode is not met + // and the device is not in a secure state. + OdmDebug + // OdmRecovery indicates a recovery or maintenance mode of some kind. + // This may mean software is being loaded from an alternate source, or + // the device is configured to trigger recovery logic instead of a + // normal boot flow. + OdmRecovery + + OdmInvalid // must be last +) + +// IsValid returns a boolean indicating whether the the mode value is valid. +func (o Mode) IsValid() bool { + return o < OdmInvalid +} + +// Config represents the configurationDescriptor decoded according to +// the convention specified in the Open DICE profile. See: +// https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/specification.md#configuration-input-value-details-optional +type Config struct { + // EnabledVerifiedBootAuthorities is indicates which verified boot + // authorities have been enabled (empty if VerifiedBootEnabled is + // false). + EnabledVerifiedBootAuthorities []int + // Version encodes target software version information. + Version uint16 + // ImplementationSpecific may be used by an implementation for any + // other security-relevant configuration. + ImplementationSpecific [32]byte + // VerifiedBootEnabled indicates whether a verified boot feature is enabled. + VerifiedBootEnabled bool + // DebugPortsEnabled is a bit map indicating which debug ports and + // features have been enabled. + DebugPortsEnabled byte + // BootSource indicates where the target software was loaded from. + BootSource byte +} + +// Entry represents Open DICE-relevant claims extracted from a +// certificate (either CBOR or X.509). +type Entry struct { + // UdsID is an identifier derived from the UDS (or, in case of multiple + // layers, previous layer's CDI) public key. + UdsID []byte `json:"UDS_ID"` + + // CdiID is an identifier derived from the (this layer's) CDI public + // key. + CdiID []byte `json:"CDI_ID"` + + // Fields below correspond to the Open DICE custom extension entries + // described here: + // https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/specification.md#custom-extension-format + + // CodeHash is the exact 64-byte code input value used to compute CDI values. + CodeHash []byte `json:"codeHash"` + // CodeDescriptor contains additional information about CodeHash. + CodeDescriptor []byte `json:"codeDescriptor,omitempty"` + // ConfigurationHash is the exact 64-byte configuration input value + // used to compute CDI values. + ConfigurationHash []byte `json:"configurationHash,omitempty"` + // ConfigurationDescriptor contains the original configuration data, if + // ConfigurationHash is present. Otherwise, it contains the exact + // 64-byte configuration input data used to compute CDI values. + ConfigurationDescriptor []byte `json:"configurationDescriptor"` + // AuthorityHash is the exact 64-byte authority input value used to + // compute CDI values. + AuthorityHash []byte `json:"authorityHash"` + // AuthorityDescriptor contains additional information about the + // authority input value. + AuthorityDescriptor []byte `json:"authorityDescriptor,omitempty"` + // Mode is the mode input value. + Mode Mode `json:"mode"` +} + +// GetConfigDetails parses the Entry's ConfigurationDescriptor into an +// Config entry. +func (o *Entry) GetConfigDetails() (*Config, error) { + if len(o.ConfigurationDescriptor) != 64 { + return nil, fmt.Errorf( + "configurationDescriptor must be exactly 64 bytes (found %d)", + len(o.ConfigurationDescriptor), + ) + } + + var config Config + + config.VerifiedBootEnabled = (o.ConfigurationDescriptor[0] & 0x80) != 0 + // If the MSb of the verified boot byte is set, the remaining bits, in + // big endian order, indicate the authorities that have been enabled + // (i.e. LSb indicates whether the authority 7 has been enabled). See: + // https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/specification.md#configuration-input-value-details-optional + if config.VerifiedBootEnabled { + for i := 1; i < 8; i++ { + if (o.ConfigurationDescriptor[0] & (1 << (7 - i))) != 0 { + config.EnabledVerifiedBootAuthorities = + append(config.EnabledVerifiedBootAuthorities, i) + } + } + } else if o.ConfigurationDescriptor[0] != 0x00 { // If a verified boot system is disabled or not supported, all bits are clear. + return nil, fmt.Errorf( + "VerifiedBootEnabled bit is unset, expecting the remaining verified boot bits to be unset (found 0x%x)", // nolint:golint + o.ConfigurationDescriptor[0], + ) + } + + config.DebugPortsEnabled = o.ConfigurationDescriptor[1] + config.BootSource = o.ConfigurationDescriptor[2] + config.Version = binary.BigEndian.Uint16(o.ConfigurationDescriptor[3:5]) + copy(config.ImplementationSpecific[:], o.ConfigurationDescriptor[32:]) + + return &config, nil +} + +// ExtractChainFromCbor extracts Open DICE claim entries from a +// concatenated chain of CBOR CDI certificates. If verify is true, the +// signatures on the certificates are verified, and chained back to the +// provided UDS certificate. +func ExtractChainFromCbor( + data []byte, + roots []*CborUdsCert, + verify bool, +) ([]*Entry, error) { + var certs []*CborCdiCert + + buf := bytes.NewBuffer(data) + decoder := cbor.NewDecoder(buf) + + i := 0 + for decoder.NumBytesRead() < len(data) { + var cert CborCdiCert + if err := decoder.Decode(&cert); err != nil { + return nil, fmt.Errorf("could not parse cert %d: %w", i, err) + } + + certs = append(certs, &cert) + i++ + } + + // nolint:prealloc + var entries []*Entry + + for _, cert := range certs { + entries = append(entries, cert.GetEntry()) + } + + if verify { + // We're accepting a slice of roots to be consistent with the X509 interface, + // however, for simplicity of the initial implementation, we're expecting to match + // against a single trust anchor. + if len(roots) != 1 { + return nil, fmt.Errorf( + "could not verify: exactly one root cert must be provided", + ) + } + + issuer := roots[0].Subject + verifier, err := roots[0].SubjectPublicKey.Verifier() + if err != nil { + return nil, fmt.Errorf("could get root cert key: %w", err) + } + + for i, cert := range certs { + if err = cert.Cose.Verify(nil, verifier); err != nil { + return nil, fmt.Errorf("could not verify cert %d: %w", i, err) + } + + if issuer != cert.Issuer { + return nil, fmt.Errorf("issuer mismatch for cert %d", i) + } + + issuer = cert.Subject + verifier, err = cert.SubjectPublicKey.Verifier() + if err != nil { + return nil, fmt.Errorf("could get cert %d key: %w", i, err) + } + } + } + + return entries, nil +} + +// CborCdiCertClaims represents the claims extracted from a CBOR UDS certificate. +type CborUdsCertClaims struct { + // Standard CWT fields. See: + // https://www.rfc-editor.org/rfc/rfc8392 + + // Issuer identifies the principal that issued the certificate. The + // value is implementation-dependant. + Issuer string `cbor:"1,keyasint" json:"iss"` + // Subject identifies the principal that is the subject of the + // certificate. This must set to the UDS_ID. + Subject string `cbor:"2,keyasint" json:"sub"` + + // RawSubjectPublicKey is the bstr-encoded COSE_Key containing UDS_Public + RawSubjectPublicKey []byte `cbor:"-4670552,keyasint" json:"subjectPublicKey"` + // KeyUsage bits are set according to X.509 key usage. See: + // https://www.rfc-editor.org/rfc/rfc8392#section-3.1.2 + KeyUsage []byte `cbor:"-4670553,keyasint" json:"keyUsage"` +} + +// CborUdsCert represents an Open DICE UDS certificate. +type CborUdsCert struct { + CborUdsCertClaims + + // SubjectPublicKey is the decoded COSE_Key containing UDS_Public + SubjectPublicKey *cose.Key +} + +// UnmarshalCBOR decodes a CBOR UDS certificate. +func (o *CborUdsCert) UnmarshalCBOR(data []byte) error { + var msg cose.UntaggedSign1Message + + if err := msg.UnmarshalCBOR(data); err != nil { + return err + } + + if err := cbor.Unmarshal(msg.Payload, &o.CborUdsCertClaims); err != nil { + return err + } + + if err := cbor.Unmarshal(o.RawSubjectPublicKey, &o.SubjectPublicKey); err != nil { + return err + } + + return nil +} + +// CborCdiCertClaims represents the claims extracted from a CBOR CDI certificate. +type CborCdiCertClaims struct { + // Standard CWT fields. See: + // https://www.rfc-editor.org/rfc/rfc8392 + Issuer string `cbor:"1,keyasint" json:"iss"` + Subject string `cbor:"2,keyasint" json:"sub"` + ExpirationTime int `cbor:"4,keyasint,omitempty" json:"exp,omitempty"` + NotBefore int `cbor:"5,keyasint,omitempty" json:"nbf,omitempty"` + IssuedAt int `cbor:"6,keyasint,omitempty" json:"iat,omitempty"` + + // Additional, OpenDICE-defined fields. See: + // https://pigweed.googlesource.com/open-dice/+/HEAD/docs/specification.md#profile-design-certificate-details-cbor-cdi-certificates-additional-fields + CodeHash []byte `cbor:"-4670545,keyasint" json:"codeHash"` + CodeDescriptor []byte `cbor:"-4670546,keyasint,omitempty" json:"codeDescriptor,omitempty"` + ConfigurationHash []byte `cbor:"-4670547,keyasint,omitempty" json:"configurationHash,omitempty"` + ConfigurationDescriptor []byte `cbor:"-4670548,keyasint,omitempty" json:"configurationDescriptor,omitempty"` + AuthorityHash []byte `cbor:"-4670549,keyasint" json:"authorityHash"` + AuthorityDescriptor []byte `cbor:"-4670550,keyasint,omitempty" json:"authorityDescriptor,omitempty"` + Mode [1]byte `cbor:"-4670551,keyasint" json:"mode"` + + RawSubjectPublicKey []byte `cbor:"-4670552,keyasint" json:"subjectPublicKey"` + KeyUsage []byte `cbor:"-4670553,keyasint" json:"keyUsage"` +} + +// CborCdiCert rersents a CBOR CDI certificate. +type CborCdiCert struct { + CborCdiCertClaims + // Raw bytes of the certificate + Raw []byte + // SubjectPublicKey is the cose.Key parsed from subjectPublicKey field. + SubjectPublicKey *cose.Key + // Cose is the parsed COSE_Sign1 structure form the certificate. + Cose *cose.UntaggedSign1Message +} + +// UnmarshalCBOR decodes an untagged COSE_Sign1 structure into an +// CborCdiCert. +func (o *CborCdiCert) UnmarshalCBOR(data []byte) error { + var msg cose.UntaggedSign1Message + + if err := msg.UnmarshalCBOR(data); err != nil { + return err + } + + o.Raw = data + o.Cose = &msg + + if err := cbor.Unmarshal(msg.Payload, &o.CborCdiCertClaims); err != nil { + return err + } + + if err := cbor.Unmarshal(o.RawSubjectPublicKey, &o.SubjectPublicKey); err != nil { + return err + } + + return nil +} + +// GetEntry extracts an Entry from the cert. +func (o *CborCdiCert) GetEntry() *Entry { + return &Entry{ + CodeHash: o.CodeHash, + CodeDescriptor: o.CodeDescriptor, + ConfigurationHash: o.ConfigurationHash, + ConfigurationDescriptor: o.ConfigurationDescriptor, + AuthorityHash: o.AuthorityHash, + AuthorityDescriptor: o.AuthorityDescriptor, + Mode: Mode(o.Mode[0]), + } +} + +// ExtractChainFromX509 processes a chain of x509 certificates, +// extracting the Open DICE data from each, retruning a slice of +// *Entry, where the order matches the order of x509 certs in the +// input. If verify is true, each certificate in the chain is also verified by +// chaining it back to the root. The certs in the input are assumed to be in +// order, starting with DICE Layer 0. +// See: +// https://trustedcomputinggroup.org/wp-content/uploads/DICE-Layering-Architecture-r19_pub.pdf +// Input certificates and roots must be either []byte containing +// concatenated DER-encoded certs, or []*x509.Certificate (the types of othe +// two parameters do not need to match). +func ExtractChainFromX509( + data any, + roots any, + verify bool, +) ([]*Entry, error) { + var certs, rootCerts []*x509.Certificate + var err error + + switch t := data.(type) { + case []byte: + certs, err = x509.ParseCertificates(t) + if err != nil { + return nil, fmt.Errorf("could not parse certs: %w", err) + } + case []*x509.Certificate: + certs = t + default: + return nil, fmt.Errorf( + "unexpected data type (%T); must be []byte or []*x509.Certificate", + data, + ) + } + + var verifOpts x509.VerifyOptions + + if verify { + switch t := roots.(type) { + case []byte: + rootCerts, err = x509.ParseCertificates(t) + if err != nil { + return nil, fmt.Errorf("could not parse certs: %w", err) + } + case []*x509.Certificate: + rootCerts = t + default: + return nil, fmt.Errorf( + "unexpected roots type (%T); must be []byte or []*x509.Certificate", + roots, + ) + } + + verifOpts.Roots = x509.NewCertPool() + verifOpts.Intermediates = x509.NewCertPool() + + for _, root := range rootCerts { + verifOpts.Roots.AddCert(root) + } + + } + + // nolint:prealloc + var result []*Entry + var leaf *x509.Certificate + + for i, cert := range certs { + var odCert X509CdiCert + + if err = odCert.PopulateFromX509Cert(cert); err != nil { + return nil, fmt.Errorf( + "cert at index %d does appear to match Open DICE profile: %w", + i, err, + ) + } + + if verify { + if i == (len(certs) - 1) { + leaf = &odCert.Certificate + } else { + verifOpts.Intermediates.AddCert(&odCert.Certificate) + } + } + + result = append(result, odCert.GetEntry()) + + } + + if verify { + if _, err = leaf.Verify(verifOpts); err != nil { + return nil, fmt.Errorf("failed to verify cert: %w", err) + } + } + + return result, nil +} + +// X509CdiExt is the custom X.509 cert extension for CDI. See: +// https://pigweed.googlesource.com/open-dice/+/refs/heads/master/docs/specification.md#custom-extension-format +type X509CdiExt struct { + CodeHash []byte `asn1:"tag:0,explicit"` + CodeDescriptor []byte `asn1:"tag:1,explicit,optional"` + ConfigurationHash []byte `asn1:"tag:2,explicit,optional"` + ConfigurationDescriptor []byte `asn1:"tag:3,explicit"` + AuthorityHash []byte `asn1:"tag:4,explicit,optional"` + AuthorityDescriptor []byte `asn1:"tag:5,explicit,optional"` + Mode asn1.Enumerated `asn1:"tag:6,explicit"` +} + +// X509CdiCert represents the decoded X.509 CID certificate. +type X509CdiCert struct { + x509.Certificate + X509CdiExt +} + +// GetUdsID returns the cert's UDS_ID. +func (o *X509CdiCert) GetUdsID() []byte { + return o.AuthorityKeyId +} + +// GetCdiID returns the cert's CDI_ID. +func (o *X509CdiCert) GetCdiID() []byte { + return o.SubjectKeyId +} + +// Unmarshal decodes the der-encoded X.509 data into the X509CdiCert. +func (o *X509CdiCert) Unmarshal(data []byte) error { + x509Cert, err := x509.ParseCertificate(data) + if err != nil { + return err + } + + return o.PopulateFromX509Cert(x509Cert) +} + +// PopulateFromX509Cert populatess the X509CdiCert from the provided +// x509.Certificate (which must contain the custom CDI extension). +func (o *X509CdiCert) PopulateFromX509Cert(x509Cert *x509.Certificate) error { + if x509Cert.KeyUsage != x509.KeyUsageCertSign { + return fmt.Errorf("unexpected KeyUsage: %v", x509Cert.KeyUsage) + } + + // All must be set to CDI_ID + if x509Cert.Subject.SerialNumber != fmt.Sprintf("%040x", x509Cert.SubjectKeyId) || + x509Cert.Subject.SerialNumber != fmt.Sprintf("%040x", x509Cert.SerialNumber) { + return fmt.Errorf( + "SerialNumber(%040x), Subject SERIALNUMBER(%s), and subjectKeyIdentifer(%040x) do not match", // nolint:golint + x509Cert.SerialNumber, + x509Cert.Subject.SerialNumber, + x509Cert.SubjectKeyId, + ) + } + + // Both must be set to UDS_ID + if x509Cert.Issuer.SerialNumber != fmt.Sprintf("%040x", x509Cert.AuthorityKeyId) { + return fmt.Errorf( + "Issuer SERIALNUMBER(%s), and authorityKeyIdentifer(%040x) do not match", // nolint:golint + x509Cert.Issuer.SerialNumber, + x509Cert.AuthorityKeyId, + ) + } + + if !x509Cert.IsCA { + return fmt.Errorf("cA basic contraint is not set to TRUE") + } + + if x509Cert.MaxPathLen > 0 { + return fmt.Errorf("pathLenConstraint is greater than zero") + } + + isCdiExt := func(id asn1.ObjectIdentifier) bool { + return id.Equal(X509CdiExtOid) + } + cdiExtIndex := slices.IndexFunc(x509Cert.UnhandledCriticalExtensions, isCdiExt) + if cdiExtIndex == -1 { + return errors.New("x509 cert does not contain CDI custom extension") + } + + o.Certificate = *x509Cert + + for _, ext := range o.Certificate.Extensions { + if ext.Id.Equal(X509CdiExtOid) { + rest, err := asn1.Unmarshal(ext.Value, &o.X509CdiExt) + if err != nil { + return fmt.Errorf("CDI ext error: %w", err) + } + if len(rest) != 0 { + return fmt.Errorf("CDI ext error: trailing bytes") + } + + o.Certificate.UnhandledCriticalExtensions = slices.Delete( + o.Certificate.UnhandledCriticalExtensions, + cdiExtIndex, cdiExtIndex+1, + ) + } + } + + return nil +} + +// GetEntry returns an Entry popluated from the X509CdiCert. +func (o *X509CdiCert) GetEntry() *Entry { + return &Entry{ + UdsID: o.GetUdsID(), + CdiID: o.GetCdiID(), + CodeHash: o.CodeHash, + CodeDescriptor: o.CodeDescriptor, + ConfigurationHash: o.ConfigurationHash, + ConfigurationDescriptor: o.ConfigurationDescriptor, + AuthorityHash: o.AuthorityHash, + AuthorityDescriptor: o.AuthorityDescriptor, + Mode: Mode(o.Mode), + } +} diff --git a/open/open_test.go b/open/open_test.go new file mode 100644 index 0000000..6610b85 --- /dev/null +++ b/open/open_test.go @@ -0,0 +1,316 @@ +package open + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "math/big" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_UnmarshalCborCdiCert(t *testing.T) { + data, err := os.ReadFile("test/_CBOR_Ed25519_cert_full_cert_chain_0.cert") + require.Nil(t, err) + + var cert CborCdiCert + err = cert.UnmarshalCBOR(data) + assert.Nil(t, err) +} + +func Test_UnmarshalX509CdiCert(t *testing.T) { + type DeltaFunc func(cert *x509.Certificate) + + type TestVector struct { + Name string + Delta DeltaFunc + ExpectedError string + } + tvs := []TestVector{ + { + Name: "ok", + Delta: func(cert *x509.Certificate) { + // No-op: use the template as is + }, + ExpectedError: "", + }, + { + Name: "no CDI ext", + Delta: func(cert *x509.Certificate) { + cert.ExtraExtensions = []pkix.Extension{} + }, + ExpectedError: "x509 cert does not contain CDI custom extension", + }, + { + Name: "KeyUsage Extra", + Delta: func(cert *x509.Certificate) { + cert.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign + }, + ExpectedError: "unexpected KeyUsage: 96", + }, + { + Name: "KeyUsage None", + Delta: func(cert *x509.Certificate) { + cert.KeyUsage = 0 + }, + ExpectedError: "unexpected KeyUsage: 0", + }, + { + Name: "Subject mismatch", + Delta: func(cert *x509.Certificate) { + cert.SerialNumber = big.NewInt(0xdeadbeef) + }, + ExpectedError: "SerialNumber(00000000000000000000000000000000deadbeef), Subject SERIALNUMBER(229795c1584dfea60b82d349970647263f884ffc), and subjectKeyIdentifer(229795c1584dfea60b82d349970647263f884ffc) do not match", + }, + { + Name: "SubjectKeyId mismatch", + Delta: func(cert *x509.Certificate) { + cert.SubjectKeyId = []byte{0xde, 0xad, 0xbe, 0xef} + }, + ExpectedError: "SerialNumber(229795c1584dfea60b82d349970647263f884ffc), Subject SERIALNUMBER(229795c1584dfea60b82d349970647263f884ffc), and subjectKeyIdentifer(00000000000000000000000000000000deadbeef) do not match", + }, + { + Name: "AuthorityKeyId mismatch", + Delta: func(cert *x509.Certificate) { + cert.AuthorityKeyId = []byte{0xde, 0xad, 0xbe, 0xef} + }, + ExpectedError: "Issuer SERIALNUMBER(7a06eee41b789f4863d86b8778b1a201a6fedd56), and authorityKeyIdentifer(00000000000000000000000000000000deadbeef) do not match", + }, + { + Name: "IsCA false", + Delta: func(cert *x509.Certificate) { + cert.IsCA = false + }, + ExpectedError: "cA basic contraint is not set to TRUE", + }, + { + Name: "MaxPathLen > 0", + Delta: func(cert *x509.Certificate) { + cert.MaxPathLen = 42 + }, + ExpectedError: "pathLenConstraint is greater than zero", + }, + } + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + udsRaw, err := os.ReadFile("test/_X509_Ed25519_uds_cert.cert") + require.Nil(t, err) + udsCert, err := x509.ParseCertificate(udsRaw) + require.Nil(t, err) + + templateRaw, err := os.ReadFile("test/_X509_Ed25519_cert_full_cert_chain_0.cert") + require.Nil(t, err) + templateCert, err := x509.ParseCertificate(templateRaw) + require.Nil(t, err) + + udsCert.PublicKey = pub + // If the parent SubjectKeyId is set, x509.CreateCertificate() will + // ignore the AuthorityKeyId in the template and use it instead. + udsCert.SubjectKeyId = nil + + applyDelta := func(delta DeltaFunc) []byte { + cert := *templateCert + cert.ExtraExtensions = []pkix.Extension{cert.Extensions[4]} // custom CDI ext + delta(&cert) + + bytes, err := x509.CreateCertificate(rand.Reader, &cert, udsCert, pub, priv) // nolint:govet + require.NoError(t, err) + + return bytes + } + + for _, tv := range tvs { + t.Run(tv.Name, func(t *testing.T) { + data := applyDelta(tv.Delta) + + var cert X509CdiCert + err = cert.Unmarshal(data) + if tv.ExpectedError == "" { + assert.Nil(t, err) + } else { + assert.EqualError(t, err, tv.ExpectedError) + } + }) + } +} + +func Test_X509CdiCert_getters(t *testing.T) { + data, err := os.ReadFile("test/_X509_Ed25519_cert_full_cert_chain_0.cert") + require.Nil(t, err) + + x509Cert, err := x509.ParseCertificate(data) + require.Nil(t, err) + + var cert X509CdiCert + err = cert.PopulateFromX509Cert(x509Cert) + require.Nil(t, err) + + assert.Equal(t, x509Cert.SubjectKeyId, cert.GetCdiID()) + assert.Equal(t, x509Cert.AuthorityKeyId, cert.GetUdsID()) +} + +func Test_Config(t *testing.T) { + tvs := []struct { + Name string + Value []byte + Test func(config *Config) + ExpectedError string + }{ + { + Name: "ok", + Value: []byte{ + 0x96, // verified boot + 0x06, // debug ports + 0x10, // boot source + 0x00, 0x01, // version + + // reserved + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + + // implementation specific + 0xde, 0xad, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbe, 0xef, + }, + Test: func(config *Config) { + assert.True(t, config.VerifiedBootEnabled) + assert.EqualValues(t, + []int{3, 5, 6}, + config.EnabledVerifiedBootAuthorities, + ) + assert.EqualValues(t, config.BootSource, 16) + assert.EqualValues(t, config.Version, 1) + assert.EqualValues(t, + []byte{0xde, 0xad}, + config.ImplementationSpecific[:2], + ) + assert.EqualValues(t, + []byte{0xbe, 0xef}, + config.ImplementationSpecific[30:], + ) + }, + ExpectedError: "", + }, + { + Name: "too short", + Value: []byte{0xde, 0xad, 0xbe, 0xef}, + Test: nil, + ExpectedError: "configurationDescriptor must be exactly 64 bytes (found 4)", + }, + { + Name: "too long", + Value: bytes.Repeat([]byte{0xde, 0xad, 0xbe, 0xef}, 20), + Test: nil, + ExpectedError: "configurationDescriptor must be exactly 64 bytes (found 80)", + }, + { + Name: "verified boot disabled", + Value: append([]byte{0x16}, bytes.Repeat([]byte{0x00}, 63)...), + Test: nil, + ExpectedError: "VerifiedBootEnabled bit is unset, expecting the remaining verified boot bits to be unset (found 0x16)", + }, + } + + for _, tv := range tvs { + t.Run(tv.Name, func(t *testing.T) { + entry := Entry{ConfigurationDescriptor: tv.Value} + config, err := entry.GetConfigDetails() + + if tv.ExpectedError == "" { + tv.Test(config) + } else { + assert.EqualError(t, err, tv.ExpectedError) + } + + }) + } +} + +func Test_ExtractChainFromX509(t *testing.T) { + rawRoot, err := os.ReadFile("test/_X509_Ed25519_uds_cert.cert") + require.Nil(t, err) + root, err := x509.ParseCertificate(rawRoot) + require.Nil(t, err) + + var chain []byte + for i := 0; i < 7; i++ { + path := fmt.Sprintf("test/_X509_Ed25519_cert_full_cert_chain_%d.cert", i) + rawCert, err := os.ReadFile(path) // nolint:govet + require.NoError(t, err) + + chain = append(chain, rawCert...) + } + + entries, err := ExtractChainFromX509(chain, []*x509.Certificate{root}, true) + + assert.NoError(t, err) + assert.Len(t, entries, 7) + assert.Equal(t, root.Subject.SerialNumber, fmt.Sprintf("%040x", entries[0].UdsID)) + + _, err = ExtractChainFromX509(chain, nil, true) + assert.EqualError(t, err, "unexpected roots type (); must be []byte or []*x509.Certificate") + _, err = ExtractChainFromX509(chain, nil, false) + assert.NoError(t, err) + + _, err = ExtractChainFromX509(nil, []*x509.Certificate{root}, true) + assert.EqualError(t, err, "unexpected data type (); must be []byte or []*x509.Certificate") + + var emptyRoots []*x509.Certificate + _, err = ExtractChainFromX509(chain, emptyRoots, true) + assert.EqualError(t, err, "failed to verify cert: x509: certificate signed by unknown authority") +} + +func Test_ExtractChainFromCbor(t *testing.T) { + rawRoot, err := os.ReadFile("test/_CBOR_Ed25519_uds_cert.cert") + require.Nil(t, err) + var root CborUdsCert + err = root.UnmarshalCBOR(rawRoot) + require.Nil(t, err) + + var chain []byte + for i := 0; i < 7; i++ { + path := fmt.Sprintf("test/_CBOR_Ed25519_cert_full_cert_chain_%d.cert", i) + rawCert, err := os.ReadFile(path) // nolint:govet + require.NoError(t, err) + + chain = append(chain, rawCert...) + } + + entries, err := ExtractChainFromCbor(chain, []*CborUdsCert{&root}, true) + assert.NoError(t, err) + assert.Len(t, entries, 7) +} + +func Test__SignatureVerify(t *testing.T) { + var cert0, cert1 CborCdiCert + + rawCert, err := os.ReadFile("test/_CBOR_Ed25519_cert_full_cert_chain_0.cert") + require.NoError(t, err) + + err = cert0.UnmarshalCBOR(rawCert) + require.NoError(t, err) + + rawCert, err = os.ReadFile("test/_CBOR_Ed25519_cert_full_cert_chain_1.cert") + require.NoError(t, err) + + err = cert1.UnmarshalCBOR(rawCert) + require.NoError(t, err) + + verifier, err := cert0.SubjectPublicKey.Verifier() + require.NoError(t, err) + + err = cert1.Cose.Verify(nil, verifier) + assert.NoError(t, err) +} diff --git a/open/test/_CBOR_Ed25519_cert_full_cert_chain_0.cert b/open/test/_CBOR_Ed25519_cert_full_cert_chain_0.cert new file mode 100644 index 0000000..c0883c9 Binary files /dev/null and b/open/test/_CBOR_Ed25519_cert_full_cert_chain_0.cert differ diff --git a/open/test/_CBOR_Ed25519_cert_full_cert_chain_1.cert b/open/test/_CBOR_Ed25519_cert_full_cert_chain_1.cert new file mode 100644 index 0000000..4a16295 Binary files /dev/null and b/open/test/_CBOR_Ed25519_cert_full_cert_chain_1.cert differ diff --git a/open/test/_CBOR_Ed25519_cert_full_cert_chain_2.cert b/open/test/_CBOR_Ed25519_cert_full_cert_chain_2.cert new file mode 100644 index 0000000..a1a598c Binary files /dev/null and b/open/test/_CBOR_Ed25519_cert_full_cert_chain_2.cert differ diff --git a/open/test/_CBOR_Ed25519_cert_full_cert_chain_3.cert b/open/test/_CBOR_Ed25519_cert_full_cert_chain_3.cert new file mode 100644 index 0000000..166aff9 Binary files /dev/null and b/open/test/_CBOR_Ed25519_cert_full_cert_chain_3.cert differ diff --git a/open/test/_CBOR_Ed25519_cert_full_cert_chain_4.cert b/open/test/_CBOR_Ed25519_cert_full_cert_chain_4.cert new file mode 100644 index 0000000..a7bfc7d Binary files /dev/null and b/open/test/_CBOR_Ed25519_cert_full_cert_chain_4.cert differ diff --git a/open/test/_CBOR_Ed25519_cert_full_cert_chain_5.cert b/open/test/_CBOR_Ed25519_cert_full_cert_chain_5.cert new file mode 100644 index 0000000..0e67c01 Binary files /dev/null and b/open/test/_CBOR_Ed25519_cert_full_cert_chain_5.cert differ diff --git a/open/test/_CBOR_Ed25519_cert_full_cert_chain_6.cert b/open/test/_CBOR_Ed25519_cert_full_cert_chain_6.cert new file mode 100644 index 0000000..bfe7d0a Binary files /dev/null and b/open/test/_CBOR_Ed25519_cert_full_cert_chain_6.cert differ diff --git a/open/test/_CBOR_Ed25519_uds_cert.cert b/open/test/_CBOR_Ed25519_uds_cert.cert new file mode 100644 index 0000000..df7b3c2 Binary files /dev/null and b/open/test/_CBOR_Ed25519_uds_cert.cert differ diff --git a/open/test/_X509_Ed25519_cert_full_cert_chain_0.cert b/open/test/_X509_Ed25519_cert_full_cert_chain_0.cert new file mode 100644 index 0000000..3c78f23 Binary files /dev/null and b/open/test/_X509_Ed25519_cert_full_cert_chain_0.cert differ diff --git a/open/test/_X509_Ed25519_cert_full_cert_chain_1.cert b/open/test/_X509_Ed25519_cert_full_cert_chain_1.cert new file mode 100644 index 0000000..50a9120 Binary files /dev/null and b/open/test/_X509_Ed25519_cert_full_cert_chain_1.cert differ diff --git a/open/test/_X509_Ed25519_cert_full_cert_chain_2.cert b/open/test/_X509_Ed25519_cert_full_cert_chain_2.cert new file mode 100644 index 0000000..ceb05ed Binary files /dev/null and b/open/test/_X509_Ed25519_cert_full_cert_chain_2.cert differ diff --git a/open/test/_X509_Ed25519_cert_full_cert_chain_3.cert b/open/test/_X509_Ed25519_cert_full_cert_chain_3.cert new file mode 100644 index 0000000..a08e33e Binary files /dev/null and b/open/test/_X509_Ed25519_cert_full_cert_chain_3.cert differ diff --git a/open/test/_X509_Ed25519_cert_full_cert_chain_4.cert b/open/test/_X509_Ed25519_cert_full_cert_chain_4.cert new file mode 100644 index 0000000..75f09b8 Binary files /dev/null and b/open/test/_X509_Ed25519_cert_full_cert_chain_4.cert differ diff --git a/open/test/_X509_Ed25519_cert_full_cert_chain_5.cert b/open/test/_X509_Ed25519_cert_full_cert_chain_5.cert new file mode 100644 index 0000000..2de414a Binary files /dev/null and b/open/test/_X509_Ed25519_cert_full_cert_chain_5.cert differ diff --git a/open/test/_X509_Ed25519_cert_full_cert_chain_6.cert b/open/test/_X509_Ed25519_cert_full_cert_chain_6.cert new file mode 100644 index 0000000..e5d3ca1 Binary files /dev/null and b/open/test/_X509_Ed25519_cert_full_cert_chain_6.cert differ diff --git a/open/test/_X509_Ed25519_uds_cert.cert b/open/test/_X509_Ed25519_uds_cert.cert new file mode 100644 index 0000000..5ccf401 Binary files /dev/null and b/open/test/_X509_Ed25519_uds_cert.cert differ diff --git a/scripts/cov.py b/scripts/cov.py new file mode 100644 index 0000000..0028264 --- /dev/null +++ b/scripts/cov.py @@ -0,0 +1,22 @@ +# Copyright 2022 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 +import os +import re +import sys + +# read output of (potentially many invocations of) "go test -cover -short", +# e.g., "coverage: 65.7% of statements" +cover_report_lines = sys.stdin.read() + +if len(cover_report_lines) == 0: + sys.exit(2) + +# extract min coverage from GITHUB_WORKFLOW, e.g., "60.4%" +min_cover = float(re.findall(r'\d*\.\d+|\d+', os.environ['GITHUB_WORKFLOW'])[0]) + +for l in cover_report_lines.splitlines(): + cover = float(re.findall(r'\d*\.\d+|\d+', l)[0]) + if cover < min_cover: + sys.exit(1) + +sys.exit(0) diff --git a/tcg/tcg.go b/tcg/tcg.go index 2dbaad4..19fb1e2 100644 --- a/tcg/tcg.go +++ b/tcg/tcg.go @@ -15,6 +15,63 @@ import ( // DiceOID is the standard object identifier for the DICE extension var DiceOID = asn1.ObjectIdentifier{2, 23, 133, 5, 4, 1} +// DiceTcbInfoOid encodes the TCBInfo extension OID +var DiceTcbInfoOid = asn1.ObjectIdentifier{2, 23, 133, 5, 4, 2} + +type FwID struct { + // HashAlg is an algorithm identifier for the hash algorithm used to + // produce the Digest value. + HashAlg asn1.ObjectIdentifier + // Digest is a digest of firmware, initialization values or other + // settings of the target TCB. + Digest []byte +} + +type TcbInfo struct { + // Vender is the entity that created the target TCB (e.g., a TCI + // value). + Vendor string `asn1:"tag:0,implicit,optional,utf8"` + // Model is the product name associated with the target TCB. + Model string `asn1:"tag:1,implicit,optional,utf8"` + // Version is the revision string associated with the target TCB. + Version string `asn1:"tag:2,implicit,optional,utf8"` + // Svn is the security version number associated with the target TCB. + Svn int `asn1:"tag:3,implicit,optional"` + // Layer is the DICE layer associated with the target TCB. + Layer int `asn1:"tag:4,implicit,optional"` + // Index enumerates assests or keys within the target TCB and DICE + // layer. + Index int `asn1:"tag:5,implicit,optional"` + // FwIDList is a list of FWID valuees resulting from applying the + // HashAlg function over the target TCB values used to compute TCI and + // CDI values. It is computed by the DICE layer that is the Attesting + // Environment and certificate Issues. + FwIDList []FwID `asn1:"tag:6,implicit,optional,omitempty"` + // Flags enumerates possible TCB states. A TCB MAY operate according to + // combinations of these operational states (in bit order, starting + // with bit 0): notConfigured, notSecure, recover, debug. + Flags asn1.BitString `asn1:"tag:7,implicit,optional"` + // VendorInfo contains vendor-supplied values that encode vendor-, + // model-, or device-specific state. + VendorInfo []byte `asn1:"tag:8,implicit,optional,omitempty"` +} + +func (o TcbInfo) IsNotConfigured() bool { + return o.Flags.At(0) == 1 +} + +func (o TcbInfo) IsNotSecure() bool { + return o.Flags.At(1) == 1 +} + +func (o TcbInfo) IsRecovery() bool { + return o.Flags.At(2) == 1 +} + +func (o TcbInfo) IsDebug() bool { + return o.Flags.At(3) == 1 +} + // This structure is defined in pkix package but is not exported, so // re-definding here. type SubjectPublicKeyInfo struct { diff --git a/tcg/tcg_test.go b/tcg/tcg_test.go index 33bf6ba..f81f92d 100644 --- a/tcg/tcg_test.go +++ b/tcg/tcg_test.go @@ -4,20 +4,73 @@ package tcg import ( - "io/ioutil" + "encoding/asn1" + "os" "testing" "github.com/stretchr/testify/assert" ) func Test_DiceExtension_Unmarshal(t *testing.T) { - assert := assert.New(t) - - data, err := ioutil.ReadFile("test/riot-ext.der") - assert.Nil(err) + data, err := os.ReadFile("test/riot-ext.der") + assert.Nil(t, err) var dice DiceExtension rest, err := dice.UnmarshalDER(data) - assert.Empty(rest) - assert.Nil(err) + assert.Empty(t, rest) + assert.Nil(t, err) +} + +func Test_TCBInfo_flags(t *testing.T) { + var tv TcbInfo + + tv.Flags = asn1.BitString{ + Bytes: []byte{0x80}, + BitLength: 8, + } + + assert.True(t, tv.IsNotConfigured()) + assert.False(t, tv.IsNotSecure()) + assert.False(t, tv.IsRecovery()) + assert.False(t, tv.IsDebug()) + + tv.Flags = asn1.BitString{ + Bytes: []byte{0x40}, + BitLength: 8, + } + + assert.False(t, tv.IsNotConfigured()) + assert.True(t, tv.IsNotSecure()) + assert.False(t, tv.IsRecovery()) + assert.False(t, tv.IsDebug()) + + tv.Flags = asn1.BitString{ + Bytes: []byte{0x20}, + BitLength: 8, + } + + assert.False(t, tv.IsNotConfigured()) + assert.False(t, tv.IsNotSecure()) + assert.True(t, tv.IsRecovery()) + assert.False(t, tv.IsDebug()) + + tv.Flags = asn1.BitString{ + Bytes: []byte{0x10}, + BitLength: 8, + } + + assert.False(t, tv.IsNotConfigured()) + assert.False(t, tv.IsNotSecure()) + assert.False(t, tv.IsRecovery()) + assert.True(t, tv.IsDebug()) + + tv.Flags = asn1.BitString{ + Bytes: []byte{0xf0}, + BitLength: 8, + } + + assert.True(t, tv.IsNotConfigured()) + assert.True(t, tv.IsNotSecure()) + assert.True(t, tv.IsRecovery()) + assert.True(t, tv.IsDebug()) }