forked from TrueCloudLab/certificates
Merge pull request #1063 from smallstep/herman/acme-da-tpm
Add ACME DA TPM attestation
This commit is contained in:
commit
64e39cb0c9
8 changed files with 2312 additions and 43 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -23,4 +23,5 @@ jobs:
|
|||
os-dependencies: "libpcsclite-dev"
|
||||
run-gitleaks: true
|
||||
run-codeql: true
|
||||
make-test: true # run `make test` instead of the default test workflow
|
||||
secrets: inherit
|
||||
|
|
14
Makefile
14
Makefile
|
@ -90,13 +90,21 @@ generate:
|
|||
#########################################
|
||||
# Test
|
||||
#########################################
|
||||
test:
|
||||
$Q $(GOFLAGS) gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./...
|
||||
test: testdefault testtpmsimulator combinecoverage
|
||||
|
||||
testdefault:
|
||||
$Q $(GOFLAGS) gotestsum -- -coverprofile=defaultcoverage.out -short -covermode=atomic ./...
|
||||
|
||||
testtpmsimulator:
|
||||
$Q CGO_ENALBED=1 gotestsum -- -coverprofile=tpmsimulatorcoverage.out -short -covermode=atomic -tags tpmsimulator ./acme
|
||||
|
||||
testcgo:
|
||||
$Q gotestsum -- -coverprofile=coverage.out -short -covermode=atomic ./...
|
||||
|
||||
.PHONY: test testcgo
|
||||
combinecoverage:
|
||||
cat defaultcoverage.out tpmsimulatorcoverage.out > coverage.out
|
||||
|
||||
.PHONY: test testdefault testtpmsimulator testcgo combinecoverage
|
||||
|
||||
integrate: integration
|
||||
|
||||
|
|
|
@ -26,9 +26,14 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/google/go-attestation/attest"
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"go.step.sm/crypto/x509util"
|
||||
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
@ -437,6 +442,38 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
|
|||
return storeError(ctx, db, ch, true, NewError(ErrorBadAttestationStatementType, "permanent identifier does not match").AddSubproblems(subproblem))
|
||||
}
|
||||
|
||||
// Update attestation key fingerprint to compare against the CSR
|
||||
az.Fingerprint = data.Fingerprint
|
||||
|
||||
case "tpm":
|
||||
data, err := doTPMAttestationFormat(ctx, prov, ch, jwk, &att)
|
||||
if err != nil {
|
||||
// TODO(hs): we should provide more details in the error reported to the client;
|
||||
// "Attestation statement cannot be verified" is VERY generic. Also holds true for the other formats.
|
||||
var acmeError *Error
|
||||
if errors.As(err, &acmeError) {
|
||||
if acmeError.Status == 500 {
|
||||
return acmeError
|
||||
}
|
||||
return storeError(ctx, db, ch, true, acmeError)
|
||||
}
|
||||
return WrapErrorISE(err, "error validating attestation")
|
||||
}
|
||||
|
||||
// TODO(hs): currently this will allow a request for which no PermanentIdentifiers have been
|
||||
// extracted from the AK certificate. This is currently the case for AK certs from the CLI, as we
|
||||
// haven't implemented a way for AK certs requested by the CLI to always contain the requested
|
||||
// PermanentIdentifier. Omitting the check below doesn't allow just any request, as the Order can
|
||||
// still fail if the challenge value isn't equal to the CSR subject.
|
||||
if len(data.PermanentIdentifiers) > 0 && !slices.Contains(data.PermanentIdentifiers, ch.Value) { // TODO(hs): add support for HardwareModuleName
|
||||
subproblem := NewSubproblemWithIdentifier(
|
||||
ErrorMalformedType,
|
||||
Identifier{Type: "permanent-identifier", Value: ch.Value},
|
||||
"challenge identifier %q doesn't match any of the attested hardware identifiers %q", ch.Value, data.PermanentIdentifiers,
|
||||
)
|
||||
return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, "permanent identifier does not match").AddSubproblems(subproblem))
|
||||
}
|
||||
|
||||
// Update attestation key fingerprint to compare against the CSR
|
||||
az.Fingerprint = data.Fingerprint
|
||||
default:
|
||||
|
@ -463,6 +500,301 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
|
|||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
oidSubjectAlternativeName = asn1.ObjectIdentifier{2, 5, 29, 17}
|
||||
)
|
||||
|
||||
type tpmAttestationData struct {
|
||||
Certificate *x509.Certificate
|
||||
VerifiedChains [][]*x509.Certificate
|
||||
PermanentIdentifiers []string
|
||||
Fingerprint string
|
||||
}
|
||||
|
||||
// coseAlgorithmIdentifier models a COSEAlgorithmIdentifier.
|
||||
// Also see https://www.w3.org/TR/webauthn-2/#sctn-alg-identifier.
|
||||
type coseAlgorithmIdentifier int32
|
||||
|
||||
const (
|
||||
coseAlgES256 coseAlgorithmIdentifier = -7
|
||||
coseAlgRS256 coseAlgorithmIdentifier = -257
|
||||
)
|
||||
|
||||
func doTPMAttestationFormat(ctx context.Context, prov Provisioner, ch *Challenge, jwk *jose.JSONWebKey, att *attestationObject) (*tpmAttestationData, error) {
|
||||
ver, ok := att.AttStatement["ver"].(string)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "ver not present")
|
||||
}
|
||||
if ver != "2.0" {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "version %q is not supported", ver)
|
||||
}
|
||||
|
||||
x5c, ok := att.AttStatement["x5c"].([]interface{})
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "x5c not present")
|
||||
}
|
||||
if len(x5c) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "x5c is empty")
|
||||
}
|
||||
|
||||
akCertBytes, ok := x5c[0].([]byte)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed")
|
||||
}
|
||||
akCert, err := x509.ParseCertificate(akCertBytes)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed")
|
||||
}
|
||||
|
||||
intermediates := x509.NewCertPool()
|
||||
for _, v := range x5c[1:] {
|
||||
intCertBytes, vok := v.([]byte)
|
||||
if !vok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "x5c is malformed")
|
||||
}
|
||||
intCert, err := x509.ParseCertificate(intCertBytes)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is malformed")
|
||||
}
|
||||
intermediates.AddCert(intCert)
|
||||
}
|
||||
|
||||
// TODO(hs): this can be removed when permanent-identifier/hardware-module-name are handled correctly in
|
||||
// the stdlib in https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/crypto/x509/parser.go;drc=b5b2cf519fe332891c165077f3723ee74932a647;l=362,
|
||||
// but I doubt that will happen.
|
||||
if len(akCert.UnhandledCriticalExtensions) > 0 {
|
||||
unhandledCriticalExtensions := akCert.UnhandledCriticalExtensions[:0]
|
||||
for _, extOID := range akCert.UnhandledCriticalExtensions {
|
||||
if !extOID.Equal(oidSubjectAlternativeName) {
|
||||
// critical extensions other than the Subject Alternative Name remain unhandled
|
||||
unhandledCriticalExtensions = append(unhandledCriticalExtensions, extOID)
|
||||
}
|
||||
}
|
||||
akCert.UnhandledCriticalExtensions = unhandledCriticalExtensions
|
||||
}
|
||||
|
||||
roots, ok := prov.GetAttestationRoots()
|
||||
if !ok {
|
||||
return nil, NewErrorISE("no root CA bundle available to verify the attestation certificate")
|
||||
}
|
||||
|
||||
// verify that the AK certificate was signed by a trusted root,
|
||||
// chained to by the intermediates provided by the client. As part
|
||||
// of building the verified certificate chain, the signature over the
|
||||
// AK certificate is checked to be a valid signature of one of the
|
||||
// provided intermediates. Signatures over the intermediates are in
|
||||
// turn also verified to be valid signatures from one of the trusted
|
||||
// roots.
|
||||
verifiedChains, err := akCert.Verify(x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
Intermediates: intermediates,
|
||||
CurrentTime: time.Now().Truncate(time.Second),
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "x5c is not valid")
|
||||
}
|
||||
|
||||
// validate additional AK certificate requirements
|
||||
if err := validateAKCertificate(akCert); err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "AK certificate is not valid")
|
||||
}
|
||||
|
||||
// TODO(hs): implement revocation check; Verify() doesn't perform CRL check nor OCSP lookup.
|
||||
|
||||
sans, err := x509util.ParseSubjectAlternativeNames(akCert)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed parsing AK certificate Subject Alternative Names")
|
||||
}
|
||||
|
||||
permanentIdentifiers := make([]string, len(sans.PermanentIdentifiers))
|
||||
for i, pi := range sans.PermanentIdentifiers {
|
||||
permanentIdentifiers[i] = pi.Identifier
|
||||
}
|
||||
|
||||
// extract and validate pubArea, sig, certInfo and alg properties from the request body
|
||||
pubArea, ok := att.AttStatement["pubArea"].([]byte)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid pubArea in attestation statement")
|
||||
}
|
||||
if len(pubArea) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "pubArea is empty")
|
||||
}
|
||||
|
||||
sig, ok := att.AttStatement["sig"].([]byte)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid sig in attestation statement")
|
||||
}
|
||||
if len(sig) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "sig is empty")
|
||||
}
|
||||
|
||||
certInfo, ok := att.AttStatement["certInfo"].([]byte)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid certInfo in attestation statement")
|
||||
}
|
||||
if len(certInfo) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "certInfo is empty")
|
||||
}
|
||||
|
||||
alg, ok := att.AttStatement["alg"].(int64)
|
||||
if !ok {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid alg in attestation statement")
|
||||
}
|
||||
|
||||
// only RS256 and ES256 are allowed
|
||||
coseAlg := coseAlgorithmIdentifier(alg)
|
||||
if coseAlg != coseAlgRS256 && coseAlg != coseAlgES256 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "invalid alg %d in attestation statement", alg)
|
||||
}
|
||||
|
||||
// set the hash algorithm to use to SHA256
|
||||
hash := crypto.SHA256
|
||||
|
||||
// recreate the generated key certification parameter values and verify
|
||||
// the attested key using the public key of the AK.
|
||||
certificationParameters := &attest.CertificationParameters{
|
||||
Public: pubArea, // the public key that was attested
|
||||
CreateAttestation: certInfo, // the attested properties of the key
|
||||
CreateSignature: sig, // signature over the attested properties
|
||||
}
|
||||
verifyOpts := attest.VerifyOpts{
|
||||
Public: akCert.PublicKey, // public key of the AK that attested the key
|
||||
Hash: hash,
|
||||
}
|
||||
if err = certificationParameters.Verify(verifyOpts); err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "invalid certification parameters")
|
||||
}
|
||||
|
||||
// decode the "certInfo" data. This won't fail, as it's also done as part of Verify().
|
||||
tpmCertInfo, err := tpm2.DecodeAttestationData(certInfo)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed decoding attestation data")
|
||||
}
|
||||
|
||||
keyAuth, err := KeyAuthorization(ch.Token, jwk)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed creating key auth digest")
|
||||
}
|
||||
hashedKeyAuth := sha256.Sum256([]byte(keyAuth))
|
||||
|
||||
// verify the WebAuthn object contains the expect key authorization digest, which is carried
|
||||
// within the encoded `certInfo` property of the attestation statement.
|
||||
if subtle.ConstantTimeCompare(hashedKeyAuth[:], []byte(tpmCertInfo.ExtraData)) == 0 {
|
||||
return nil, NewError(ErrorBadAttestationStatementType, "key authorization does not match")
|
||||
}
|
||||
|
||||
// decode the (attested) public key and determine its fingerprint. This won't fail, as it's also done as part of Verify().
|
||||
pub, err := tpm2.DecodePublic(pubArea)
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed decoding pubArea")
|
||||
}
|
||||
|
||||
publicKey, err := pub.Key()
|
||||
if err != nil {
|
||||
return nil, WrapError(ErrorBadAttestationStatementType, err, "failed getting public key")
|
||||
}
|
||||
|
||||
data := &tpmAttestationData{
|
||||
Certificate: akCert,
|
||||
VerifiedChains: verifiedChains,
|
||||
PermanentIdentifiers: permanentIdentifiers,
|
||||
}
|
||||
|
||||
if data.Fingerprint, err = keyutil.Fingerprint(publicKey); err != nil {
|
||||
return nil, WrapErrorISE(err, "error calculating key fingerprint")
|
||||
}
|
||||
|
||||
// TODO(hs): pass more attestation data, so that that can be used/recorded too?
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var (
|
||||
oidExtensionExtendedKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37}
|
||||
oidTCGKpAIKCertificate = asn1.ObjectIdentifier{2, 23, 133, 8, 3}
|
||||
)
|
||||
|
||||
// validateAKCertifiate validates the X.509 AK certificate to be
|
||||
// in accordance with the required properties. The requirements come from:
|
||||
// https://www.w3.org/TR/webauthn-2/#sctn-tpm-cert-requirements.
|
||||
//
|
||||
// - Version MUST be set to 3.
|
||||
// - Subject field MUST be set to empty.
|
||||
// - The Subject Alternative Name extension MUST be set as defined
|
||||
// in [TPMv2-EK-Profile] section 3.2.9.
|
||||
// - The Extended Key Usage extension MUST contain the OID 2.23.133.8.3
|
||||
// ("joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)").
|
||||
// - The Basic Constraints extension MUST have the CA component set to false.
|
||||
// - An Authority Information Access (AIA) extension with entry id-ad-ocsp
|
||||
// and a CRL Distribution Point extension [RFC5280] are both OPTIONAL as
|
||||
// the status of many attestation certificates is available through metadata
|
||||
// services. See, for example, the FIDO Metadata Service.
|
||||
func validateAKCertificate(c *x509.Certificate) error {
|
||||
if c.Version != 3 {
|
||||
return fmt.Errorf("AK certificate has invalid version %d; only version 3 is allowed", c.Version)
|
||||
}
|
||||
if c.Subject.String() != "" {
|
||||
return fmt.Errorf("AK certificate subject must be empty; got %q", c.Subject)
|
||||
}
|
||||
if c.IsCA {
|
||||
return errors.New("AK certificate must not be a CA")
|
||||
}
|
||||
if err := validateAKCertificateExtendedKeyUsage(c); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateAKCertificateSubjectAlternativeNames(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAKCertificateSubjectAlternativeNames checks if the AK certificate
|
||||
// has TPM hardware details set.
|
||||
func validateAKCertificateSubjectAlternativeNames(c *x509.Certificate) error {
|
||||
sans, err := x509util.ParseSubjectAlternativeNames(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing AK certificate Subject Alternative Names: %w", err)
|
||||
}
|
||||
|
||||
details := sans.TPMHardwareDetails
|
||||
manufacturer, model, version := details.Manufacturer, details.Model, details.Version
|
||||
|
||||
switch {
|
||||
case manufacturer == "":
|
||||
return errors.New("missing TPM manufacturer")
|
||||
case model == "":
|
||||
return errors.New("missing TPM model")
|
||||
case version == "":
|
||||
return errors.New("missing TPM version")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAKCertificateExtendedKeyUsage checks if the AK certificate
|
||||
// has the "tcg-kp-AIKCertificate" Extended Key Usage set.
|
||||
func validateAKCertificateExtendedKeyUsage(c *x509.Certificate) error {
|
||||
var (
|
||||
valid = false
|
||||
ekus []asn1.ObjectIdentifier
|
||||
)
|
||||
for _, ext := range c.Extensions {
|
||||
if ext.Id.Equal(oidExtensionExtendedKeyUsage) {
|
||||
if _, err := asn1.Unmarshal(ext.Value, &ekus); err != nil || !ekus[0].Equal(oidTCGKpAIKCertificate) {
|
||||
return errors.New("AK certificate is missing Extended Key Usage value tcg-kp-AIKCertificate (2.23.133.8.3)")
|
||||
}
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return errors.New("AK certificate is missing Extended Key Usage extension")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apple Enterprise Attestation Root CA from
|
||||
// https://www.apple.com/certificateauthority/private/
|
||||
const appleEnterpriseAttestationRootCA = `-----BEGIN CERTIFICATE-----
|
||||
|
@ -760,10 +1092,10 @@ func uitoa(val uint) string {
|
|||
var buf [20]byte // big enough for 64bit value base 10
|
||||
i := len(buf) - 1
|
||||
for val >= 10 {
|
||||
q := val / 10
|
||||
buf[i] = byte('0' + val - q*10)
|
||||
v := val / 10
|
||||
buf[i] = byte('0' + val - v*10)
|
||||
i--
|
||||
val = q
|
||||
val = v
|
||||
}
|
||||
// val < 10
|
||||
buf[i] = byte('0' + val)
|
||||
|
|
|
@ -31,15 +31,14 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/minica"
|
||||
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
type mockClient struct {
|
||||
|
@ -4008,3 +4007,291 @@ func Test_deviceAttest01Validate(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
oidTPMManufacturer = asn1.ObjectIdentifier{2, 23, 133, 2, 1}
|
||||
oidTPMModel = asn1.ObjectIdentifier{2, 23, 133, 2, 2}
|
||||
oidTPMVersion = asn1.ObjectIdentifier{2, 23, 133, 2, 3}
|
||||
)
|
||||
|
||||
func generateValidAKCertificate(t *testing.T) *x509.Certificate {
|
||||
t.Helper()
|
||||
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
template := &x509.Certificate{
|
||||
PublicKey: signer.Public(),
|
||||
Version: 3,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
asn1Value := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMModel, "SLB 9670 TPM2.0", oidTPMVersion, "7.55"))
|
||||
sans := []x509util.SubjectAlternativeName{
|
||||
{Type: x509util.DirectoryNameType,
|
||||
ASN1Value: asn1Value},
|
||||
}
|
||||
ext, err := createSubjectAltNameExtension(nil, nil, nil, nil, sans, true)
|
||||
require.NoError(t, err)
|
||||
ext.Set(template)
|
||||
ca, err := minica.New()
|
||||
require.NoError(t, err)
|
||||
cert, err := ca.Sign(template)
|
||||
require.NoError(t, err)
|
||||
|
||||
return cert
|
||||
}
|
||||
|
||||
func Test_validateAKCertificate(t *testing.T) {
|
||||
cert := generateValidAKCertificate(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
c *x509.Certificate
|
||||
expErr error
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
c: cert,
|
||||
expErr: nil,
|
||||
},
|
||||
{
|
||||
name: "fail/version",
|
||||
c: &x509.Certificate{
|
||||
Version: 1,
|
||||
},
|
||||
expErr: errors.New("AK certificate has invalid version 1; only version 3 is allowed"),
|
||||
},
|
||||
{
|
||||
name: "fail/subject",
|
||||
c: &x509.Certificate{
|
||||
Version: 3,
|
||||
Subject: pkix.Name{CommonName: "fail!"},
|
||||
},
|
||||
expErr: errors.New(`AK certificate subject must be empty; got "CN=fail!"`),
|
||||
},
|
||||
{
|
||||
name: "fail/isCA",
|
||||
c: &x509.Certificate{
|
||||
Version: 3,
|
||||
IsCA: true,
|
||||
},
|
||||
expErr: errors.New("AK certificate must not be a CA"),
|
||||
},
|
||||
{
|
||||
name: "fail/extendedKeyUsage",
|
||||
c: &x509.Certificate{
|
||||
Version: 3,
|
||||
},
|
||||
expErr: errors.New("AK certificate is missing Extended Key Usage extension"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateAKCertificate(tt.c)
|
||||
if tt.expErr != nil {
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualError(t, err, tt.expErr.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validateAKCertificateSubjectAlternativeNames(t *testing.T) {
|
||||
ok := generateValidAKCertificate(t)
|
||||
t.Helper()
|
||||
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
getBase := func() *x509.Certificate {
|
||||
return &x509.Certificate{
|
||||
PublicKey: signer.Public(),
|
||||
Version: 3,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
}
|
||||
|
||||
ca, err := minica.New()
|
||||
require.NoError(t, err)
|
||||
missingManufacturerASN1 := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMModel, "SLB 9670 TPM2.0", oidTPMVersion, "7.55"))
|
||||
sans := []x509util.SubjectAlternativeName{
|
||||
{Type: x509util.DirectoryNameType,
|
||||
ASN1Value: missingManufacturerASN1},
|
||||
}
|
||||
ext, err := createSubjectAltNameExtension(nil, nil, nil, nil, sans, true)
|
||||
require.NoError(t, err)
|
||||
missingManufacturer := getBase()
|
||||
ext.Set(missingManufacturer)
|
||||
|
||||
missingManufacturer, err = ca.Sign(missingManufacturer)
|
||||
require.NoError(t, err)
|
||||
|
||||
missingModelASN1 := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMVersion, "7.55"))
|
||||
sans = []x509util.SubjectAlternativeName{
|
||||
{Type: x509util.DirectoryNameType,
|
||||
ASN1Value: missingModelASN1},
|
||||
}
|
||||
ext, err = createSubjectAltNameExtension(nil, nil, nil, nil, sans, true)
|
||||
require.NoError(t, err)
|
||||
missingModel := getBase()
|
||||
ext.Set(missingModel)
|
||||
|
||||
missingModel, err = ca.Sign(missingModel)
|
||||
require.NoError(t, err)
|
||||
|
||||
missingFirmwareVersionASN1 := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMModel, "SLB 9670 TPM2.0"))
|
||||
sans = []x509util.SubjectAlternativeName{
|
||||
{Type: x509util.DirectoryNameType,
|
||||
ASN1Value: missingFirmwareVersionASN1},
|
||||
}
|
||||
ext, err = createSubjectAltNameExtension(nil, nil, nil, nil, sans, true)
|
||||
require.NoError(t, err)
|
||||
missingFirmwareVersion := getBase()
|
||||
ext.Set(missingFirmwareVersion)
|
||||
|
||||
missingFirmwareVersion, err = ca.Sign(missingFirmwareVersion)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
c *x509.Certificate
|
||||
expErr error
|
||||
}{
|
||||
{"ok", ok, nil},
|
||||
{"fail/missing-manufacturer", missingManufacturer, errors.New("missing TPM manufacturer")},
|
||||
{"fail/missing-model", missingModel, errors.New("missing TPM model")},
|
||||
{"fail/missing-firmware-version", missingFirmwareVersion, errors.New("missing TPM version")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateAKCertificateSubjectAlternativeNames(tt.c)
|
||||
if tt.expErr != nil {
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualError(t, err, tt.expErr.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validateAKCertificateExtendedKeyUsage(t *testing.T) {
|
||||
ok := generateValidAKCertificate(t)
|
||||
missingEKU := &x509.Certificate{}
|
||||
t.Helper()
|
||||
signer, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
template := &x509.Certificate{
|
||||
PublicKey: signer.Public(),
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
}
|
||||
ca, err := minica.New()
|
||||
require.NoError(t, err)
|
||||
wrongEKU, err := ca.Sign(template)
|
||||
require.NoError(t, err)
|
||||
tests := []struct {
|
||||
name string
|
||||
c *x509.Certificate
|
||||
expErr error
|
||||
}{
|
||||
{"ok", ok, nil},
|
||||
{"fail/wrong-eku", wrongEKU, errors.New("AK certificate is missing Extended Key Usage value tcg-kp-AIKCertificate (2.23.133.8.3)")},
|
||||
{"fail/missing-eku", missingEKU, errors.New("AK certificate is missing Extended Key Usage extension")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateAKCertificateExtendedKeyUsage(tt.c)
|
||||
if tt.expErr != nil {
|
||||
if assert.Error(t, err) {
|
||||
assert.EqualError(t, err, tt.expErr.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// createSubjectAltNameExtension will construct an Extension containing all
|
||||
// SubjectAlternativeNames held in a Certificate. It implements more types than
|
||||
// the golang x509 library, so it is used whenever OtherName or RegisteredID
|
||||
// type SANs are present in the certificate.
|
||||
//
|
||||
// See also https://datatracker.ietf.org/doc/html/rfc5280.html#section-4.2.1.6
|
||||
//
|
||||
// TODO(hs): this was copied from go.step.sm/crypto/x509util to make it easier
|
||||
// to create the SAN extension for testing purposes. Should it be exposed instead?
|
||||
func createSubjectAltNameExtension(dnsNames, emailAddresses x509util.MultiString, ipAddresses x509util.MultiIP, uris x509util.MultiURL, sans []x509util.SubjectAlternativeName, subjectIsEmpty bool) (x509util.Extension, error) {
|
||||
var zero x509util.Extension
|
||||
|
||||
var rawValues []asn1.RawValue
|
||||
for _, dnsName := range dnsNames {
|
||||
rawValue, err := x509util.SubjectAlternativeName{
|
||||
Type: x509util.DNSType, Value: dnsName,
|
||||
}.RawValue()
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
rawValues = append(rawValues, rawValue)
|
||||
}
|
||||
|
||||
for _, emailAddress := range emailAddresses {
|
||||
rawValue, err := x509util.SubjectAlternativeName{
|
||||
Type: x509util.EmailType, Value: emailAddress,
|
||||
}.RawValue()
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
rawValues = append(rawValues, rawValue)
|
||||
}
|
||||
|
||||
for _, ip := range ipAddresses {
|
||||
rawValue, err := x509util.SubjectAlternativeName{
|
||||
Type: x509util.IPType, Value: ip.String(),
|
||||
}.RawValue()
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
rawValues = append(rawValues, rawValue)
|
||||
}
|
||||
|
||||
for _, uri := range uris {
|
||||
rawValue, err := x509util.SubjectAlternativeName{
|
||||
Type: x509util.URIType, Value: uri.String(),
|
||||
}.RawValue()
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
rawValues = append(rawValues, rawValue)
|
||||
}
|
||||
|
||||
for _, san := range sans {
|
||||
rawValue, err := san.RawValue()
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
rawValues = append(rawValues, rawValue)
|
||||
}
|
||||
|
||||
// Now marshal the rawValues into the ASN1 sequence, and create an Extension object to hold the extension
|
||||
rawBytes, err := asn1.Marshal(rawValues)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("error marshaling SubjectAlternativeName extension to ASN1: %w", err)
|
||||
}
|
||||
|
||||
return x509util.Extension{
|
||||
ID: x509util.ObjectIdentifier(oidSubjectAlternativeName),
|
||||
Critical: subjectIsEmpty,
|
||||
Value: rawBytes,
|
||||
}, nil
|
||||
}
|
||||
|
|
859
acme/challenge_tpmsimulator_test.go
Normal file
859
acme/challenge_tpmsimulator_test.go
Normal file
|
@ -0,0 +1,859 @@
|
|||
//go:build tpmsimulator
|
||||
// +build tpmsimulator
|
||||
|
||||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/google/go-attestation/attest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/keyutil"
|
||||
"go.step.sm/crypto/minica"
|
||||
"go.step.sm/crypto/tpm"
|
||||
"go.step.sm/crypto/tpm/simulator"
|
||||
tpmstorage "go.step.sm/crypto/tpm/storage"
|
||||
"go.step.sm/crypto/x509util"
|
||||
)
|
||||
|
||||
func newSimulatedTPM(t *testing.T) *tpm.TPM {
|
||||
t.Helper()
|
||||
tmpDir := t.TempDir()
|
||||
tpm, err := tpm.New(withSimulator(t), tpm.WithStore(tpmstorage.NewDirstore(tmpDir))) // TODO: provide in-memory storage implementation instead
|
||||
require.NoError(t, err)
|
||||
return tpm
|
||||
}
|
||||
|
||||
func withSimulator(t *testing.T) tpm.NewTPMOption {
|
||||
t.Helper()
|
||||
var sim simulator.Simulator
|
||||
t.Cleanup(func() {
|
||||
if sim == nil {
|
||||
return
|
||||
}
|
||||
err := sim.Close()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
sim = simulator.New()
|
||||
err := sim.Open()
|
||||
require.NoError(t, err)
|
||||
return tpm.WithSimulator(sim)
|
||||
}
|
||||
|
||||
func generateKeyID(t *testing.T, pub crypto.PublicKey) []byte {
|
||||
t.Helper()
|
||||
b, err := x509.MarshalPKIXPublicKey(pub)
|
||||
require.NoError(t, err)
|
||||
hash := sha256.Sum256(b)
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
func mustAttestTPM(t *testing.T, keyAuthorization string, permanentIdentifiers []string) ([]byte, crypto.Signer, *x509.Certificate) {
|
||||
t.Helper()
|
||||
aca, err := minica.New(
|
||||
minica.WithName("TPM Testing"),
|
||||
minica.WithGetSignerFunc(
|
||||
func() (crypto.Signer, error) {
|
||||
return keyutil.GenerateSigner("RSA", "", 2048)
|
||||
},
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// prepare simulated TPM and create an AK
|
||||
stpm := newSimulatedTPM(t)
|
||||
eks, err := stpm.GetEKs(context.Background())
|
||||
require.NoError(t, err)
|
||||
ak, err := stpm.CreateAK(context.Background(), "first-ak")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ak)
|
||||
|
||||
// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())
|
||||
ap, err := ak.AttestationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
akp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create template and sign certificate for the AK public key
|
||||
keyID := generateKeyID(t, eks[0].Public())
|
||||
template := &x509.Certificate{
|
||||
PublicKey: akp.Public,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
sans := []x509util.SubjectAlternativeName{}
|
||||
uris := []*url.URL{{Scheme: "urn", Opaque: "ek:sha256:" + base64.StdEncoding.EncodeToString(keyID)}}
|
||||
for _, pi := range permanentIdentifiers {
|
||||
sans = append(sans, x509util.SubjectAlternativeName{
|
||||
Type: x509util.PermanentIdentifierType,
|
||||
Value: pi,
|
||||
})
|
||||
}
|
||||
asn1Value := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMModel, "SLB 9670 TPM2.0", oidTPMVersion, "7.55"))
|
||||
sans = append(sans, x509util.SubjectAlternativeName{
|
||||
Type: x509util.DirectoryNameType,
|
||||
ASN1Value: asn1Value,
|
||||
})
|
||||
ext, err := createSubjectAltNameExtension(nil, nil, nil, uris, sans, true)
|
||||
require.NoError(t, err)
|
||||
ext.Set(template)
|
||||
akCert, err := aca.Sign(template)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, akCert)
|
||||
|
||||
// create a new key attested by the AK, while including
|
||||
// the key authorization bytes as qualifying data.
|
||||
keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
|
||||
config := tpm.AttestKeyConfig{
|
||||
Algorithm: "RSA",
|
||||
Size: 2048,
|
||||
QualifyingData: keyAuthSum[:],
|
||||
}
|
||||
key, err := stpm.AttestKey(context.Background(), "first-ak", "first-key", config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
require.Equal(t, "first-key", key.Name())
|
||||
require.NotEqual(t, 0, len(key.Data()))
|
||||
require.Equal(t, "first-ak", key.AttestedBy())
|
||||
require.True(t, key.WasAttested())
|
||||
require.True(t, key.WasAttestedBy(ak))
|
||||
|
||||
signer, err := key.Signer(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// prepare the attestation object with the AK certificate chain,
|
||||
// the attested key, its metadata and the signature signed by the
|
||||
// AK.
|
||||
params, err := key.CertificationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
attObj, err := cbor.Marshal(struct {
|
||||
Format string `json:"fmt"`
|
||||
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
||||
}{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// marshal the ACME payload
|
||||
payload, err := json.Marshal(struct {
|
||||
AttObj string `json:"attObj"`
|
||||
}{
|
||||
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return payload, signer, aca.Root
|
||||
}
|
||||
|
||||
func Test_deviceAttest01ValidateWithTPMSimulator(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
ch *Challenge
|
||||
db DB
|
||||
jwk *jose.JSONWebKey
|
||||
payload []byte
|
||||
}
|
||||
type test struct {
|
||||
args args
|
||||
wantErr *Error
|
||||
}
|
||||
tests := map[string]func(t *testing.T) test{
|
||||
"ok/doTPMAttestationFormat-storeError": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, _, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
|
||||
// parse payload, set invalid "ver", remarshal
|
||||
var p payloadType
|
||||
err := json.Unmarshal(payload, &p)
|
||||
require.NoError(t, err)
|
||||
attObj, err := base64.RawURLEncoding.DecodeString(p.AttObj)
|
||||
require.NoError(t, err)
|
||||
att := attestationObject{}
|
||||
err = cbor.Unmarshal(attObj, &att)
|
||||
require.NoError(t, err)
|
||||
att.AttStatement["ver"] = "bogus"
|
||||
attObj, err = cbor.Marshal(struct {
|
||||
Format string `json:"fmt"`
|
||||
AttStatement map[string]interface{} `json:"attStmt,omitempty"`
|
||||
}{
|
||||
Format: "tpm",
|
||||
AttStatement: att.AttStatement,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
payload, err = json.Marshal(struct {
|
||||
AttObj string `json:"attObj"`
|
||||
}{
|
||||
AttObj: base64.RawURLEncoding.EncodeToString(attObj),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusInvalid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.12345678", updch.Value)
|
||||
|
||||
err := NewError(ErrorBadAttestationStatementType, `version "bogus" is not supported`)
|
||||
|
||||
assert.EqualError(t, updch.Error.Err, err.Err.Error())
|
||||
assert.Equal(t, err.Type, updch.Error.Type)
|
||||
assert.Equal(t, err.Detail, updch.Error.Detail)
|
||||
assert.Equal(t, err.Status, updch.Error.Status)
|
||||
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok with invalid PermanentIdentifier SAN": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, _, root := mustAttestTPM(t, keyAuth, []string{"device.id.12345678"}) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.99999999",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusInvalid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.99999999", updch.Value)
|
||||
|
||||
err := NewError(ErrorRejectedIdentifierType, `permanent identifier does not match`).
|
||||
AddSubproblems(NewSubproblemWithIdentifier(
|
||||
ErrorMalformedType,
|
||||
Identifier{Type: "permanent-identifier", Value: "device.id.99999999"},
|
||||
`challenge identifier "device.id.99999999" doesn't match any of the attested hardware identifiers ["device.id.12345678"]`,
|
||||
))
|
||||
|
||||
assert.EqualError(t, updch.Error.Err, err.Err.Error())
|
||||
assert.Equal(t, err.Type, updch.Error.Type)
|
||||
assert.Equal(t, err.Detail, updch.Error.Detail)
|
||||
assert.Equal(t, err.Status, updch.Error.Status)
|
||||
assert.Equal(t, err.Subproblems, updch.Error.Subproblems)
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, signer, root := mustAttestTPM(t, keyAuth, nil) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "azID", az.ID)
|
||||
assert.Equal(t, fingerprint, az.Fingerprint)
|
||||
return nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusValid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.12345678", updch.Value)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
"ok with PermanentIdentifier SAN": func(t *testing.T) test {
|
||||
jwk, keyAuth := mustAccountAndKeyAuthorization(t, "token")
|
||||
payload, signer, root := mustAttestTPM(t, keyAuth, []string{"device.id.12345678"}) // TODO: value(s) for AK cert?
|
||||
caRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: root.Raw})
|
||||
ctx := NewProvisionerContext(context.Background(), mustAttestationProvisioner(t, caRoot))
|
||||
return test{
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
jwk: jwk,
|
||||
ch: &Challenge{
|
||||
ID: "chID",
|
||||
AuthorizationID: "azID",
|
||||
Token: "token",
|
||||
Type: "device-attest-01",
|
||||
Status: StatusPending,
|
||||
Value: "device.id.12345678",
|
||||
},
|
||||
payload: payload,
|
||||
db: &MockDB{
|
||||
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
|
||||
assert.Equal(t, "azID", id)
|
||||
return &Authorization{ID: "azID"}, nil
|
||||
},
|
||||
MockUpdateAuthorization: func(ctx context.Context, az *Authorization) error {
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "azID", az.ID)
|
||||
assert.Equal(t, fingerprint, az.Fingerprint)
|
||||
return nil
|
||||
},
|
||||
MockUpdateChallenge: func(ctx context.Context, updch *Challenge) error {
|
||||
assert.Equal(t, "chID", updch.ID)
|
||||
assert.Equal(t, "token", updch.Token)
|
||||
assert.Equal(t, StatusValid, updch.Status)
|
||||
assert.Equal(t, ChallengeType("device-attest-01"), updch.Type)
|
||||
assert.Equal(t, "device.id.12345678", updch.Value)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
}
|
||||
},
|
||||
}
|
||||
for name, run := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc := run(t)
|
||||
|
||||
if err := deviceAttest01Validate(tc.args.ctx, tc.args.ch, tc.args.db, tc.args.jwk, tc.args.payload); err != nil {
|
||||
assert.Error(t, tc.wantErr)
|
||||
assert.EqualError(t, err, tc.wantErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.Nil(t, tc.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newBadAttestationStatementError(msg string) *Error {
|
||||
return &Error{
|
||||
Type: "urn:ietf:params:acme:error:badAttestationStatement",
|
||||
Status: 400,
|
||||
Err: errors.New(msg),
|
||||
}
|
||||
}
|
||||
|
||||
func newInternalServerError(msg string) *Error {
|
||||
return &Error{
|
||||
Type: "urn:ietf:params:acme:error:serverInternal",
|
||||
Status: 500,
|
||||
Err: errors.New(msg),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
oidPermanentIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3}
|
||||
oidHardwareModuleNameIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 4}
|
||||
)
|
||||
|
||||
func Test_doTPMAttestationFormat(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
aca, err := minica.New(
|
||||
minica.WithName("TPM Testing"),
|
||||
minica.WithGetSignerFunc(
|
||||
func() (crypto.Signer, error) {
|
||||
return keyutil.GenerateSigner("RSA", "", 2048)
|
||||
},
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
acaRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: aca.Root.Raw})
|
||||
|
||||
// prepare simulated TPM and create an AK
|
||||
stpm := newSimulatedTPM(t)
|
||||
eks, err := stpm.GetEKs(context.Background())
|
||||
require.NoError(t, err)
|
||||
ak, err := stpm.CreateAK(context.Background(), "first-ak")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, ak)
|
||||
|
||||
// extract the AK public key // TODO(hs): replace this when there's a simpler method to get the AK public key (e.g. ak.Public())
|
||||
ap, err := ak.AttestationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
akp, err := attest.ParseAKPublic(attest.TPMVersion20, ap.Public)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create template and sign certificate for the AK public key
|
||||
keyID := generateKeyID(t, eks[0].Public())
|
||||
template := &x509.Certificate{
|
||||
PublicKey: akp.Public,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
sans := []x509util.SubjectAlternativeName{}
|
||||
uris := []*url.URL{{Scheme: "urn", Opaque: "ek:sha256:" + base64.StdEncoding.EncodeToString(keyID)}}
|
||||
asn1Value := []byte(fmt.Sprintf(`{"extraNames":[{"type": %q, "value": %q},{"type": %q, "value": %q},{"type": %q, "value": %q}]}`, oidTPMManufacturer, "1414747215", oidTPMModel, "SLB 9670 TPM2.0", oidTPMVersion, "7.55"))
|
||||
sans = append(sans, x509util.SubjectAlternativeName{
|
||||
Type: x509util.DirectoryNameType,
|
||||
ASN1Value: asn1Value,
|
||||
})
|
||||
ext, err := createSubjectAltNameExtension(nil, nil, nil, uris, sans, true)
|
||||
require.NoError(t, err)
|
||||
ext.Set(template)
|
||||
akCert, err := aca.Sign(template)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, akCert)
|
||||
|
||||
invalidTemplate := &x509.Certificate{
|
||||
PublicKey: akp.Public,
|
||||
IsCA: false,
|
||||
UnknownExtKeyUsage: []asn1.ObjectIdentifier{oidTCGKpAIKCertificate},
|
||||
}
|
||||
invalidAKCert, err := aca.Sign(invalidTemplate)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, invalidAKCert)
|
||||
|
||||
// generate a JWK and the key authorization value
|
||||
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
||||
require.NoError(t, err)
|
||||
keyAuthorization, err := KeyAuthorization("token", jwk)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a new key attested by the AK, while including
|
||||
// the key authorization bytes as qualifying data.
|
||||
keyAuthSum := sha256.Sum256([]byte(keyAuthorization))
|
||||
config := tpm.AttestKeyConfig{
|
||||
Algorithm: "RSA",
|
||||
Size: 2048,
|
||||
QualifyingData: keyAuthSum[:],
|
||||
}
|
||||
key, err := stpm.AttestKey(context.Background(), "first-ak", "first-key", config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
params, err := key.CertificationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
signer, err := key.Signer(context.Background())
|
||||
require.NoError(t, err)
|
||||
fingerprint, err := keyutil.Fingerprint(signer.Public())
|
||||
require.NoError(t, err)
|
||||
|
||||
// attest another key and get its certification parameters
|
||||
anotherKey, err := stpm.AttestKey(context.Background(), "first-ak", "another-key", config)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, key)
|
||||
anotherKeyParams, err := anotherKey.CertificationParameters(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
prov Provisioner
|
||||
ch *Challenge
|
||||
jwk *jose.JSONWebKey
|
||||
att *attestationObject
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *tpmAttestationData
|
||||
expErr *Error
|
||||
}{
|
||||
{"ok", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, nil},
|
||||
{"fail ver not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("ver not present")},
|
||||
{"fail ver type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": []interface{}{},
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("ver not present")},
|
||||
{"fail bogus ver", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "bogus",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError(`version "bogus" is not supported`)},
|
||||
{"fail x5c not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c not present")},
|
||||
{"fail x5c type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": [][]byte{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c not present")},
|
||||
{"fail x5c empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is empty")},
|
||||
{"fail leaf type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{"leaf", aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed")},
|
||||
{"fail leaf parse", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw[:100], aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed: x509: malformed certificate")},
|
||||
{"fail intermediate type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, "intermediate"},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed")},
|
||||
{"fail intermediate parse", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw[:100]},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is malformed: x509: malformed certificate")},
|
||||
{"fail roots", args{ctx, mustAttestationProvisioner(t, nil), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newInternalServerError("no root CA bundle available to verify the attestation certificate")},
|
||||
{"fail verify", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "step",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("x5c is not valid: x509: certificate signed by unknown authority")},
|
||||
{"fail validateAKCertificate", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{invalidAKCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("AK certificate is not valid: missing TPM manufacturer")},
|
||||
{"fail pubArea not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid pubArea in attestation statement")},
|
||||
{"fail pubArea type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": []interface{}{},
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid pubArea in attestation statement")},
|
||||
{"fail pubArea empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": []byte{},
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("pubArea is empty")},
|
||||
{"fail sig not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid sig in attestation statement")},
|
||||
{"fail sig type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": []interface{}{},
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid sig in attestation statement")},
|
||||
{"fail sig empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": []byte{},
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("sig is empty")},
|
||||
{"fail certInfo not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid certInfo in attestation statement")},
|
||||
{"fail certInfo type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": []interface{}{},
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid certInfo in attestation statement")},
|
||||
{"fail certInfo empty", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": []byte{},
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("certInfo is empty")},
|
||||
{"fail alg not present", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid alg in attestation statement")},
|
||||
{"fail alg type", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(0), // invalid alg
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid alg 0 in attestation statement")},
|
||||
{"fail attestation verification", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": anotherKeyParams.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("invalid certification parameters: certification refers to a different key")},
|
||||
{"fail keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "token"}, &jose.JSONWebKey{Key: []byte("not an asymmetric key")}, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), // RS256
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newInternalServerError("failed creating key auth digest: error generating JWK thumbprint: square/go-jose: unknown key type '[]uint8'")},
|
||||
{"fail different keyAuthorization", args{ctx, mustAttestationProvisioner(t, acaRoot), &Challenge{Token: "aDifferentToken"}, jwk, &attestationObject{
|
||||
Format: "tpm",
|
||||
AttStatement: map[string]interface{}{
|
||||
"ver": "2.0",
|
||||
"x5c": []interface{}{akCert.Raw, aca.Intermediate.Raw},
|
||||
"alg": int64(-257), //
|
||||
"sig": params.CreateSignature,
|
||||
"certInfo": params.CreateAttestation,
|
||||
"pubArea": params.Public,
|
||||
},
|
||||
}}, nil, newBadAttestationStatementError("key authorization does not match")},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := doTPMAttestationFormat(tt.args.ctx, tt.args.prov, tt.args.ch, tt.args.jwk, tt.args.att)
|
||||
if tt.expErr != nil {
|
||||
var ae *Error
|
||||
if assert.True(t, errors.As(err, &ae)) {
|
||||
assert.EqualError(t, err, tt.expErr.Error())
|
||||
assert.Equal(t, ae.StatusCode(), tt.expErr.StatusCode())
|
||||
assert.Equal(t, ae.Type, tt.expErr.Type)
|
||||
}
|
||||
assert.Nil(t, got)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, got) {
|
||||
assert.Equal(t, akCert, got.Certificate)
|
||||
assert.Equal(t, [][]*x509.Certificate{
|
||||
{
|
||||
akCert, aca.Intermediate, aca.Root,
|
||||
},
|
||||
}, got.VerifiedChains)
|
||||
assert.Equal(t, fingerprint, got.Fingerprint)
|
||||
assert.Empty(t, got.PermanentIdentifiers) // currently expected to be always empty
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -48,7 +48,7 @@ func (c ACMEChallenge) Validate() error {
|
|||
type ACMEAttestationFormat string
|
||||
|
||||
const (
|
||||
// APPLE is the format used to enable device-attest-01 on apple devices.
|
||||
// APPLE is the format used to enable device-attest-01 on Apple devices.
|
||||
APPLE ACMEAttestationFormat = "apple"
|
||||
|
||||
// STEP is the format used to enable device-attest-01 on devices that
|
||||
|
@ -57,7 +57,7 @@ const (
|
|||
// TODO(mariano): should we rename this to something else.
|
||||
STEP ACMEAttestationFormat = "step"
|
||||
|
||||
// TPM is the format used to enable device-attest-01 on TPMs.
|
||||
// TPM is the format used to enable device-attest-01 with TPMs.
|
||||
TPM ACMEAttestationFormat = "tpm"
|
||||
)
|
||||
|
||||
|
@ -184,7 +184,7 @@ func (p *ACME) Init(config Config) (err error) {
|
|||
}
|
||||
|
||||
// Parse attestation roots.
|
||||
// The pool will be nil if the there are not roots.
|
||||
// The pool will be nil if there are no roots.
|
||||
if rest := p.AttestationRoots; len(rest) > 0 {
|
||||
var block *pem.Block
|
||||
var hasCert bool
|
||||
|
|
42
go.mod
42
go.mod
|
@ -3,29 +3,20 @@ module github.com/smallstep/certificates
|
|||
go 1.18
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.4.1
|
||||
cloud.google.com/go/security v1.13.0
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.225 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0
|
||||
github.com/go-chi/chi v4.1.2+incompatible
|
||||
github.com/go-kit/kit v0.10.0 // indirect
|
||||
github.com/go-piv/piv-go v1.11.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/go-attestation v0.4.4-0.20220404204839-8820d49b18d9
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/google/go-tpm v0.3.3
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/googleapis/gax-go/v2 v2.8.0
|
||||
github.com/hashicorp/vault/api v1.9.0
|
||||
github.com/hashicorp/vault/api/auth/approle v0.4.0
|
||||
github.com/hashicorp/vault/api/auth/kubernetes v0.4.0
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||
github.com/micromdm/scep/v2 v2.1.0
|
||||
github.com/newrelic/go-agent/v3 v3.21.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
|
@ -38,20 +29,20 @@ require (
|
|||
github.com/urfave/cli v1.22.12
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
|
||||
go.step.sm/cli-utils v0.7.5
|
||||
go.step.sm/crypto v0.28.0
|
||||
go.step.sm/crypto v0.29.0
|
||||
go.step.sm/linkedca v0.19.0
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0
|
||||
golang.org/x/net v0.8.0
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
|
||||
google.golang.org/api v0.114.0
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
|
||||
google.golang.org/grpc v1.54.0
|
||||
google.golang.org/protobuf v1.30.0
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.0 // indirect
|
||||
cloud.google.com/go/compute v1.18.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v0.12.0 // indirect
|
||||
|
@ -66,6 +57,8 @@ require (
|
|||
github.com/AzureAD/microsoft-authentication-library-for-go v0.9.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.235 // indirect
|
||||
github.com/cenkalti/backoff/v3 v3.0.0 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
|
@ -74,14 +67,22 @@ require (
|
|||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/go-kit/kit v0.10.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/go-piv/piv-go v1.11.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.4 // indirect
|
||||
github.com/google/go-tpm-tools v0.3.10 // indirect
|
||||
github.com/google/go-tspi v0.3.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
|
@ -106,16 +107,20 @@ require (
|
|||
github.com/klauspost/compress v1.15.11 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1 // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/schollz/jsonstore v1.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
|
@ -124,9 +129,11 @@ require (
|
|||
go.etcd.io/bbolt v1.3.7 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/oauth2 v0.6.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/time v0.1.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
@ -138,3 +145,6 @@ require (
|
|||
|
||||
// use github.com/smallstep/pkcs7 fork with patches applied
|
||||
replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20230302202335-4c094085c948
|
||||
|
||||
// use github.com/smallstep/go-attestation fork with patches for Windows AK support applied
|
||||
replace github.com/google/go-attestation v0.4.4-0.20220404204839-8820d49b18d9 => github.com/smallstep/go-attestation v0.4.4-0.20230224121042-1bcb20a75add
|
||||
|
|
Loading…
Reference in a new issue