Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 7 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
50 changes: 50 additions & 0 deletions attest/ek_order.go
Original file line number Diff line number Diff line change
@@ -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
}
161 changes: 161 additions & 0 deletions attest/ek_order_test.go
Original file line number Diff line number Diff line change
@@ -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 "<nil>"
}
return c.Subject.CommonName
}
94 changes: 84 additions & 10 deletions attest/pcp_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading