diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 76caa79..79475f3 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -10,9 +10,9 @@ jobs: steps: - uses: actions/setup-go@v3 with: - go-version: 1.20.x + go-version: 1.24.x - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: - version: v1.53.3 + version: v1.64.8 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 06e0d73..5cba2f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: test-linux: strategy: matrix: - go-version: [1.20.x] + go-version: [1.24.x] runs-on: ubuntu-latest steps: - name: Install Go @@ -27,7 +27,7 @@ jobs: test-linux-tpm12: strategy: matrix: - go-version: [1.20.x] + go-version: [1.24.x] runs-on: ubuntu-latest steps: - name: Install Go @@ -43,7 +43,7 @@ jobs: test-macos: strategy: matrix: - go-version: [1.20.x] + go-version: [1.24.x] runs-on: macos-latest steps: - name: Install Go @@ -54,15 +54,15 @@ jobs: uses: actions/checkout@v2 # See https://github.com/google/go-tpm-tools#macos-dev - name: Install openssl - run: brew install openssl@1.1 + run: brew install openssl@3.5 - name: Link openssl - run: sudo ln -s $(brew --prefix openssl@1.1)/include/openssl /usr/local/include + run: sudo ln -s $(brew --prefix openssl@3.5)/include/openssl /usr/local/include - name: Test - run: C_INCLUDE_PATH="$(brew --prefix openssl@1.1)/include" LIBRARY_PATH="$(brew --prefix openssl@1.1)/lib" go test ./... + run: C_INCLUDE_PATH="$(brew --prefix openssl@3.5)/include" LIBRARY_PATH="$(brew --prefix openssl@3.5)/lib" go test ./... test-windows: strategy: matrix: - go-version: [1.20.x] + go-version: [1.24.x] runs-on: windows-latest steps: - name: Install Go diff --git a/attest/ek_order.go b/attest/ek_order.go new file mode 100644 index 0000000..7d4697e --- /dev/null +++ b/attest/ek_order.go @@ -0,0 +1,50 @@ +// Copyright 2026 Smallstep Labs, Inc. +// +// 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 attest + +import ( + "crypto" + "crypto/x509" +) + +// providerEKFirst builds the EK list for a TPM whose platform key provider +// activates credentials with one specific endorsement key. On Windows the +// Platform Crypto Provider always activates with its own EK (PCP_EKPUB) and +// offers no way to choose another, so on a TPM with several EKs only that one +// can complete a credential activation challenge. +// +// The provider's key is returned first, paired with the certificate in certs +// whose public key matches it, if there is one. The remaining certificates +// follow in their original order, so callers that report every EK still see +// all of them. Callers that send a single EK to an attestation CA must send +// the first one. +// +// matched reports whether a certificate was paired with the provider's key. +func providerEKFirst(provider crypto.PublicKey, certs []*x509.Certificate) (eks []EK, matched bool) { + pub, canCompare := provider.(interface{ Equal(crypto.PublicKey) bool }) + + first := EK{Public: provider} + rest := make([]EK, 0, len(certs)) + for _, cert := range certs { + if !matched && canCompare && pub.Equal(cert.PublicKey) { + first.Certificate = cert + matched = true + continue + } + rest = append(rest, EK{Certificate: cert, Public: cert.PublicKey}) + } + + return append([]EK{first}, rest...), matched +} diff --git a/attest/ek_order_test.go b/attest/ek_order_test.go new file mode 100644 index 0000000..15eb9bb --- /dev/null +++ b/attest/ek_order_test.go @@ -0,0 +1,161 @@ +// Copyright 2026 Smallstep Labs, Inc. +// +// 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 attest + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "testing" +) + +func TestProviderEKFirst(t *testing.T) { + rsa2048 := mustGenerateRSAKey(t, 2048) + rsa3072 := mustGenerateRSAKey(t, 3072) + p256 := mustGenerateECDSAKey(t, elliptic.P256()) + p384 := mustGenerateECDSAKey(t, elliptic.P384()) + + rsa2048Cert := testEKCert("rsa2048", rsa2048) + rsa3072Cert := testEKCert("rsa3072", rsa3072) + p256Cert := testEKCert("p256", p256) + p384Cert := testEKCert("p384", p384) + + type wantEK struct { + public crypto.PublicKey + cert *x509.Certificate + } + + tests := []struct { + name string + provider crypto.PublicKey + certs []*x509.Certificate + want []wantEK + wantMatched bool + }{ + { + // ST33KTPM2XSPI: certificates for all four EK profiles, listed + // with RSA 3072 first; the provider activates with RSA 2048. + name: "provider key certified but not listed first", + provider: rsa2048, + certs: []*x509.Certificate{rsa3072Cert, rsa2048Cert, p384Cert, p256Cert}, + want: []wantEK{ + {rsa2048, rsa2048Cert}, + {rsa3072, rsa3072Cert}, + {p384, p384Cert}, + {p256, p256Cert}, + }, + wantMatched: true, + }, + { + // Nuvoton NPCT75x: certificates for RSA 3072 and P-384 only; the + // provider's RSA 2048 EK has none. + name: "provider key not certified", + provider: rsa2048, + certs: []*x509.Certificate{rsa3072Cert, p384Cert}, + want: []wantEK{ + {rsa2048, nil}, + {rsa3072, rsa3072Cert}, + {p384, p384Cert}, + }, + wantMatched: false, + }, + { + name: "single certified EK", + provider: rsa2048, + certs: []*x509.Certificate{rsa2048Cert}, + want: []wantEK{ + {rsa2048, rsa2048Cert}, + }, + wantMatched: true, + }, + { + name: "provider key already first", + provider: rsa2048, + certs: []*x509.Certificate{rsa2048Cert, p256Cert}, + want: []wantEK{ + {rsa2048, rsa2048Cert}, + {p256, p256Cert}, + }, + wantMatched: true, + }, + { + name: "no certificates", + provider: rsa2048, + want: []wantEK{ + {rsa2048, nil}, + }, + wantMatched: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, matched := providerEKFirst(tt.provider, tt.certs) + if matched != tt.wantMatched { + t.Errorf("providerEKFirst() matched = %v, want %v", matched, tt.wantMatched) + } + if len(got) != len(tt.want) { + t.Fatalf("providerEKFirst() returned %d EKs, want %d", len(got), len(tt.want)) + } + for i, w := range tt.want { + pub, ok := got[i].Public.(interface{ Equal(crypto.PublicKey) bool }) + if !ok || !pub.Equal(w.public) { + t.Errorf("EK %d: unexpected public key %T", i, got[i].Public) + } + if got[i].Certificate != w.cert { + t.Errorf("EK %d: certificate = %v, want %v", i, certName(got[i].Certificate), certName(w.cert)) + } + if got[i].CertificateURL != "" { + t.Errorf("EK %d: unexpected certificate URL %q", i, got[i].CertificateURL) + } + } + }) + } +} + +func mustGenerateRSAKey(t *testing.T, bits int) *rsa.PublicKey { + t.Helper() + k, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + t.Fatalf("rsa.GenerateKey(%d) failed: %v", bits, err) + } + return &k.PublicKey +} + +func mustGenerateECDSAKey(t *testing.T, c elliptic.Curve) *ecdsa.PublicKey { + t.Helper() + k, err := ecdsa.GenerateKey(c, rand.Reader) + if err != nil { + t.Fatalf("ecdsa.GenerateKey(%s) failed: %v", c.Params().Name, err) + } + return &k.PublicKey +} + +// testEKCert returns a certificate carrying pub. providerEKFirst only looks at +// the public key, so the certificate does not need to be signed. +func testEKCert(name string, pub crypto.PublicKey) *x509.Certificate { + return &x509.Certificate{Subject: pkix.Name{CommonName: name}, PublicKey: pub} +} + +func certName(c *x509.Certificate) string { + if c == nil { + return "" + } + return c.Subject.CommonName +} diff --git a/attest/pcp_windows.go b/attest/pcp_windows.go index 5c0db7e..442e498 100644 --- a/attest/pcp_windows.go +++ b/attest/pcp_windows.go @@ -198,6 +198,89 @@ var ( 0x80280801: "TPM_E_NEEDS_SELFTEST", 0x80280802: "TPM_E_DOING_SELFTEST", 0x80280803: "TPM_E_DEFEND_LOCK_RUNNING", + // TPM 2.0 response codes, as reported by TBS and the PCP provider. + 0x80280081: "TPM_20_E_ASYMMETRIC", + 0x80280082: "TPM_20_E_ATTRIBUTES", + 0x80280083: "TPM_20_E_HASH", + 0x80280084: "TPM_20_E_VALUE", + 0x80280085: "TPM_20_E_HIERARCHY", + 0x80280087: "TPM_20_E_KEY_SIZE", + 0x80280088: "TPM_20_E_MGF", + 0x80280089: "TPM_20_E_MODE", + 0x8028008A: "TPM_20_E_TYPE", + 0x8028008B: "TPM_20_E_HANDLE", + 0x8028008C: "TPM_20_E_KDF", + 0x8028008D: "TPM_20_E_RANGE", + 0x8028008E: "TPM_20_E_AUTH_FAIL", + 0x8028008F: "TPM_20_E_NONCE", + 0x80280090: "TPM_20_E_PP", + 0x80280092: "TPM_20_E_SCHEME", + 0x80280095: "TPM_20_E_SIZE", + 0x80280096: "TPM_20_E_SYMMETRIC", + 0x80280097: "TPM_20_E_TAG", + 0x80280098: "TPM_20_E_SELECTOR", + 0x8028009A: "TPM_20_E_INSUFFICIENT", + 0x8028009B: "TPM_20_E_SIGNATURE", + 0x8028009C: "TPM_20_E_KEY", + 0x8028009D: "TPM_20_E_POLICY_FAIL", + 0x8028009F: "TPM_20_E_INTEGRITY", + 0x802800A0: "TPM_20_E_TICKET", + 0x802800A1: "TPM_20_E_RESERVED_BITS", + 0x802800A2: "TPM_20_E_BAD_AUTH", + 0x802800A3: "TPM_20_E_EXPIRED", + 0x802800A4: "TPM_20_E_POLICY_CC", + 0x802800A5: "TPM_20_E_BINDING", + 0x802800A6: "TPM_20_E_CURVE", + 0x802800A7: "TPM_20_E_ECC_POINT", + 0x80280100: "TPM_20_E_INITIALIZE", + 0x80280101: "TPM_20_E_FAILURE", + 0x80280103: "TPM_20_E_SEQUENCE", + 0x8028010B: "TPM_20_E_PRIVATE", + 0x80280119: "TPM_20_E_HMAC", + 0x80280120: "TPM_20_E_DISABLED", + 0x80280121: "TPM_20_E_EXCLUSIVE", + 0x80280123: "TPM_20_E_ECC_CURVE", + 0x80280124: "TPM_20_E_AUTH_TYPE", + 0x80280125: "TPM_20_E_AUTH_MISSING", + 0x80280126: "TPM_20_E_POLICY", + 0x80280127: "TPM_20_E_PCR", + 0x80280128: "TPM_20_E_PCR_CHANGED", + 0x8028012D: "TPM_20_E_UPGRADE", + 0x8028012E: "TPM_20_E_TOO_MANY_CONTEXTS", + 0x8028012F: "TPM_20_E_AUTH_UNAVAILABLE", + 0x80280130: "TPM_20_E_REBOOT", + 0x80280131: "TPM_20_E_UNBALANCED", + 0x80280142: "TPM_20_E_COMMAND_SIZE", + 0x80280143: "TPM_20_E_COMMAND_CODE", + 0x80280144: "TPM_20_E_AUTHSIZE", + 0x80280145: "TPM_20_E_AUTH_CONTEXT", + 0x80280146: "TPM_20_E_NV_RANGE", + 0x80280147: "TPM_20_E_NV_SIZE", + 0x80280148: "TPM_20_E_NV_LOCKED", + 0x80280149: "TPM_20_E_NV_AUTHORIZATION", + 0x8028014A: "TPM_20_E_NV_UNINITIALIZED", + 0x8028014B: "TPM_20_E_NV_SPACE", + 0x8028014C: "TPM_20_E_NV_DEFINED", + 0x80280150: "TPM_20_E_BAD_CONTEXT", + 0x80280151: "TPM_20_E_CPHASH", + 0x80280152: "TPM_20_E_PARENT", + 0x80280153: "TPM_20_E_NEEDS_TEST", + 0x80280154: "TPM_20_E_NO_RESULT", + 0x80280155: "TPM_20_E_SENSITIVE", + 0x80280901: "TPM_20_E_CONTEXT_GAP", + 0x80280902: "TPM_20_E_OBJECT_MEMORY", + 0x80280903: "TPM_20_E_SESSION_MEMORY", + 0x80280904: "TPM_20_E_MEMORY", + 0x80280905: "TPM_20_E_SESSION_HANDLES", + 0x80280906: "TPM_20_E_OBJECT_HANDLES", + 0x80280907: "TPM_20_E_LOCALITY", + 0x80280908: "TPM_20_E_YIELDED", + 0x80280909: "TPM_20_E_CANCELED", + 0x8028090A: "TPM_20_E_TESTING", + 0x80280920: "TPM_20_E_NV_RATE", + 0x80280921: "TPM_20_E_LOCKOUT", + 0x80280922: "TPM_20_E_RETRY", + 0x80280923: "TPM_20_E_NV_UNAVAILABLE", 0x80284001: "TBS_E_INTERNAL_ERROR", 0x80284002: "TBS_E_BAD_PARAMETER", 0x80284003: "TBS_E_INVALID_OUTPUT_POINTER", @@ -858,16 +941,7 @@ func (h *winPCP) ActivateCredential(hKey uintptr, activationBlob []byte) ([]byte return nil, fmt.Errorf("NCryptSetProperty returned %X (%v) for key activation", r, msg) } - secretBuff := make([]byte, 256) - var size uint32 - r, _, msg = nCryptGetProperty.Call(hKey, uintptr(unsafe.Pointer(&utf16ActivationStr[0])), uintptr(unsafe.Pointer(&secretBuff[0])), uintptr(len(secretBuff)), uintptr(unsafe.Pointer(&size)), 0) - if r != 0 { - if tpmErr := maybeWinErr(r); tpmErr != nil { - msg = tpmErr - } - return nil, fmt.Errorf("NCryptGetProperty returned %X (%v) for key activation", r, msg) - } - return secretBuff[:size], nil + return getNCryptBufferProperty(hKey, "PCP_TPM12_IDACTIVATION") } // openPCP initializes a reference to the Microsoft PCP provider. diff --git a/attest/tpm_windows.go b/attest/tpm_windows.go index 605f80e..fd3eba6 100644 --- a/attest/tpm_windows.go +++ b/attest/tpm_windows.go @@ -165,12 +165,22 @@ func (t *windowsTPM) ekCertificates() ([]EK, error) { return eks, nil } +// eks returns the TPM's endorsement keys. The first one is PCP_EKPUB, the EK +// the Platform Crypto Provider activates credentials with, paired with its +// certificate when the TPM has one (see providerEKFirst). If PCP_EKPUB can't +// be read, the EK certificates are returned in the order the provider lists +// them. func (t *windowsTPM) eks() ([]EK, error) { ekCerts, err := t.pcp.EKCerts() if err != nil { return nil, fmt.Errorf("could not read EKCerts: %v", err) } - if len(ekCerts) > 0 { + + pub, err := t.ekPub() + if err != nil { + if len(ekCerts) == 0 { + return nil, fmt.Errorf("could not read ek public key from tpm: %v", err) + } var eks []EK for _, cert := range ekCerts { eks = append(eks, EK{Certificate: cert, Public: cert.PublicKey}) @@ -178,20 +188,17 @@ func (t *windowsTPM) eks() ([]EK, error) { return eks, nil } - pub, err := t.ekPub() - if err != nil { - return nil, fmt.Errorf("could not read ek public key from tpm: %v", err) - } - ek := EK{Public: pub} - - i, err := t.info() - if err != nil { - return nil, err - } - if i.Manufacturer.String() == manufacturerIntel { - ek.CertificateURL = intelEKURL(pub) + eks, matched := providerEKFirst(pub, ekCerts) + if !matched { + i, err := t.info() + if err != nil { + return nil, err + } + if i.Manufacturer.String() == manufacturerIntel { + eks[0].CertificateURL = intelEKURL(pub) + } } - return []EK{ek}, nil + return eks, nil } func (t *windowsTPM) ekPub() (*rsa.PublicKey, error) {