Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions command/ca/provisioner/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ ACME
**step ca provisioner add** <name> **--type**=ACME
[**--force-cn**] [**--require-eab**] [**--challenge**=<challenge>]
[**--attestation-format**=<format>] [**--attestation-roots**=<file>]
[**--issuer-domain-name**=<domain>] [**--account-hash-prefix**=<prefix>]
[**--admin-cert**=<file>] [**--admin-key**=<file>]
[**--admin-subject**=<subject>] [**--admin-provisioner**=<name>] [**--admin-password-file**=<file>]
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>] [**--ca-config**=<file>]
Expand Down Expand Up @@ -152,6 +153,8 @@ SCEP
challengeFlag, // ACME + SCEP
attestationFormatFlag, // ACME
attestationRootsFlag, // ACME
issuerDomainNameFlag, // ACME
accountHashPrefixFlag, // ACME

// SCEP provisioner flags
scepCapabilitiesFlag,
Expand Down Expand Up @@ -268,6 +271,13 @@ Create an ACME provisioner for device attestation:
step ca provisioner add attestation --type ACME --challenge device-attest-01
'''

Create an ACME provisioner with persistent DNS validation:
'''
step ca provisioner add acme --type ACME --challenge dns-persist-01 \
--issuer-domain-name ca.example.com \
--account-hash-prefix https://ca.example.com/acme/acme/account-hash/
'''

Create an K8SSA provisioner:
'''
step ca provisioner add kube --type K8SSA --ssh --public-key key.pub
Expand Down Expand Up @@ -609,6 +619,10 @@ func createACMEDetails(ctx *cli.Context) (*linkedca.ProvisionerDetails, error) {
acmeAttestationFormatToLinkedca(ctx.StringSlice("attestation-format")),
),
AttestationRoots: attestationRoots,
IssuerDomainNames: sliceutil.RemoveDuplicates(
ctx.StringSlice("issuer-domain-name"),
),
AccountHashPrefix: ctx.String("account-hash-prefix"),
},
},
}, nil
Expand Down Expand Up @@ -906,16 +920,16 @@ func validateChallengeFlag(ctx *cli.Context, typ linkedca.Provisioner_Type) erro
case linkedca.Provisioner_ACME:
for _, v := range ctx.StringSlice("challenge") {
switch strings.ToLower(v) {
case "http-01", "dns-01", "tls-alpn-01", "device-attest-01":
case "http-01", "dns-01", "dns-persist-01", "tls-alpn-01", "device-attest-01":
default:
return errs.InvalidFlagValue(ctx, "challenge", v, "http-01, dns-01, tls-alpn-01 and device-attest-01")
return errs.InvalidFlagValue(ctx, "challenge", v, "http-01, dns-01, dns-persist-01, tls-alpn-01 and device-attest-01")
}
}
for _, v := range ctx.StringSlice("remove-challenge") {
switch strings.ToLower(v) {
case "http-01", "dns-01", "tls-alpn-01", "device-attest-01":
case "http-01", "dns-01", "dns-persist-01", "tls-alpn-01", "device-attest-01":
default:
return errs.InvalidFlagValue(ctx, "remove-challenge", v, "http-01, dns-01, tls-alpn-01 and device-attest-01")
return errs.InvalidFlagValue(ctx, "remove-challenge", v, "http-01, dns-01, dns-persist-01, tls-alpn-01 and device-attest-01")
}
}

Expand Down Expand Up @@ -958,6 +972,8 @@ func acmeChallengeToLinkedca(challenges []string) []linkedca.ACMEProvisioner_Cha
ret = append(ret, linkedca.ACMEProvisioner_HTTP_01)
case "dns-01":
ret = append(ret, linkedca.ACMEProvisioner_DNS_01)
case "dns-persist-01":
ret = append(ret, linkedca.ACMEProvisioner_DNS_PERSIST_01)
case "tls-alpn-01":
ret = append(ret, linkedca.ACMEProvisioner_TLS_ALPN_01)
case "device-attest-01":
Expand Down
81 changes: 81 additions & 0 deletions command/ca/provisioner/add_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package provisioner

import (
"flag"
"testing"

"github.com/smallstep/linkedca"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli"
)

// newACMEContext returns a context with the ACME provisioner flags parsed from
// args.
func newACMEContext(t *testing.T, args ...string) *cli.Context {
t.Helper()
set := flag.NewFlagSet(t.Name(), flag.ContinueOnError)
for _, f := range []cli.Flag{
forceCNFlag, requireEABFlag, challengeFlag, removeChallengeFlag,
attestationFormatFlag, removeAttestationFormatFlag, attestationRootsFlag,
issuerDomainNameFlag, removeIssuerDomainNameFlag, accountHashPrefixFlag,
} {
f.Apply(set)
}
require.NoError(t, set.Parse(args))
return cli.NewContext(&cli.App{}, set, nil)
}

func Test_validateChallengeFlag(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
}{
{"ok/dns-persist-01", []string{"--challenge", "dns-persist-01"}, false},
{"ok/dns-persist-01 uppercase", []string{"--challenge", "DNS-PERSIST-01"}, false},
{"ok/remove dns-persist-01", []string{"--remove-challenge", "dns-persist-01"}, false},
{"fail/challenge", []string{"--challenge", "dns-persist-02"}, true},
{"fail/remove-challenge", []string{"--remove-challenge", "dns-persist-02"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateChallengeFlag(newACMEContext(t, tt.args...), linkedca.Provisioner_ACME)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

func Test_acmeChallengeToLinkedca(t *testing.T) {
got := acmeChallengeToLinkedca([]string{"http-01", "dns-01", "DNS-PERSIST-01", "tls-alpn-01", "device-attest-01"})
assert.Equal(t, []linkedca.ACMEProvisioner_ChallengeType{
linkedca.ACMEProvisioner_HTTP_01,
linkedca.ACMEProvisioner_DNS_01,
linkedca.ACMEProvisioner_DNS_PERSIST_01,
linkedca.ACMEProvisioner_TLS_ALPN_01,
linkedca.ACMEProvisioner_DEVICE_ATTEST_01,
}, got)
}

func Test_createACMEDetails(t *testing.T) {
ctx := newACMEContext(t,
"--challenge", "dns-01", "--challenge", "dns-persist-01",
"--issuer-domain-name", "ca.example.com",
"--issuer-domain-name", "ca.example.net",
"--issuer-domain-name", "ca.example.com",
"--account-hash-prefix", "https://ca.example.com/acme/acme/account-hash/",
)
got, err := createACMEDetails(ctx)
require.NoError(t, err)
assert.Equal(t, &linkedca.ACMEProvisioner{
Challenges: []linkedca.ACMEProvisioner_ChallengeType{
linkedca.ACMEProvisioner_DNS_01, linkedca.ACMEProvisioner_DNS_PERSIST_01,
},
IssuerDomainNames: []string{"ca.example.com", "ca.example.net"},
AccountHashPrefix: "https://ca.example.com/acme/acme/account-hash/",
}, got.GetACME())
}
28 changes: 28 additions & 0 deletions command/ca/provisioner/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,13 @@ accessible under that domain name.
provisioning a TXT resource record containing a designated value for a specific
validation domain name.

**dns-persist-01**
: With the persistent DNS challenge, the client can prove control of a domain by
provisioning a long-lived TXT resource record that authorizes its ACME account
to get certificates for the domain. The same record can be reused for later
validations. This challenge requires the **--issuer-domain-name** and
**--account-hash-prefix** flags.

**tls-alpn-01**
: With the TLS with Application-Layer Protocol Negotiation (TLS ALPN) challenge,
the client can prove control over a domain name by configuring a TLS server to
Expand Down Expand Up @@ -401,6 +408,27 @@ certificates. Use the flag multiple times to read from multiple files.`,
Use the flag multiple times to remove multiple formats.`,
}

issuerDomainNameFlag = cli.StringSliceFlag{
Name: "issuer-domain-name",
Usage: `The issuer <domain> that dns-persist-01 TXT records must name for the
provisioner to accept them. It must be a lowercase domain name without a
trailing dot. Use the flag multiple times to configure up to 10 names.`,
}

removeIssuerDomainNameFlag = cli.StringSliceFlag{
Name: "remove-issuer-domain-name",
Usage: `Remove an issuer <domain> from the list configured in the provisioner.
Use the flag multiple times to remove multiple names.`,
}

accountHashPrefixFlag = cli.StringFlag{
Name: "account-hash-prefix",
Usage: `The URL <prefix> of the hashed account URIs that clients publish in their
dns-persist-01 TXT records, for example
'https://ca.example.com/acme/acme/account-hash/'. Records stop validating if
the prefix changes, so it must stay the same for as long as they are in use.`,
}

// SCEP provisioner flags
scepCapabilitiesFlag = cli.StringSliceFlag{
Name: "capabilities",
Expand Down
28 changes: 27 additions & 1 deletion command/ca/provisioner/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ ACME
**step ca provisioner update** <name> [**--force-cn**] [**--require-eab**]
[**--challenge**=<challenge>] [**--remove-challenge**=<challenge>]
[**--attestation-format**=<format>] [**--remove-attestation-format**=<format>]
[**--attestation-roots**=<file>] [**--admin-cert**=<file>] [**--admin-key**=<file>]
[**--attestation-roots**=<file>] [**--issuer-domain-name**=<domain>]
[**--remove-issuer-domain-name**=<domain>] [**--account-hash-prefix**=<prefix>]
[**--admin-cert**=<file>] [**--admin-key**=<file>]
[**--admin-subject**=<subject>] [**--admin-provisioner**=<name>] [**--admin-password-file**=<file>]
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>] [**--ca-config**=<file>]
[**--x509-template**=<file>] [**--x509-template-data**=<file>]
Expand Down Expand Up @@ -150,6 +152,9 @@ SCEP
attestationFormatFlag, // ACME
removeAttestationFormatFlag, // ACME
attestationRootsFlag, // ACME
issuerDomainNameFlag, // ACME
removeIssuerDomainNameFlag, // ACME
accountHashPrefixFlag, // ACME

// SCEP flags
scepCapabilitiesFlag,
Expand Down Expand Up @@ -275,6 +280,13 @@ Update an ACME provisioner:
step ca provisioner update acme --force-cn --require-eab
'''

Enable persistent DNS validation in an ACME provisioner:
'''
step ca provisioner update acme --challenge dns-persist-01 \
--issuer-domain-name ca.example.com \
--account-hash-prefix https://ca.example.com/acme/acme/account-hash/
'''

Update an K8SSA provisioner:
'''
step ca provisioner update kube --public-key key.pub --x509-min-duration 30m
Expand Down Expand Up @@ -682,6 +694,20 @@ func updateACMEDetails(ctx *cli.Context, p *linkedca.Provisioner) error {
}
details.AttestationRoots = attestationRoots
}
if ctx.IsSet("remove-issuer-domain-name") {
values := ctx.StringSlice("remove-issuer-domain-name")
details.IssuerDomainNames = sliceutil.RemoveValues(details.IssuerDomainNames, values)
}
if ctx.IsSet("issuer-domain-name") {
values := ctx.StringSlice("issuer-domain-name")
details.IssuerDomainNames = append(details.IssuerDomainNames, values...)
}
if ctx.IsSet("issuer-domain-name") || ctx.IsSet("remove-issuer-domain-name") {
details.IssuerDomainNames = sliceutil.RemoveDuplicates(details.IssuerDomainNames)
}
if ctx.IsSet("account-hash-prefix") {
details.AccountHashPrefix = ctx.String("account-hash-prefix")
}
return nil
}

Expand Down
65 changes: 65 additions & 0 deletions command/ca/provisioner/update_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package provisioner

import (
"testing"

"github.com/smallstep/linkedca"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_updateACMEDetails(t *testing.T) {
const (
oldPrefix = "https://ca.example.com/acme/acme/account-hash/"
newPrefix = "https://ca.example.org/acme/acme/account-hash/"
)
tests := []struct {
name string
args []string
want *linkedca.ACMEProvisioner
}{
{"ok/unchanged", nil, &linkedca.ACMEProvisioner{
Challenges: []linkedca.ACMEProvisioner_ChallengeType{linkedca.ACMEProvisioner_DNS_PERSIST_01},
IssuerDomainNames: []string{"ca.example.com", "ca.example.net"},
AccountHashPrefix: oldPrefix,
}},
{"ok/add", []string{
"--challenge", "dns-01",
"--issuer-domain-name", "ca.example.org",
"--issuer-domain-name", "ca.example.com",
"--account-hash-prefix", newPrefix,
}, &linkedca.ACMEProvisioner{
Challenges: []linkedca.ACMEProvisioner_ChallengeType{
linkedca.ACMEProvisioner_DNS_PERSIST_01, linkedca.ACMEProvisioner_DNS_01,
},
IssuerDomainNames: []string{"ca.example.com", "ca.example.net", "ca.example.org"},
AccountHashPrefix: newPrefix,
}},
{"ok/remove", []string{
"--remove-challenge", "dns-persist-01",
"--remove-issuer-domain-name", "ca.example.com",
"--account-hash-prefix", "",
}, &linkedca.ACMEProvisioner{
Challenges: []linkedca.ACMEProvisioner_ChallengeType{},
IssuerDomainNames: []string{"ca.example.net"},
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &linkedca.Provisioner{
Type: linkedca.Provisioner_ACME,
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_ACME{
ACME: &linkedca.ACMEProvisioner{
Challenges: []linkedca.ACMEProvisioner_ChallengeType{linkedca.ACMEProvisioner_DNS_PERSIST_01},
IssuerDomainNames: []string{"ca.example.com", "ca.example.net"},
AccountHashPrefix: oldPrefix,
},
},
},
}
require.NoError(t, updateACMEDetails(newACMEContext(t, tt.args...), p))
assert.Equal(t, tt.want, p.GetDetails().GetACME())
})
}
}
Loading
Loading