Merge pull request #1048 from smallstep/attest-platform

Attestation Formats
pull/1052/head
Mariano Cano 2 years ago committed by GitHub
commit 666f695616
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

4
.gitignore vendored

@ -6,6 +6,10 @@
*.so *.so
*.dylib *.dylib
# Go Workspaces
go.work
go.work.sum
# Test binary, build with `go test -c` # Test binary, build with `go test -c`
*.test *.test

@ -45,6 +45,10 @@ func (*fakeProvisioner) IsChallengeEnabled(ctx context.Context, challenge provis
return true return true
} }
func (*fakeProvisioner) IsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool {
return true
}
func (*fakeProvisioner) AuthorizeRevoke(ctx context.Context, token string) error { return nil } func (*fakeProvisioner) AuthorizeRevoke(ctx context.Context, token string) error { return nil }
func (*fakeProvisioner) GetID() string { return "" } func (*fakeProvisioner) GetID() string { return "" }
func (*fakeProvisioner) GetName() string { return "" } func (*fakeProvisioner) GetName() string { return "" }

@ -26,6 +26,7 @@ import (
"time" "time"
"github.com/fxamacker/cbor/v2" "github.com/fxamacker/cbor/v2"
"github.com/smallstep/certificates/authority/provisioner"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/pemutil" "go.step.sm/crypto/pemutil"
) )
@ -341,6 +342,12 @@ func deviceAttest01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose
return WrapErrorISE(err, "error unmarshalling CBOR") return WrapErrorISE(err, "error unmarshalling CBOR")
} }
prov := MustProvisionerFromContext(ctx)
if !prov.IsAttestationFormatEnabled(ctx, provisioner.ACMEAttestationFormat(att.Format)) {
return storeError(ctx, db, ch, true,
NewError(ErrorBadAttestationStatementType, "attestation format %q is not enabled", att.Format))
}
switch att.Format { switch att.Format {
case "apple": case "apple":
data, err := doAppleAttestationFormat(ctx, ch, db, &att) data, err := doAppleAttestationFormat(ctx, ch, db, &att)

@ -72,6 +72,7 @@ type Provisioner interface {
AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error) AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error)
AuthorizeRevoke(ctx context.Context, token string) error AuthorizeRevoke(ctx context.Context, token string) error
IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool
IsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool
GetID() string GetID() string
GetName() string GetName() string
DefaultTLSCertDuration() time.Duration DefaultTLSCertDuration() time.Duration
@ -110,7 +111,8 @@ type MockProvisioner struct {
MauthorizeOrderIdentifier func(ctx context.Context, identifier provisioner.ACMEIdentifier) error MauthorizeOrderIdentifier func(ctx context.Context, identifier provisioner.ACMEIdentifier) error
MauthorizeSign func(ctx context.Context, ott string) ([]provisioner.SignOption, error) MauthorizeSign func(ctx context.Context, ott string) ([]provisioner.SignOption, error)
MauthorizeRevoke func(ctx context.Context, token string) error MauthorizeRevoke func(ctx context.Context, token string) error
MisChallengeEnabled func(Ctx context.Context, challenge provisioner.ACMEChallenge) bool MisChallengeEnabled func(ctx context.Context, challenge provisioner.ACMEChallenge) bool
MisAttFormatEnabled func(ctx context.Context, format provisioner.ACMEAttestationFormat) bool
MdefaultTLSCertDuration func() time.Duration MdefaultTLSCertDuration func() time.Duration
MgetOptions func() *provisioner.Options MgetOptions func() *provisioner.Options
} }
@ -147,7 +149,7 @@ func (m *MockProvisioner) AuthorizeRevoke(ctx context.Context, token string) err
return m.Merr return m.Merr
} }
// AuthorizeChallenge mock // IsChallengeEnabled mock
func (m *MockProvisioner) IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool { func (m *MockProvisioner) IsChallengeEnabled(ctx context.Context, challenge provisioner.ACMEChallenge) bool {
if m.MisChallengeEnabled != nil { if m.MisChallengeEnabled != nil {
return m.MisChallengeEnabled(ctx, challenge) return m.MisChallengeEnabled(ctx, challenge)
@ -155,6 +157,14 @@ func (m *MockProvisioner) IsChallengeEnabled(ctx context.Context, challenge prov
return m.Merr == nil return m.Merr == nil
} }
// IsAttestationFormatEnabled mock
func (m *MockProvisioner) IsAttestationFormatEnabled(ctx context.Context, format provisioner.ACMEAttestationFormat) bool {
if m.MisAttFormatEnabled != nil {
return m.MisAttFormatEnabled(ctx, format)
}
return m.Merr == nil
}
// DefaultTLSCertDuration mock // DefaultTLSCertDuration mock
func (m *MockProvisioner) DefaultTLSCertDuration() time.Duration { func (m *MockProvisioner) DefaultTLSCertDuration() time.Duration {
if m.MdefaultTLSCertDuration != nil { if m.MdefaultTLSCertDuration != nil {

@ -41,6 +41,39 @@ func (c ACMEChallenge) Validate() error {
} }
} }
// ACMEAttestationFormat represents the format used on a device-attest-01
// challenge.
type ACMEAttestationFormat string
const (
// 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
// provide attestation certificates like the PIV interface on YubiKeys.
//
// 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 ACMEAttestationFormat = "tpm"
)
// String returns a normalized version of the attestation format.
func (f ACMEAttestationFormat) String() string {
return strings.ToLower(string(f))
}
// Validate returns an error if the attestation format is not a valid one.
func (f ACMEAttestationFormat) Validate() error {
switch ACMEAttestationFormat(f.String()) {
case APPLE, STEP, TPM:
return nil
default:
return fmt.Errorf("acme attestation format %q is not supported", f)
}
}
// ACME is the acme provisioner type, an entity that can authorize the ACME // ACME is the acme provisioner type, an entity that can authorize the ACME
// provisioning flow. // provisioning flow.
type ACME struct { type ACME struct {
@ -58,8 +91,12 @@ type ACME struct {
// value is not set the default http-01, dns-01 and tls-alpn-01 challenges // value is not set the default http-01, dns-01 and tls-alpn-01 challenges
// will be enabled, device-attest-01 will be disabled. // will be enabled, device-attest-01 will be disabled.
Challenges []ACMEChallenge `json:"challenges,omitempty"` Challenges []ACMEChallenge `json:"challenges,omitempty"`
Claims *Claims `json:"claims,omitempty"` // AttestationFormats contains the enabled attestation formats for this
Options *Options `json:"options,omitempty"` // provisioner. If this value is not set the default apple, step and tpm
// will be used.
AttestationFormats []ACMEAttestationFormat `json:"attestationFormats,omitempty"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
ctl *Controller ctl *Controller
} }
@ -123,6 +160,11 @@ func (p *ACME) Init(config Config) (err error) {
return err return err
} }
} }
for _, f := range p.AttestationFormats {
if err := f.Validate(); err != nil {
return err
}
}
p.ctl, err = NewController(p, p.Claims, config, p.Options) p.ctl, err = NewController(p, p.Claims, config, p.Options)
return return
@ -222,3 +264,21 @@ func (p *ACME) IsChallengeEnabled(ctx context.Context, challenge ACMEChallenge)
} }
return false return false
} }
// IsAttestationFormatEnabled checks if the given attestation format is enabled.
// By default apple, step and tpm are enabled, to disable any of them the
// AttestationFormat provisioner property should have at least one element.
func (p *ACME) IsAttestationFormatEnabled(ctx context.Context, format ACMEAttestationFormat) bool {
enabledFormats := []ACMEAttestationFormat{
APPLE, STEP, TPM,
}
if len(p.AttestationFormats) > 0 {
enabledFormats = p.AttestationFormats
}
for _, f := range enabledFormats {
if strings.EqualFold(string(f), string(format)) {
return true
}
}
return false
}

@ -35,6 +35,27 @@ func TestACMEChallenge_Validate(t *testing.T) {
} }
} }
func TestACMEAttestationFormat_Validate(t *testing.T) {
tests := []struct {
name string
f ACMEAttestationFormat
wantErr bool
}{
{"apple", APPLE, false},
{"step", STEP, false},
{"tpm", TPM, false},
{"uppercase", "APPLE", false},
{"fail", "FOO", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.f.Validate(); (err != nil) != tt.wantErr {
t.Errorf("ACMEAttestationFormat.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestACME_Getters(t *testing.T) { func TestACME_Getters(t *testing.T) {
p, err := generateACME() p, err := generateACME()
assert.FatalError(t, err) assert.FatalError(t, err)
@ -93,17 +114,24 @@ func TestACME_Init(t *testing.T) {
err: errors.New("acme challenge \"zar\" is not supported"), err: errors.New("acme challenge \"zar\" is not supported"),
} }
}, },
"fail-bad-attestation-format": func(t *testing.T) ProvisionerValidateTest {
return ProvisionerValidateTest{
p: &ACME{Name: "foo", Type: "bar", AttestationFormats: []ACMEAttestationFormat{APPLE, "zar"}},
err: errors.New("acme attestation format \"zar\" is not supported"),
}
},
"ok": func(t *testing.T) ProvisionerValidateTest { "ok": func(t *testing.T) ProvisionerValidateTest {
return ProvisionerValidateTest{ return ProvisionerValidateTest{
p: &ACME{Name: "foo", Type: "bar"}, p: &ACME{Name: "foo", Type: "bar"},
} }
}, },
"ok with challenges": func(t *testing.T) ProvisionerValidateTest { "ok attestation": func(t *testing.T) ProvisionerValidateTest {
return ProvisionerValidateTest{ return ProvisionerValidateTest{
p: &ACME{ p: &ACME{
Name: "foo", Name: "foo",
Type: "bar", Type: "bar",
Challenges: []ACMEChallenge{DNS_01, DEVICE_ATTEST_01}, Challenges: []ACMEChallenge{DNS_01, DEVICE_ATTEST_01},
AttestationFormats: []ACMEAttestationFormat{APPLE, STEP},
}, },
} }
}, },
@ -282,3 +310,39 @@ func TestACME_IsChallengeEnabled(t *testing.T) {
}) })
} }
} }
func TestACME_IsAttestationFormatEnabled(t *testing.T) {
ctx := context.Background()
type fields struct {
AttestationFormats []ACMEAttestationFormat
}
type args struct {
ctx context.Context
format ACMEAttestationFormat
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{"ok", fields{[]ACMEAttestationFormat{APPLE, STEP, TPM}}, args{ctx, TPM}, true},
{"ok empty apple", fields{nil}, args{ctx, APPLE}, true},
{"ok empty step", fields{nil}, args{ctx, STEP}, true},
{"ok empty tpm", fields{[]ACMEAttestationFormat{}}, args{ctx, "tpm"}, true},
{"ok uppercase", fields{[]ACMEAttestationFormat{APPLE, STEP, TPM}}, args{ctx, "STEP"}, true},
{"fail apple", fields{[]ACMEAttestationFormat{STEP, TPM}}, args{ctx, APPLE}, false},
{"fail step", fields{[]ACMEAttestationFormat{APPLE, TPM}}, args{ctx, STEP}, false},
{"fail step", fields{[]ACMEAttestationFormat{APPLE, STEP}}, args{ctx, TPM}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &ACME{
AttestationFormats: tt.fields.AttestationFormats,
}
if got := p.IsAttestationFormatEnabled(tt.args.ctx, tt.args.format); got != tt.want {
t.Errorf("ACME.IsAttestationFormatEnabled() = %v, want %v", got, tt.want)
}
})
}
}

@ -748,14 +748,15 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
case *linkedca.ProvisionerDetails_ACME: case *linkedca.ProvisionerDetails_ACME:
cfg := d.ACME cfg := d.ACME
return &provisioner.ACME{ return &provisioner.ACME{
ID: p.Id, ID: p.Id,
Type: p.Type.String(), Type: p.Type.String(),
Name: p.Name, Name: p.Name,
ForceCN: cfg.ForceCn, ForceCN: cfg.ForceCn,
RequireEAB: cfg.RequireEab, RequireEAB: cfg.RequireEab,
Challenges: challengesToCertificates(cfg.Challenges), Challenges: challengesToCertificates(cfg.Challenges),
Claims: claims, AttestationFormats: attestationFormatsToCertificates(cfg.AttestationFormats),
Options: options, Claims: claims,
Options: options,
}, nil }, nil
case *linkedca.ProvisionerDetails_OIDC: case *linkedca.ProvisionerDetails_OIDC:
cfg := d.OIDC cfg := d.OIDC
@ -1002,8 +1003,9 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro
Details: &linkedca.ProvisionerDetails{ Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_ACME{ Data: &linkedca.ProvisionerDetails_ACME{
ACME: &linkedca.ACMEProvisioner{ ACME: &linkedca.ACMEProvisioner{
ForceCn: p.ForceCN, ForceCn: p.ForceCN,
Challenges: challengesToLinkedca(p.Challenges), Challenges: challengesToLinkedca(p.Challenges),
AttestationFormats: attestationFormatsToLinkedca(p.AttestationFormats),
}, },
}, },
}, },
@ -1135,7 +1137,7 @@ func challengesToCertificates(challenges []linkedca.ACMEProvisioner_ChallengeTyp
ret = append(ret, provisioner.HTTP_01) ret = append(ret, provisioner.HTTP_01)
case linkedca.ACMEProvisioner_DNS_01: case linkedca.ACMEProvisioner_DNS_01:
ret = append(ret, provisioner.DNS_01) ret = append(ret, provisioner.DNS_01)
case linkedca.ACMEProvisioner_TLS_ALPN_O1: case linkedca.ACMEProvisioner_TLS_ALPN_01:
ret = append(ret, provisioner.TLS_ALPN_01) ret = append(ret, provisioner.TLS_ALPN_01)
case linkedca.ACMEProvisioner_DEVICE_ATTEST_01: case linkedca.ACMEProvisioner_DEVICE_ATTEST_01:
ret = append(ret, provisioner.DEVICE_ATTEST_01) ret = append(ret, provisioner.DEVICE_ATTEST_01)
@ -1155,10 +1157,44 @@ func challengesToLinkedca(challenges []provisioner.ACMEChallenge) []linkedca.ACM
case provisioner.DNS_01: case provisioner.DNS_01:
ret = append(ret, linkedca.ACMEProvisioner_DNS_01) ret = append(ret, linkedca.ACMEProvisioner_DNS_01)
case provisioner.TLS_ALPN_01: case provisioner.TLS_ALPN_01:
ret = append(ret, linkedca.ACMEProvisioner_TLS_ALPN_O1) ret = append(ret, linkedca.ACMEProvisioner_TLS_ALPN_01)
case provisioner.DEVICE_ATTEST_01: case provisioner.DEVICE_ATTEST_01:
ret = append(ret, linkedca.ACMEProvisioner_DEVICE_ATTEST_01) ret = append(ret, linkedca.ACMEProvisioner_DEVICE_ATTEST_01)
} }
} }
return ret return ret
} }
// attestationFormatsToCertificates converts linkedca attestation formats to
// provisioner ones skipping the unknown ones.
func attestationFormatsToCertificates(formats []linkedca.ACMEProvisioner_AttestationFormatType) []provisioner.ACMEAttestationFormat {
ret := make([]provisioner.ACMEAttestationFormat, 0, len(formats))
for _, f := range formats {
switch f {
case linkedca.ACMEProvisioner_APPLE:
ret = append(ret, provisioner.APPLE)
case linkedca.ACMEProvisioner_STEP:
ret = append(ret, provisioner.STEP)
case linkedca.ACMEProvisioner_TPM:
ret = append(ret, provisioner.TPM)
}
}
return ret
}
// attestationFormatsToLinkedca converts provisioner attestation formats to
// linkedca ones skipping the unknown ones.
func attestationFormatsToLinkedca(formats []provisioner.ACMEAttestationFormat) []linkedca.ACMEProvisioner_AttestationFormatType {
ret := make([]linkedca.ACMEProvisioner_AttestationFormatType, 0, len(formats))
for _, f := range formats {
switch provisioner.ACMEAttestationFormat(f.String()) {
case provisioner.APPLE:
ret = append(ret, linkedca.ACMEProvisioner_APPLE)
case provisioner.STEP:
ret = append(ret, linkedca.ACMEProvisioner_STEP)
case provisioner.TPM:
ret = append(ret, linkedca.ACMEProvisioner_TPM)
}
}
return ret
}

@ -40,7 +40,7 @@ require (
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
go.step.sm/cli-utils v0.7.4 go.step.sm/cli-utils v0.7.4
go.step.sm/crypto v0.19.0 go.step.sm/crypto v0.19.0
go.step.sm/linkedca v0.18.1-0.20220824000236-47827c8eb300 go.step.sm/linkedca v0.18.1-0.20220909212924-c69cf68797cb
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/net v0.0.0-20220607020251-c690dde0001d golang.org/x/net v0.0.0-20220607020251-c690dde0001d
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect

@ -641,8 +641,8 @@ go.step.sm/cli-utils v0.7.4/go.mod h1:taSsY8haLmXoXM3ZkywIyRmVij/4Aj0fQbNTlJvv71
go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
go.step.sm/crypto v0.19.0 h1:WxjUDeTDpuPZ1IR3v6c4jc6WdlQlS5IYYQBhfnG5uW0= go.step.sm/crypto v0.19.0 h1:WxjUDeTDpuPZ1IR3v6c4jc6WdlQlS5IYYQBhfnG5uW0=
go.step.sm/crypto v0.19.0/go.mod h1:qZ+pNU1nV+THwP7TPTNCRMRr9xrRURhETTAK7U5psfw= go.step.sm/crypto v0.19.0/go.mod h1:qZ+pNU1nV+THwP7TPTNCRMRr9xrRURhETTAK7U5psfw=
go.step.sm/linkedca v0.18.1-0.20220824000236-47827c8eb300 h1:kDqCHUh4jqqqf+m5IXjFjlwsTXuIXpf5ciGKigqJH14= go.step.sm/linkedca v0.18.1-0.20220909212924-c69cf68797cb h1:qCG7i7PAZcTDLqyFmOzBBl5tfyHI033U5jONS9DuN+8=
go.step.sm/linkedca v0.18.1-0.20220824000236-47827c8eb300/go.mod h1:qSuYlIIhvPmA2+DSSS03E2IXhbXWTLW61Xh9zDQJ3VM= go.step.sm/linkedca v0.18.1-0.20220909212924-c69cf68797cb/go.mod h1:qSuYlIIhvPmA2+DSSS03E2IXhbXWTLW61Xh9zDQJ3VM=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=

Loading…
Cancel
Save