diff --git a/authority/provisioner/acme.go b/authority/provisioner/acme.go index 37a4fd28..f86ab07d 100644 --- a/authority/provisioner/acme.go +++ b/authority/provisioner/acme.go @@ -41,6 +41,38 @@ func (c ACMEChallenge) Validate() error { } } +// ACMEPlatform 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 // provisioning flow. type ACME struct { @@ -58,8 +90,12 @@ type ACME struct { // value is not set the default http-01, dns-01 and tls-alpn-01 challenges // will be enabled, device-attest-01 will be disabled. Challenges []ACMEChallenge `json:"challenges,omitempty"` - Claims *Claims `json:"claims,omitempty"` - Options *Options `json:"options,omitempty"` + // AttestationFormats contains the enabled attestation formats for this + // 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 } @@ -123,6 +159,11 @@ func (p *ACME) Init(config Config) (err error) { 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) return @@ -222,3 +263,21 @@ func (p *ACME) IsChallengeEnabled(ctx context.Context, challenge ACMEChallenge) } 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 +} diff --git a/authority/provisioner/acme_test.go b/authority/provisioner/acme_test.go index 641b7f38..6152a8c9 100644 --- a/authority/provisioner/acme_test.go +++ b/authority/provisioner/acme_test.go @@ -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) { p, err := generateACME() assert.FatalError(t, err) @@ -93,17 +114,24 @@ func TestACME_Init(t *testing.T) { 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 { return ProvisionerValidateTest{ p: &ACME{Name: "foo", Type: "bar"}, } }, - "ok with challenges": func(t *testing.T) ProvisionerValidateTest { + "ok attestation": func(t *testing.T) ProvisionerValidateTest { return ProvisionerValidateTest{ p: &ACME{ - Name: "foo", - Type: "bar", - Challenges: []ACMEChallenge{DNS_01, DEVICE_ATTEST_01}, + Name: "foo", + Type: "bar", + 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) + } + }) + } +}