diff --git a/.gitignore b/.gitignore index 4f7a04a6c..c399faf07 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ *.so *.dylib +# Go Workspaces +go.work +go.work.sum + # Test binary, build with `go test -c` *.test diff --git a/command/ca/certificate.go b/command/ca/certificate.go index 3a96cde58..dcd6be4ee 100644 --- a/command/ca/certificate.go +++ b/command/ca/certificate.go @@ -167,6 +167,7 @@ multiple SANs. The '--san' flag and the '--token' flag are mutually exclusive.`, flags.Size, flags.NotAfter, flags.NotBefore, + flags.AttestationURI, flags.Force, flags.Offline, flags.PasswordFile, @@ -186,10 +187,15 @@ multiple SANs. The '--san' flag and the '--token' flag are mutually exclusive.`, } func certificateAction(ctx *cli.Context) error { - if err := errs.NumberOfArguments(ctx, 3); err != nil { + if err := errs.MinMaxNumberOfArguments(ctx, 2, 3); err != nil { return err } + // Allow two arguments with the attestation uri. + if ctx.NArg() == 2 && ctx.String("attestation-uri") == "" { + return errs.TooFewArguments(ctx) + } + args := ctx.Args() subject := args.Get(0) crtFile, keyFile := args.Get(1), args.Get(2) diff --git a/command/ca/provisioner/add.go b/command/ca/provisioner/add.go index 4b55972a5..93720ab10 100644 --- a/command/ca/provisioner/add.go +++ b/command/ca/provisioner/add.go @@ -815,7 +815,7 @@ func acmeChallengeToLinkedca(challenges []string) []linkedca.ACMEProvisioner_Cha case "dns-01": ret = append(ret, linkedca.ACMEProvisioner_DNS_01) case "tls-alpn-01": - ret = append(ret, linkedca.ACMEProvisioner_TLS_ALPN_O1) + ret = append(ret, linkedca.ACMEProvisioner_TLS_ALPN_01) case "device-attest-01": ret = append(ret, linkedca.ACMEProvisioner_DEVICE_ATTEST_01) } diff --git a/flags/flags.go b/flags/flags.go index 8740f7ff2..890b893bb 100644 --- a/flags/flags.go +++ b/flags/flags.go @@ -398,6 +398,11 @@ flag exists so it can be configured in $STEPPATH/config/defaults.json.`, Name: "kms", Usage: "The to configure a Cloud KMS or an HSM.", } + + AttestationURI = cli.StringFlag{ + Name: "attestation-uri", + Usage: "The KMS used for attestation.", + } ) // ParseTimeOrDuration is a helper that returns the time or the current time diff --git a/go.mod b/go.mod index 3931657d5..fdaca0ce7 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/Microsoft/go-winio v0.4.14 github.com/ThomasRooney/gexpect v0.0.0-20161231170123-5482f0350944 + github.com/fxamacker/cbor/v2 v2.4.0 github.com/google/uuid v1.3.0 github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 github.com/manifoldco/promptui v0.9.0 @@ -14,8 +15,8 @@ require ( github.com/shurcooL/sanitized_anchor_name v1.0.0 github.com/slackhq/nebula v1.5.2 github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 - github.com/smallstep/certificates v0.22.2-0.20220908202628-b2119e9f2c68 - github.com/smallstep/certinfo v1.7.0 + github.com/smallstep/certificates v0.22.2-0.20220912233041-df975122a0da + github.com/smallstep/certinfo v1.8.1 github.com/smallstep/truststore v0.12.0 github.com/smallstep/zcrypto v0.0.0-20210924233136-66c2600f6e71 github.com/smallstep/zlint v0.0.0-20180727184541-d84eaafe274f @@ -24,8 +25,8 @@ require ( go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.5 go.step.sm/crypto v0.19.0 - go.step.sm/linkedca v0.18.1-0.20220908140115-147cf75c525d - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 + go.step.sm/linkedca v0.19.0-rc.1 + golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d golang.org/x/net v0.0.0-20220607020251-c690dde0001d golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 @@ -81,7 +82,6 @@ require ( github.com/envoyproxy/protoc-gen-validate v0.3.0-java // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/fullstorydev/grpcurl v1.8.2 // indirect - github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-kit/kit v0.10.0 // indirect github.com/go-logfmt/logfmt v0.5.0 // indirect @@ -195,9 +195,3 @@ require ( howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect sigs.k8s.io/yaml v1.2.0 // indirect ) - -// replace github.com/smallstep/certificates => ../certificates -// replace github.com/smallstep/certinfo => ../certinfo -// replace go.step.sm/linkedca => ../linkedca -// replace go.step.sm/cli-utils => ../cli-utils -// replace go.step.sm/crypto => ../crypto diff --git a/go.sum b/go.sum index d8bac859c..a541d6997 100644 --- a/go.sum +++ b/go.sum @@ -891,10 +891,10 @@ github.com/slackhq/nebula v1.5.2/go.mod h1:xaCM6wqbFk/NRmmUe1bv88fWBm3a1UioXJVIp github.com/smallstep/assert v0.0.0-20180720014142-de77670473b5/go.mod h1:TC9A4+RjIOS+HyTH7wG17/gSqVv95uDw2J64dQZx7RE= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= -github.com/smallstep/certificates v0.22.2-0.20220908202628-b2119e9f2c68 h1:s5S1IZJM4hgK78d/QQov2Vx3Re5GBbb+r4mIaCo7iJk= -github.com/smallstep/certificates v0.22.2-0.20220908202628-b2119e9f2c68/go.mod h1:rGaw/vJYWunycyCMNu2Pq2XHVjtRNMdNO3U1gOSOfcQ= -github.com/smallstep/certinfo v1.7.0 h1:1VzGgeSFLdXO242mRUkJu02epXV6E9uawjhPH0H2LzA= -github.com/smallstep/certinfo v1.7.0/go.mod h1:QRjP6s+cuishA6cdB//RX357ysYGz/QxlpWGyWjnfII= +github.com/smallstep/certificates v0.22.2-0.20220912233041-df975122a0da h1:Y0iP/MZpdl1bpt7D2WYnwa8te8+k0vDcyYQ5tRKLjbI= +github.com/smallstep/certificates v0.22.2-0.20220912233041-df975122a0da/go.mod h1:e5p/JQ4oT3IArpw1WA5BB86TBE6s6IaIiqnTx7JwRrk= +github.com/smallstep/certinfo v1.8.1 h1:M6z2uUtK6MVva0E7ZIzev9I/3mZl6Sfvvk4DiNob/s0= +github.com/smallstep/certinfo v1.8.1/go.mod h1:rRIcDFvXO8nrxnmqhrLrhOwiFtJ4iMRa9naNnZXcNU8= github.com/smallstep/nosql v0.4.0 h1:Go3WYwttUuvwqMtFiiU4g7kBIlY+hR0bIZAqVdakQ3M= github.com/smallstep/nosql v0.4.0/go.mod h1:yKZT5h7cdIVm6wEKM9+jN5dgK80Hljpuy8HNsnI7Gzo= github.com/smallstep/truststore v0.12.0 h1:973Aa6fA7Ob/GCxqziosDzkQq6tV0Le6IUe4sikyW+U= @@ -1075,8 +1075,8 @@ go.step.sm/cli-utils v0.7.5/go.mod h1:taSsY8haLmXoXM3ZkywIyRmVij/4Aj0fQbNTlJvv71 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/go.mod h1:qZ+pNU1nV+THwP7TPTNCRMRr9xrRURhETTAK7U5psfw= -go.step.sm/linkedca v0.18.1-0.20220908140115-147cf75c525d h1:grL/lXVzdUc51Aa2exEsRJl+Q9eaUwHc31heb/ims58= -go.step.sm/linkedca v0.18.1-0.20220908140115-147cf75c525d/go.mod h1:qSuYlIIhvPmA2+DSSS03E2IXhbXWTLW61Xh9zDQJ3VM= +go.step.sm/linkedca v0.19.0-rc.1 h1:8XcQvanelK1g0ijl5/itmmAIsqD2QSMHGqcWzJwwJCU= +go.step.sm/linkedca v0.19.0-rc.1/go.mod h1:G35baT7Qnh6VsRCjzSfi5xsYw0ERrU+I1aIuZswMBeA= 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.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -1123,8 +1123,9 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo= +golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/internal/cryptoutil/cryptoutil.go b/internal/cryptoutil/cryptoutil.go index a605bc6fc..da5b0b023 100644 --- a/internal/cryptoutil/cryptoutil.go +++ b/internal/cryptoutil/cryptoutil.go @@ -16,6 +16,13 @@ import ( "go.step.sm/crypto/pemutil" ) +// Attestor is the interface implemented by step-kms-plugin using the key, sign, +// and attest commands. +type Attestor interface { + crypto.Signer + Attest() ([]byte, error) +} + // CreateSigner reads a key from a file with a given name or creates a signer // with the given kms and name uri. func CreateSigner(kms, name string, opts ...pemutil.Options) (crypto.Signer, error) { @@ -33,6 +40,12 @@ func CreateSigner(kms, name string, opts ...pemutil.Options) (crypto.Signer, err return newKMSSigner(kms, name) } +// CreateAttestor creates an attestor that will use `step-kms-plugin` with the +// given kms and name. +func CreateAttestor(kms, name string) (Attestor, error) { + return newKMSSigner(kms, name) +} + // IsKMSSigner returns true if the given signer uses the step-kms-plugin signer. func IsKMSSigner(signer crypto.Signer) (ok bool) { _, ok = signer.(*kmsSigner) @@ -76,7 +89,7 @@ func exitError(cmd *exec.Cmd, err error) error { } // newKMSSigner creates a signer using `step-kms-plugin` as the signer. -func newKMSSigner(kms, key string) (crypto.Signer, error) { +func newKMSSigner(kms, key string) (*kmsSigner, error) { name, err := plugin.LookPath("kms") if err != nil { return nil, err @@ -152,3 +165,21 @@ func (s *kmsSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) } return base64.StdEncoding.DecodeString(string(out)) } + +// Attest returns an attestation certificate using the `step-kms-plugin attest` +// command. +func (s *kmsSigner) Attest() ([]byte, error) { + args := []string{"attest"} + if s.kms != "" { + args = append(args, "--kms", s.kms) + } + args = append(args, s.key) + + //nolint:gosec // arguments controlled by step. + cmd := exec.Command(s.name, args...) + out, err := cmd.Output() + if err != nil { + return nil, exitError(cmd, err) + } + return out, nil +} diff --git a/utils/cautils/acme_flow.go b/utils/cautils/acme_flow.go index 535c949fd..d68d29356 100644 --- a/utils/cautils/acme_flow.go +++ b/utils/cautils/acme_flow.go @@ -29,11 +29,16 @@ func ACMECreateCertFlow(ctx *cli.Context, provisionerName string) error { } ui.PrintSelected("Certificate", certFile) - _, err = pemutil.Serialize(af.priv, pemutil.ToFile(keyFile, 0600)) - if err != nil { - return errors.WithStack(err) + // We won't have a private key with attestation certificates + if af.priv != nil { + _, err = pemutil.Serialize(af.priv, pemutil.ToFile(keyFile, 0600)) + if err != nil { + return errors.WithStack(err) + } + ui.PrintSelected("Private Key", keyFile) + } else if v := ctx.String("attestation-uri"); v != "" { + ui.PrintSelected("Private Key", v) } - ui.PrintSelected("Private Key", keyFile) return nil } diff --git a/utils/cautils/acmeutils.go b/utils/cautils/acmeutils.go index 4ef4c4556..b0abc6056 100644 --- a/utils/cautils/acmeutils.go +++ b/utils/cautils/acmeutils.go @@ -2,10 +2,17 @@ package cautils import ( "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" "crypto/rand" + "crypto/rsa" + "crypto/sha256" "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "encoding/base64" "encoding/json" "encoding/pem" "fmt" @@ -15,18 +22,21 @@ import ( "strings" "time" + "github.com/fxamacker/cbor/v2" "github.com/pkg/errors" "github.com/smallstep/certificates/acme" acmeAPI "github.com/smallstep/certificates/acme/api" "github.com/smallstep/certificates/ca" "github.com/smallstep/certificates/pki" - "github.com/smallstep/cli/crypto/keys" "github.com/smallstep/cli/flags" + "github.com/smallstep/cli/internal/cryptoutil" "github.com/smallstep/cli/jose" "github.com/smallstep/cli/utils" "github.com/urfave/cli" "go.step.sm/cli-utils/errs" "go.step.sm/cli-utils/ui" + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/pemutil" ) func startHTTPServer(addr, token, keyAuth string) *http.Server { @@ -193,6 +203,7 @@ func serveAndValidateHTTPChallenge(ctx *cli.Context, ac *ca.ACMEClient, ch *acme } func authorizeOrder(ctx *cli.Context, ac *ca.ACMEClient, o *acme.Order) error { + isAttest := (ctx.String("attestation-uri") != "") for _, azURL := range o.AuthorizationURLs { az, err := ac.GetAuthz(azURL) if err != nil { @@ -207,13 +218,20 @@ func authorizeOrder(ctx *cli.Context, ac *ca.ACMEClient, o *acme.Order) error { chValidated := false for _, ch := range az.Challenges { // TODO: Allow other types of challenges (not just http). - if ch.Type == "http-01" { + if ch.Type == "http-01" && !isAttest { if err := serveAndValidateHTTPChallenge(ctx, ac, ch, ident); err != nil { return err } chValidated = true break } + if ch.Type == "device-attest-01" && isAttest { + if err := doDeviceAttestation(ctx, ac, ch, ident); err != nil { + return err + } + chValidated = true + break + } } if !chValidated { return errors.Errorf("unable to validate any challenges for identifier: %s", ident) @@ -287,6 +305,198 @@ func validateSANsForACME(sans []string) ([]string, []net.IP, error) { return dnsNames, ips, nil } +func createNewOrderRequest(ctx *cli.Context, acmeDir, subject string, sans []string) (interface{}, []string, []net.IP, error) { + dnsNames, ips, err := validateSANsForACME(sans) + if err != nil { + return nil, nil, nil, err + } + + var idents []acme.Identifier + for _, dns := range dnsNames { + idents = append(idents, acme.Identifier{ + Type: "dns", + Value: dns, + }) + } + for _, ip := range ips { + idents = append(idents, acme.Identifier{ + Type: "ip", + Value: ip.String(), + }) + } + + if strings.Contains(acmeDir, "letsencrypt") { + // LetsEncrypt does not support NotBefore and NotAfter attributes in + // orders. + if ctx.IsSet("not-before") || ctx.IsSet("not-after") { + return nil, nil, nil, errors.New( + "LetsEncrypt public CA does not support NotBefore/NotAfter attributes for certificates. " + + "Instead, each certificate has a default lifetime of 3 months.", + ) + } + + // LetsEncrypt requires that the Common Name of the Certificate also be + // represented as a DNSName in the SAN extension, and therefore must be + // authorized as part of the ACME order. + hasSubject := false + for _, n := range idents { + if n.Value == subject { + hasSubject = true + } + } + + if !hasSubject { + dnsNames = append(dnsNames, subject) + idents = append(idents, acme.Identifier{ + Type: "dns", + Value: subject, + }) + } + + return struct { + Identifiers []acme.Identifier + }{Identifiers: idents}, dnsNames, ips, nil + } + + // parse times or durations + nbf, naf, err := flags.ParseTimeDuration(ctx) + if err != nil { + return nil, nil, nil, err + } + + // check if the list of identifiers for which to request a certificate + // already contains the subject + hasSubject := false + for _, n := range idents { + if n.Value == subject { + hasSubject = true + } + } + + // if the subject is not yet included in the slice of identifiers, it is + // added to either the DNS names or IP addresses slice and the corresponding + // type of identifier is added to the slice of identifiers. + if !hasSubject { + if ip := net.ParseIP(subject); ip != nil { + ips = append(ips, ip) + idents = append(idents, acme.Identifier{ + Type: "ip", + Value: ip.String(), + }) + } else { + dnsNames = append(dnsNames, subject) + idents = append(idents, acme.Identifier{ + Type: "dns", + Value: subject, + }) + } + } + + return acmeAPI.NewOrderRequest{ + Identifiers: idents, + NotAfter: naf.Time(), + NotBefore: nbf.Time(), + }, dnsNames, ips, nil +} + +type attestationPayload struct { + AttObj string `json:"attObj"` +} + +type attestationObject struct { + Format string `json:"fmt"` + AttStatement map[string]interface{} `json:"attStmt,omitempty"` +} + +func doDeviceAttestation(ctx *cli.Context, ac *ca.ACMEClient, ch *acme.Challenge, identifier string) error { + attestor, err := cryptoutil.CreateAttestor("", ctx.String("attestation-uri")) + if err != nil { + return err + } + pemData, err := attestor.Attest() + if err != nil { + return err + } + + data, err := acme.KeyAuthorization(ch.Token, ac.Key) + if err != nil { + return errors.Wrap(err, "error generating ACME key authorization") + } + + var alg int64 + var digest []byte + var opts crypto.SignerOpts + switch k := attestor.Public().(type) { + case *ecdsa.PublicKey: + if k.Curve != elliptic.P256() { + return fmt.Errorf("unsupported elliptic curve %s", k.Curve) + } + alg = -7 // ES256 + opts = crypto.SHA256 + sum := sha256.Sum256([]byte(data)) + digest = sum[:] + case *rsa.PublicKey: + // TODO(mariano): support for PS256 (-37) + alg = -257 // RS256 + opts = crypto.SHA256 + sum := sha256.Sum256([]byte(data)) + digest = sum[:] + case ed25519.PublicKey: + alg = -8 // EdDSA + opts = crypto.Hash(0) + digest = []byte(data) + default: + return fmt.Errorf("unsupported public key type %T", k) + } + + // Sign proves possession of private key. Per recommendation at + // https://w3c.github.io/webauthn/#sctn-signature-attestation-types, we use + // CBOR to encode the signature. + sig, err := attestor.Sign(rand.Reader, digest, opts) + if err != nil { + return errors.Wrap(err, "error signing key authorization") + } + sig, err = cbor.Marshal(sig) + if err != nil { + return errors.Wrap(err, "error marshaling signature") + } + + certs, err := pemutil.ParseCertificateBundle(pemData) + if err != nil { + return err + } + + x5c := make([][]byte, len(certs)) + for i, c := range certs { + x5c[i] = c.Raw + } + + // step format is based on the "packed" format described in + // https://w3c.github.io/webauthn/#sctn-attestation but with the authData + // omitted as described in the device-attest-01 RFC. + obj := &attestationObject{ + Format: "step", + AttStatement: map[string]interface{}{ + "alg": alg, + "sig": sig, + "x5c": x5c, + }, + } + + b, err := cbor.Marshal(obj) + if err != nil { + return err + } + + payload, err := json.Marshal(attestationPayload{ + AttObj: base64.RawURLEncoding.EncodeToString(b), + }) + if err != nil { + return fmt.Errorf("error marshaling payload: %w", err) + } + return ac.ValidateWithPayload(ch.URL, payload) +} + type acmeFlowOp func(*acmeFlow) error func withProvisionerName(name string) acmeFlowOp { @@ -412,64 +622,18 @@ func (af *acmeFlow) getClientTruststoreOption(mergeRootCAs bool) (ca.ClientOptio } func (af *acmeFlow) GetCertificate() ([]*x509.Certificate, error) { - dnsNames, ips, err := validateSANsForACME(af.sans) - if err != nil { - return nil, err - } - - var idents []acme.Identifier - for _, dns := range dnsNames { - idents = append(idents, acme.Identifier{ - Type: "dns", - Value: dns, - }) - } - for _, ip := range ips { - idents = append(idents, acme.Identifier{ - Type: "ip", - Value: ip.String(), - }) - } - var ( - orderPayload []byte - clientOps []ca.ClientOption + err error + newOrderRequest interface{} + dnsNames []string + ips []net.IP ) - ops, err := af.getClientTruststoreOption(af.ctx.IsSet("acme")) - if err != nil { - return nil, err - } - - clientOps = append(clientOps, ops) - - if strings.Contains(af.acmeDir, "letsencrypt") { - // LetsEncrypt does not support NotBefore and NotAfter attributes in orders. - if af.ctx.IsSet("not-before") || af.ctx.IsSet("not-after") { - return nil, errors.New("LetsEncrypt public CA does not support NotBefore/NotAfter " + - "attributes for certificates. Instead, each certificate has a default lifetime of 3 months.") - } - // LetsEncrypt requires that the Common Name of the Certificate also be - // represented as a DNSName in the SAN extension, and therefore must be - // authorized as part of the ACME order. - hasSubject := false - for _, n := range idents { - if n.Value == af.subject { - hasSubject = true - } - } - if !hasSubject { - dnsNames = append(dnsNames, af.subject) - idents = append(idents, acme.Identifier{ - Type: "dns", - Value: af.subject, - }) - } - orderPayload, err = json.Marshal(struct { - Identifiers []acme.Identifier - }{Identifiers: idents}) + attestationURI := af.ctx.String("attestation-uri") + if attestationURI == "" { + newOrderRequest, dnsNames, ips, err = createNewOrderRequest(af.ctx, af.acmeDir, af.subject, af.sans) if err != nil { - return nil, errors.Wrap(err, "error marshaling new letsencrypt order request") + return nil, err } } else { // parse times or durations @@ -478,45 +642,28 @@ func (af *acmeFlow) GetCertificate() ([]*x509.Certificate, error) { return nil, err } - // check if the list of identifiers for which to - // request a certificate already contains the subject - hasSubject := false - for _, n := range idents { - if n.Value == af.subject { - hasSubject = true - } - } - // if the subject is not yet included in the slice - // of identifiers, it is added to either the DNS names - // or IP addresses slice and the corresponding type of - // identifier is added to the slice of identifers. - if !hasSubject { - if ip := net.ParseIP(af.subject); ip != nil { - ips = append(ips, ip) - idents = append(idents, acme.Identifier{ - Type: "ip", - Value: ip.String(), - }) - } else { - dnsNames = append(dnsNames, af.subject) - idents = append(idents, acme.Identifier{ - Type: "dns", - Value: af.subject, - }) - } - } - nor := acmeAPI.NewOrderRequest{ - Identifiers: idents, - NotAfter: naf.Time(), - NotBefore: nbf.Time(), - } - orderPayload, err = json.Marshal(nor) - if err != nil { - return nil, errors.Wrap(err, "error marshaling new order request") + // We currently do not accept other SANs with attestation certificates. + newOrderRequest = acmeAPI.NewOrderRequest{ + Identifiers: []acme.Identifier{{ + Type: "permanent-identifier", + Value: af.subject, + }}, + NotBefore: nbf.Time(), + NotAfter: naf.Time(), } } - ac, err := ca.NewACMEClient(af.acmeDir, af.ctx.StringSlice("contact"), clientOps...) + orderPayload, err := json.Marshal(newOrderRequest) + if err != nil { + return nil, errors.Wrap(err, "error marshaling order request") + } + + ops, err := af.getClientTruststoreOption(af.ctx.IsSet("acme")) + if err != nil { + return nil, err + } + + ac, err := ca.NewACMEClient(af.acmeDir, af.ctx.StringSlice("contact"), ops) if err != nil { return nil, errors.Wrapf(err, "error initializing ACME client with server %s", af.acmeDir) } @@ -531,25 +678,39 @@ func (af *acmeFlow) GetCertificate() ([]*x509.Certificate, error) { } if af.csr == nil { - insecure := af.ctx.Bool("insecure") - kty, crv, size, err := utils.GetKeyDetailsFromCLI(af.ctx, insecure, "kty", "curve", "size") - if err != nil { - return nil, err - } - af.priv, err = keys.GenerateKey(kty, crv, size) - if err != nil { - return nil, errors.Wrap(err, "error generating private key") + var signer crypto.Signer + var template *x509.CertificateRequest + if attestationURI == "" { + insecure := af.ctx.Bool("insecure") + kty, crv, size, err := utils.GetKeyDetailsFromCLI(af.ctx, insecure, "kty", "curve", "size") + if err != nil { + return nil, err + } + signer, err = keyutil.GenerateSigner(kty, crv, size) + if err != nil { + return nil, errors.Wrap(err, "error generating private key") + } + af.priv = signer + template = &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: af.subject, + }, + DNSNames: dnsNames, + IPAddresses: ips, + } + } else { + signer, err = cryptoutil.CreateSigner(attestationURI, attestationURI) + if err != nil { + return nil, err + } + template = &x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: af.subject, + }, + } } - _csr := &x509.CertificateRequest{ - Subject: pkix.Name{ - CommonName: af.subject, - }, - DNSNames: dnsNames, - IPAddresses: ips, - } - var csrBytes []byte - csrBytes, err = x509.CreateCertificateRequest(rand.Reader, _csr, af.priv) + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, template, signer) if err != nil { return nil, errors.Wrap(err, "error creating certificate request") }