From 89ace4cedb7b211aaf2fc92914ed8590a42badb1 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 26 Nov 2018 19:42:24 -0800 Subject: [PATCH] Do not require schema in ca-url Fixes smallstep/ca-component#117 --- Gopkg.lock | 11 ++++------- Gopkg.toml | 4 ++++ command/ca/bootstrap.go | 8 ++++++++ command/ca/ca.go | 42 +++++++++++++++++++++++++++++++++++++++++ command/ca/token.go | 42 +++++++++++++++++++++++++++++++---------- 5 files changed, 90 insertions(+), 17 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index bffd4c3e3..1ed49a09f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -46,12 +46,9 @@ [[projects]] branch = "master" - digest = "1:5f3371d6e432e3c4e0ceabb14b5c0679827ce8b96bbd963fa923977782e8e99b" + digest = "1:454adc7f974228ff789428b6dc098638c57a64aa0718f0bd61e53d3cd39d7a75" name = "github.com/chzyer/readline" - packages = [ - ".", - "runes", - ] + packages = ["."] pruneopts = "UT" revision = "2972be24d48e78746da79ba8e24e8b488c9880de" @@ -303,7 +300,7 @@ [[projects]] branch = "master" - digest = "1:d8bad241aed7d771c1018f0f8985c189d73fa27a4fcdbeaf06d4e72d80e32d9a" + digest = "1:46c14c1e0339fe0a4e607de7837d1b4d7a9881e730cb880aab79a8c2cc993572" name = "github.com/smallstep/certificates" packages = [ "api", @@ -314,7 +311,7 @@ "server", ] pruneopts = "UT" - revision = "9d7e1ab9ec3bf62185dbc7f304f99a0f6b9a213f" + revision = "60fbed7da053054b48b7c7d7d6244c94eea2a27e" [[projects]] branch = "master" diff --git a/Gopkg.toml b/Gopkg.toml index c6c969ed9..446ba32ed 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -62,3 +62,7 @@ required = [ [prune] go-tests = true unused-packages = true + +[[constraint]] + branch = "master" + name = "github.com/smallstep/certificates" \ No newline at end of file diff --git a/command/ca/bootstrap.go b/command/ca/bootstrap.go index 61c356f74..5019c262c 100644 --- a/command/ca/bootstrap.go +++ b/command/ca/bootstrap.go @@ -73,11 +73,19 @@ func bootstrapAction(ctx *cli.Context) error { return errs.FileError(err, configFile) } + // Serialize root _, err = pemutil.Serialize(resp.RootPEM.Certificate, pemutil.ToFile(rootFile, 0600)) if err != nil { return err } + // make sure to store the url with https + caURL, err = completeURL(caURL) + if err != nil { + return err + } + + // Serialize defaults.json b, err := json.MarshalIndent(bootstrapConfig{ CA: caURL, Fingerprint: fingerprint, diff --git a/command/ca/ca.go b/command/ca/ca.go index dcfc5a496..7fbe6bb6a 100644 --- a/command/ca/ca.go +++ b/command/ca/ca.go @@ -1,6 +1,10 @@ package ca import ( + "net/url" + "strings" + + "github.com/pkg/errors" "github.com/smallstep/cli/command" "github.com/smallstep/cli/command/ca/provisioner" "github.com/urfave/cli" @@ -132,3 +136,41 @@ unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", generating key.`, } ) + +// completeURL parses and validates the given URL. It supports general +// URLs like https://ca.smallstep.com[:port][/path], and incomplete URLs like +// ca.smallstep.com[:port][/path]. +func completeURL(rawurl string) (string, error) { + u, err := url.Parse(rawurl) + if err != nil { + return "", errors.Wrapf(err, "error parsing url '%s'", rawurl) + } + + // URLs are generally parsed as: + // [scheme:][//[userinfo@]host][/]path[?query][#fragment] + // But URLs that do not start with a slash after the scheme are interpreted as + // scheme:opaque[?query][#fragment] + if u.Opaque == "" { + if u.Scheme == "" { + u.Scheme = "https" + } + if u.Host == "" { + // rawurl looks like ca.smallstep.com or ca.smallstep.com/1.0/sign + if u.Path != "" { + parts := strings.SplitN(u.Path, "/", 2) + u.Host = parts[0] + if len(parts) == 2 { + u.Path = parts[1] + } else { + u.Path = "" + } + return completeURL(u.String()) + } + return "", errors.Errorf("error parsing url '%s'", rawurl) + } + return u.String(), nil + } + // scheme:opaque[?query][#fragment] + // rawurl looks like ca.smallstep.com:443 or ca.smallstep.com:443/1.0/sign + return completeURL("https://" + rawurl) +} diff --git a/command/ca/token.go b/command/ca/token.go index 7cfd62724..4705ac652 100644 --- a/command/ca/token.go +++ b/command/ca/token.go @@ -5,6 +5,7 @@ import ( "fmt" "net/url" "os" + "strings" "time" "github.com/pkg/errors" @@ -168,11 +169,11 @@ func newTokenAction(ctx *cli.Context) error { return errs.RequiredWithFlag(ctx, "offline", "key") } - audience, err := url.Parse(caURL) - if err != nil || audience.Scheme != "https" { - return errs.InvalidFlagValue(ctx, "ca-url", caURL, "") + // Get audience from ca-url + audience, err := parseAudience(ctx) + if err != nil { + return err } - audience = audience.ResolveReference(&url.URL{Path: "/1.0/sign"}) var opts []jose.Option if len(passwordFile) != 0 { @@ -183,7 +184,7 @@ func newTokenAction(ctx *cli.Context) error { return err } - token, err = generateToken(subject, kid, issuer, audience.String(), root, notBefore, notAfter, jwk) + token, err = generateToken(subject, kid, issuer, audience, root, notBefore, notAfter, jwk) if err != nil { return err } @@ -200,6 +201,27 @@ func newTokenAction(ctx *cli.Context) error { return nil } +// parseAudience creates the ca audience url from the ca-url +func parseAudience(ctx *cli.Context) (string, error) { + caURL := ctx.String("ca-url") + if len(caURL) == 0 { + return "", errs.RequiredFlag(ctx, "ca-url") + } + + audience, err := url.Parse(caURL) + if err != nil { + return "", errs.InvalidFlagValue(ctx, "ca-url", caURL, "") + } + switch strings.ToLower(audience.Scheme) { + case "https", "": + audience.Scheme = "https" + audience = audience.ResolveReference(&url.URL{Path: "/1.0/sign"}) + return audience.String(), nil + default: + return "", errs.InvalidFlagValue(ctx, "ca-url", caURL, "") + } +} + // generateToken generates a provisioning or bootstrap token with the given // parameters. func generateToken(sub, kid, iss, aud, root string, notBefore, notAfter time.Time, jwk *jose.JSONWebKey) (string, error) { @@ -238,11 +260,11 @@ func generateToken(sub, kid, iss, aud, root string, notBefore, notAfter time.Tim // newTokenFlow implements the common flow used to generate a token func newTokenFlow(ctx *cli.Context, subject, caURL, root, kid, issuer, passwordFile, keyFile string, notBefore, notAfter time.Time) (string, error) { - audience, err := url.Parse(caURL) - if err != nil || audience.Scheme != "https" { - return "", errs.InvalidFlagValue(ctx, "ca-url", caURL, "") + // Get audience from ca-url + audience, err := parseAudience(ctx) + if err != nil { + return "", err } - audience = audience.ResolveReference(&url.URL{Path: "/1.0/sign"}) provisioners, err := pki.GetProvisioners(caURL, root) if err != nil { @@ -327,7 +349,7 @@ func newTokenFlow(ctx *cli.Context, subject, caURL, root, kid, issuer, passwordF } } - return generateToken(subject, kid, issuer, audience.String(), root, notBefore, notAfter, jwk) + return generateToken(subject, kid, issuer, audience, root, notBefore, notAfter, jwk) } func parseTimeOrDuration(s string) (time.Time, bool) {